/**
 * Gére les animations.
 */
package abstractAnimation;

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

/**
 * 
 * @author Johan Boulé
 *
 * Cette classe abstraite est une applet gérant les animations sans scintillement (avec double buffer).
 * Pour l'utiliser, on définie simplement les méthodes abtraites
 * reset(), tick() et trace() dans une classe dérivée.
 */
public abstract class DoubledBufferedAnimationApplet extends Applet implements Runnable 
{
	/**
	 * Permet l'exécution en tant qu'application
	 * @param applet
	 * @param arguments
	 */
	public static void main(final Applet applet, String [] arguments)
	{
		Frame frame = new Frame();
		frame.addWindowListener
		(
				new WindowAdapter()
				{
					public void windowClosing(WindowEvent windowEvent)
					{
						applet.stop();
						applet.destroy();
						System.exit(0);
					}
				}
		);
		Window window = frame;
		window.add(applet);
		window.pack();
		window.setVisible(true);
		window.createBufferStrategy(3);
		applet.init();
		applet.start();
	}
	
	/**
	 * remise à "zero" de l'animation.
	 * (re)initialisation des coordonnées des objets de l'animation, etc.
	 */
	protected abstract void reset();

	/**
	 * "battement" de l'animation.
	 * mise à jour des coordonnées des objets de l'animation, etc.
	 */
	protected abstract void tick();

	/**
	 * le traçage proprement dit du dessin.
	 */
	protected abstract void trace(Graphics graphics, Dimension dimension);
	
	/**
	 * change la vitesse de l'animation.
	 * @param hertz la fréquence en Hertz
	 */
	protected synchronized void hertz(final float hertz) { this.hertz = hertz; }
	/**
	 * renvoie la vitesse de l'animation.
	 * @return la fréquence en Hertz
	 */
	protected synchronized float hertz() { return this.hertz; }
	private float hertz;
	
	/**
	 * image "offscreen" dans laquelle on trace.
	 */
	private VolatileImage image;

	public boolean isDoubleBuffered()
	{
		return true;
	}

	/**
	 * Allocation des resources
	 */
	public void init()
	{
		System.out.println("init()");
		super.init();

		{
			// Fréquence de rafraichissement de l'écran.
			DisplayMode displayMode = getGraphicsConfiguration().getDevice().getDisplayMode();
			int hertz = displayMode.getRefreshRate();
			if(hertz != DisplayMode.REFRESH_RATE_UNKNOWN) hertz(hertz);
			System.out.println("init(): getGraphicsConfiguration().getDevice().getDisplayMode().getRefreshRate(): " + hertz() + " Hertz");
		}
		
		// image offscreen.
		createOffscreenImage();

		setBackground(Color.BLACK); // On ne voit le fond que durant le redimensionnement du panel de l'applet.

		reset(); // Remise à "zero" de l'animation.

		{
			thread = new Thread(this);
			//thread.setPriority(Thread.MAX_PRIORITY);
			//Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
			System.out.println("init(): thread.start()");
			thread.start(); // Démarre le thread de l'animation, qui attendra isActive() et notify().
		}
	}

	/**
	 * thread de l'animation
	 */
	private Thread thread;

	/**
	 * La méthode isActive héritée de la classe Applet renvoie toujours false...
	 */
	public boolean isActive()
	{
		return isActive;
	}
	private boolean isActive = false;
	
	/**
	 * Réveille le thread de l'animation.
	 */
	public void start()
	{
		System.out.println("start()");
		synchronized(this) { isActive = true; }
		// isActive() est maintenant à true, le thread doit être réveillé pour continuer.
		super.start();
		synchronized(thread)
		{
			System.out.println("start(): thread.notify()");
			thread.notify(); // Réveille le thread de l'animation.
		}
	}

