
// WormPanel.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Kreslící povrch hry. Zobrazuje:
     - pohybujícího se červíka
     - překážky (modré kostičky)
     - aktuální průměrné hodnoty FPS a UPS

   Umístěn v adresáři /WormASF
*/


import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import java.text.DecimalFormat;

import com.sun.j3d.utils.timer.J3DTimer;


public class WormPanel extends JPanel implements Runnable
{
  private static long MAX_STATS_INTERVAL = 1000000000L;
  //private static long MAX_STATS_INTERVAL = 1000L;
    // (zhruba) každou sekundu zaznamenává statistiky

  private static final int NO_DELAYS_PER_YIELD = 16;
  /* Počet snímků s prodlevou 0 ms před tím, než animační vlákno předá
     běh ostatním vláknům. */

  private static int MAX_FRAME_SKIPS = 5;   // byla 2;
    // počet snímků, které mohou být v jakémkoliv animačním cyklu přeskočeny
    // tj. aktualizuje se stav hry, ale nedojde k jejímu vykreslení

  private static int NUM_FPS = 10;
     // počet FPS hodnot uložených pro získání průměru


  private int pWidth, pHeight;   // rozměry panelu

  // pro statistické údaje
  private long statsInterval = 0L;    // v ms
  private long prevStatsTime;   
  private long totalElapsedTime = 0L;
  private long gameStartTime;
  private int timeSpentInGame = 0;    // v sekundách

  private long frameCount = 0;
  private double fpsStore[];
  private long statsCount = 0;
  private double averageFPS = 0.0;

  private long framesSkipped = 0L;
  private long totalFramesSkipped = 0L;
  private double upsStore[];
  private double averageUPS = 0.0;


  private DecimalFormat df = new DecimalFormat("0.##");  // 2 dm
  private DecimalFormat timedf = new DecimalFormat("0.####");  // 4 dm


  private Thread animator;           // vlákno provádející animaci
  private volatile boolean running = false;   // pro pozastavení animacního vlákna
  private volatile boolean isPaused = false;

  private long period;                // interval mezi vykresleními v _nanosekundách_


  private WormChase wcTop;
  private Worm fred;       // červík
  private Obstacles obs;   // překážky


  // pro ukoncení hry
  private volatile boolean gameOver = false;
  private int score = 0;
  private Font font;
  private FontMetrics metrics;

  // vykreslování mimo obrazovku
  private Graphics dbg; 
  private Image dbImage = null;



  public WormPanel(WormChase wc, long period, int w, int h)
  {
    wcTop = wc;
    this.period = period;
    pWidth = w; pHeight = h;

    setBackground(Color.white);
    setPreferredSize( new Dimension(PWIDTH, PHEIGHT));

    setFocusable(true);
    requestFocus();    // vstup od klávesnice je náš, prijímáme její události
	readyForTermination();

    // vytvoř herní komponenty
    obs = new Obstacles(wcTop);
    fred = new Worm(PWIDTH, PHEIGHT, obs);
    gameOver = false;

    addMouseListener( new MouseAdapter() {
      public void mousePressed(MouseEvent e)
      { testPress(e.getX(), e.getY()); }
    });

    // nastav font pro zprávy
    font = new Font("SansSerif", Font.BOLD, 24);
    metrics = this.getFontMetrics(font);

    // inicializuje časovací prvky
    fpsStore = new double[NUM_FPS];
    upsStore = new double[NUM_FPS];
    for (int i=0; i < NUM_FPS; i++) {
      fpsStore[i] = 0.0;
      upsStore[i] = 0.0;
    }
  }  // konec metody: WormPanel()