	/**
	 * function du thread de l'animation.
	 */
	public void run()
	{
		System.out.println("run()");
		long lastFrameTime = 0;
		while(!Thread.interrupted()) // Tant qu'on n'a pas reçu de demande d'interruption
		{
			// Attend que l'applet soit dans l'état isActive(), c'est à dire non stoppée. 
			{
				while(!Thread.interrupted()) // Tant qu'on n'a pas reçu de demande d'interruption
				{
					boolean isActive;
					synchronized(this) { isActive = isActive(); }
					if(isActive) break;
					System.out.println("run(): !isActive()");
					// On attend que l'applet ne soit plus dans l'état stoppé.
					try
					{
						synchronized(thread)
						{
							System.out.println("run(): thread.notify()");
							thread.notify(); // signale la méthode stop() que l'on attend.
							System.out.println("run(): thread.wait()");
							thread.wait(); // attend le notify() de la méthode start().
							System.out.println("run(): thread.wait() notified");
						}
					}
					catch(InterruptedException e)
					{
						System.out.println("run(): return");
						return; // demande d'interruption reçue pendant le wait, on quite la fonction.
					}
				}
			}

			// Trace les dessins dans l'image offscreen.
			
			traceToOffscreenImage();

			// Attend une periode de 1 / hertz() diminuée du temps déjà passé dans traceToOffscreenImage(), repaint(), et tick()
			{
				final long now = nanoTime();
				final long elapsed = now - lastFrameTime;
				final long remaining = (long)(1e9 / hertz()) - elapsed; // en nanosecondes
				if(remaining < 0)
				{
					//System.err.println("underrun of " + -remaining * 1e-9 + " seconds");
					Thread.yield();
					/*
					try
					{
						sleep(1);
					}
					catch (InterruptedException e)
					{
						System.out.println("run(): return");
						return; // demande d'interruption reçue pendant le sleep, on quite la fonction.
					}
					*/
					lastFrameTime = nanoTime();
				}
				else
				try
				{
					sleep(remaining);
					//lastFrameTime = nanoTime();
					lastFrameTime = now + remaining;
				}
				catch (InterruptedException e)
				{
					System.out.println("run(): return");
					return; // demande d'interruption reçue pendant le sleep, on quite la fonction.
				}
			}

			// Invalide toute la surface (Panel) de l'applet.
			// Ceci déclenche un appel à la méthode paint(Graphics) dès que possible.
			repaint();
			// Graphics.drawImage() s'exécute de manière asynchrone (la méthode retourne immédiatement, sans que l'affichage ait été mis à jour).
			// On attend donc que l'affichage ait été mis à jour.
			Toolkit.getDefaultToolkit().sync();
			
			// Fait avancer l'animation.
			tick();
		}
		System.out.println("run(): end");
	}
	
	/**
	 * créé l'image offscreen.
	 */
	private void createOffscreenImage()
	{
		GraphicsConfiguration graphicsConfiguration = getGraphicsConfiguration();
		ImageCapabilities imageCapabilities = graphicsConfiguration.getImageCapabilities();
		System.out.println("createOffscreenImage(): getGraphicsConfiguration().getImageCapabilities().isAccelerated(): " + imageCapabilities.isAccelerated());
		System.out.println("createOffscreenImage(): getGraphicsConfiguration().getImageCapabilities().isTrueVolatile(): " + imageCapabilities.isTrueVolatile());
		System.out.println("createOffscreenImage(): getGraphicsConfiguration().createVolatileImage().createCompatibleVolatileImage()");
		image = graphicsConfiguration.createCompatibleVolatileImage(getSize().width, getSize().height);
		System.out.println("createOffscreenImage(): image.getAccelerationPriority(): " + image.getAccelerationPriority());
		//offscreenGraphics = image.createGraphics();
	}
	
	/**
	 * trace les dessins dans l'image offscreen.
	 */
	private void traceToOffscreenImage()
	{
		Graphics offscreenGraphics = image.createGraphics();
		trace(image.getGraphics(), getSize());
		offscreenGraphics.dispose();
	}
	//private Graphics offscreenGraphics;
	
	/**
	 * On redéfinit complètement la méthode update,
	 * sans appeler la méthode héritée super.update(Graphics)
	 * parce qu'elle efface toute la surface de l'applet,
	 * ce que l'on ne veut pas car cela crée un scintillement visible.
	 */
	public void update(Graphics graphics)
	{
		//super.update(graphics);
		paint(graphics);
	}

	/**
	 * Transfert (blit) l'image offscreen sur la surface (Panel) de l'applet (visible à l'écran).
	 * Si l'applet est rédimensionnée, on ajuste la taille de l'image, et on retrace.
	 */
	public void paint(Graphics graphics)
	{
		// Si l'applet a été redimensionnée,
		if(getSize().width != image.getWidth() || getSize().height != image.getHeight())
		{
			// alors, on recréé l'image offscreen.
			createOffscreenImage();
			// Retrace les dessins dans la nouvelle image offscreen.
			traceToOffscreenImage();
		}
		do
		{
			int state = image.validate(getGraphicsConfiguration());
			// Si le système d'exploitation a libéré les resources de l'image,
			if(state == VolatileImage.IMAGE_RESTORED)
			{
				System.out.println("paint(): VolatileImage.IMAGE_RESTORED");
				// alors, retrace les dessins dans l'image offscreen.
				traceToOffscreenImage();
			}
			// ou alors, si le format d'affichage a changé,
			else if(state == VolatileImage.IMAGE_INCOMPATIBLE)
			{
				System.out.println("paint(): VolatileImage.IMAGE_INCOMPATIBLE");
				// alors, on recréé l'image offscreen.
				createOffscreenImage();
				// Retrace les dessins dans la nouvelle image offscreen.
				traceToOffscreenImage();
			}
		    // Transfert (blit) l'image offscreen sur la surface (Panel) de l'applet (visible à l'écran).
		    graphics.drawImage(image, 0, 0, this);
		}
		while(image.contentsLost());

		// Paint les éventuels "lightweight components".
	    super.paint(graphics);
	}

	/**
	 * Suspend le thread de l'animation.
	 */
	public void stop()
	{
		System.out.println("stop()");
		synchronized(this) { isActive = false; }
		// isActive() est maintenant à false, le thread se suspend de lui même.
		try
		{
			synchronized(thread)
			{
				System.out.println("stop(): thread.wait()");
				thread.wait(); // attend le notify() de la méthode run().
				System.out.println("stop(): thread.wait() notified");
			}
		}
		catch(InterruptedException e)
		{
			return; // demande d'interruption reçue pendant le wait, on quite la fonction.
		}
		super.stop();
	}

	/**
	 * Libération des resources, arrêt des threads. 
	 */
	public void destroy()
	{
		System.out.println("destroy()");
		// On met fin au thread de l'animation.
		System.out.println("destroy(): thread.interrupt()");
		thread.interrupt(); // on demande au thread de s'interrompre.
		synchronized(thread)
		{
			System.out.println("destroy(): thread.notify()");
			thread.notify();
		}
		try
		{
			System.out.println("destroy(): thread.join()");
			thread.join((int)(1e3 * 10 / hertz())); // on attend que le thread se termine, 10 periodes au maximum.
		}
		catch(InterruptedException e)
		{
			System.out.println("run(): interrupted");
			// rien de spécial à faire
		}
		if(thread.isAlive()) System.err.println("Le thread ne s'est pas arrêté!");
		thread = null;
		
		// On attend donc que l'affichage ait été mis à jour.
		Toolkit.getDefaultToolkit().sync();
		
		// Libèration les resources ... dans note cas, l'image.
		image = null;
		
		super.destroy();
	}
	
	private long nanoTime()
	{
		return System.nanoTime();
		//return (long)(System.currentTimeMillis()) * (long)1e6;
	}
	
	private void sleep(long nanoSecondes) throws InterruptedException
	{
		final long milliSecondes = nanoSecondes / (long)1e6;
		nanoSecondes -= milliSecondes * (long)1e6;
		Thread.sleep((int)milliSecondes, (int)nanoSecondes);
	}
}