  private void readyForTermination()
  {
	addKeyListener( new KeyAdapter() {
	// pro snadné ukončení z celoobrazovkové konfigurace
	// naslouchej klávesám esc, q, end, ctrl-c na herní obrazovce
       public void keyPressed(KeyEvent e)
       { int keyCode = e.getKeyCode();
         if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) ||
             (keyCode == KeyEvent.VK_END) ||
             ((keyCode == KeyEvent.VK_C) && e.isControlDown()) ) {
           running = false;
         }
       }
     });
  }  // konec metody: readyForTermination()


  public void addNotify()
  // pred spuštením vlastní hry vyckej, než bude panel JPanel pridán do okna JFrame
  { super.addNotify();   // předej upozornění
    startGame();    // spusť vlákno
  }


  private void startGame()
  // inicializuj a spusť vlákno
  { 
    if (animator == null || !running) {
      animator = new Thread(this);
	  animator.start();
    }
  } // konec metody: startGame()
    

  // ------------- metody životního cyklu hry ------------
  // volají se z metody okna JFrame implementující rozhraní WindowListener


  public void resumeGame()
  // volá se pri aktivaci / de-ikonifikaci okna JFrame
  {  isPaused = false;  } 


  public void pauseGame()
  // volá se pri deaktivaci / ikonifikaci okna JFrame
  { isPaused = true;   } 


  public void stopGame() 
  // volá se pri uzavírání okna JFrame
  {  running = false;   }

  // ----------------------------------------------


  private void testPress(int x, int y)
  // je bod (x,y) blízko hlavy nebo by měla být přidána překážka?
  {
    if (!isPaused && !gameOver) {
      if (fred.nearHead(x,y)) {   // došlo ke stisku myši poblíž hlavy?
        gameOver = true;
        score =  (40 - timeSpentInGame) + (40 - obs.getNumObstacles());    
            // dej dohromady skóre
      }
      else {   // přidej překážku, je-li to možné
        if (!fred.touchedAt(x,y))   // červíkovo tělo nebylo zasaženo?
          obs.add(x,y);
      }
    }
  }  // konec metody: testPress()


  public void run()
  /* Snímky animace vykreslujeme v cyklu while. */
  {
    long beforeTime, afterTime, timeDiff, sleepTime;
    long overSleepTime = 0;
    int noDelays = 0;
    long excess = 0;
    Graphics g;

    gameStartTime = J3DTimer.getValue();
    prevStatsTime = gameStartTime;
    beforeTime = gameStartTime;

	running = true;

	while(running) {
	  gameUpdate(); 
      gameRender();   // vykresli hru do bufferu
      paintScreen();  // nakresli buffer na obrazovku

      afterTime = J3DTimer.getValue();
      timeDiff = afterTime - beforeTime;
      sleepTime = (period - timeDiff) - overSleepTime;  

      if (sleepTime > 0) {   // v cyklu ještě nějaký čas zůstal
        try {
          Thread.sleep(sleepTime/1000000L);  // nanosekundy -> milisekundy
        }
        catch(InterruptedException ex){}
        overSleepTime = (J3DTimer.getValue() - afterTime) - sleepTime;
      }
      else {    // sleepTime <= 0; snímek trval déle než-li interval
        excess -= sleepTime;  // ulož množství zbylého času
        overSleepTime = 0;

        if (++noDelays >= NO_DELAYS_PER_YIELD) {
          Thread.yield();   // dej možnost běhu ostatním vláknům
          noDelays = 0;
        }
      }

      beforeTime = J3DTimer.getValue();

      /* Trvá-li animace příliš dlouho, aktualizuj stav hry bez vykreslení,   
       abychom dostali UPS blíže k požadované hodnotě FPS */
      int skips = 0;
      while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
        excess -= period;
	    gameUpdate();    // aktualizuj stav, ale nevykresluj
        skips++;
      }
      framesSkipped += skips;

      storeStats();
	}

    printStats();
    System.exit(0);   // okno se zavírá
  } // konec metody: run()


  private void gameUpdate() 
  { if (!isPaused && !gameOver)
      fred.move();
  }  // konec metody: gameUpdate()


  private void gameRender()
  {
    if (dbImage == null){
      dbImage = createImage(PWIDTH, PHEIGHT);
      if (dbImage == null) {
        System.out.println("dbImage má hodnotu null");
        return;
      }
      else
        dbg = dbImage.getGraphics();
    }

    // vyčisti pozadí
    dbg.setColor(Color.white);
    dbg.fillRect (0, 0, PWIDTH, PHEIGHT);

	dbg.setColor(Color.blue);
    dbg.setFont(font);

    // vypiš počet snímků a prům. FPS a UPS do levého horního rohu
	// dbg.drawString("Počet snímků " + frameCount, 10, 25);
	dbg.drawString("FPS/UPS průměrně: " + df.format(averageFPS) + "/" +
                                df.format(averageUPS), 20, 25);  // was (10,55)

	dbg.setColor(Color.black);

    // nakresli prvky hry: překážky a červíka
    obs.draw(dbg);
    fred.draw(dbg);

    if (gameOver)
      gameOverMessage(dbg);
  }  // konec metody: gameRender()


  private void gameOverMessage(Graphics g)
  // V panelu vycentruj zprávu o ukončení hry.
  {
    String msg = "Hra skončila. Dosažený počet bodů: " + score;
	int x = (PWIDTH - metrics.stringWidth(msg))/2; 
	int y = (PHEIGHT - metrics.getHeight())/2;
	g.setColor(Color.red);
    g.setFont(font);
	g.drawString(msg, x, y);
  }  // konec metody: gameOverMessage()


  private void paintScreen()
  // obrázek z paměti na obrazovku přenášíme pomocí aktivního vykreslování
  { 
    Graphics g;
    try {
      g = this.getGraphics();
      if ((g != null) && (dbImage != null))
        g.drawImage(dbImage, 0, 0, null);
      Toolkit.getDefaultToolkit().sync();  // v některých systémech musíme použít synchronizaci obrazovky
      g.dispose();
    }
    catch (Exception e)
    { System.out.println("Chyba při práci s grafikou: " + e);  }
  } // konec metody: paintScreen()


  private void storeStats()
  /* Statistické údaje:
       - součet dob trvání všech dosud provedených iterací,
         skutečná doba trvání tohoto periodu,
         diff (odchylka) těchto dvou čísel

       - celkový počet volání metody paintComponent();
   
       - Počet snímků za sekundu v tomto mezidobí a průměrná
         hodnota FPS za posledních NUM_FPS periodů.

       - FPS (snímků za sekudnu) a UPS (aktualizací za sekundu) pro tento interval, 
         průměrné hodnoty FPS & UPS za posledních NUM_FPSs intervalů.

     Tyto informace ukládáme každých MAX_STATS_INTERVAL/1000 sekund.
  */
  { 
    frameCount++;
    statsInterval += period;

    // zaznamenej statistiky každých MAX_STATS_INTERVAL intervalů
    if (statsInterval >= MAX_STATS_INTERVAL) {     
      long timeNow = J3DTimer.getValue();
      timeSpentInGame = (int) ((timeNow - gameStartTime)/1000000000L);  // ns --> sekund
      wcTop.setTimeSpent( timeSpentInGame );

      long realElapsedTime = timeNow - prevStatsTime;   // čas od posledního shromažďování statistik
      totalElapsedTime += realElapsedTime;

      double timingError = 
         ((double)(realElapsedTime - statsInterval) / statsInterval) * 100.0;

      totalFramesSkipped += framesSkipped;

      double actualFPS = 0;     // vypočti nejnovější FPS u UPS
      double actualUPS = 0;
      if (totalElapsedTime > 0) {
        actualFPS = (((double)frameCount / totalElapsedTime) * 1000000000L);
        actualUPS = (((double)(frameCount + totalFramesSkipped) / totalElapsedTime) 
                                                             * 1000000000L);
      }

      // ulož poslední FPS a UPS
      fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS;
      upsStore[ (int)statsCount%NUM_FPS ] = actualUPS;
      statsCount = statsCount+1;

      double totalFPS = 0.0;     // celkem uložených hodnot FPS a UPS
      double totalUPS = 0.0;
      for (int i=0; i < NUM_FPS; i++) {
        totalFPS += fpsStore[i];
        totalUPS += upsStore[i];
      }

      if (statsCount < NUM_FPS) { // vypočti průměrné hodnoty FPS a UPS
        averageFPS = totalFPS/statsCount;
        averageUPS = totalUPS/statsCount;
      }
      else {
        averageFPS = totalFPS/NUM_FPS;
        averageUPS = totalUPS/NUM_FPS;
      }
/*
      System.out.println(timedf.format( (double) statsInterval/1000000000L) + " " + 
                    timedf.format((double) realElapsedTime/1000000000L) + "s " + 
			        df.format(timingError) + "% " + 
                    frameCount + "v " +
                    framesSkipped + "/" + totalFramesSkipped + " vynecháno; " +
                    df.format(actualFPS) + " " + df.format(averageFPS) + " prům. fps; " + 
                    df.format(actualUPS) + " " + df.format(averageUPS) + " prům. ups" );
*/
      framesSkipped = 0;
      prevStatsTime = timeNow;
      statsInterval = 0L;   // vynuluj
    }
  }  // konec metody: storeStats()


  private void printStats()
  {
    System.out.println("Snímků celkem/vynecháno: " + frameCount + " / " + totalFramesSkipped);
	System.out.println("FPS průměrně: " + df.format(averageFPS));
	System.out.println("UPS průměrně: " + df.format(averageUPS));
    System.out.println("Celkový čas: " + timeSpentInGame + " sekund");
    System.out.println("Použitých krabiček: " + obs.getNumObstacles());
  }  // konec metody: printStats()

}  // konec třídy: WormPanel
