/* $Id$
 *
 * Copyright (c) 1999 Xenonsoft Limited. All Rights Reserved.
 *
 * This software is protected by copyright. You are hereby notified from
 * now by reading this message. This software is also the confidential 
 * and proprietary information of Xenonsoft Limited.  ("Confidential
 * Information"). 
 *
 * This software is distributed under the Xenonsoft Public end user 
 * License ("XPeL"), where the machine-readable source code is provided 
 * under the "Open Source" model.
 * For more information, please read the file "LICENSE-XPL.txt" 
 */

// File:`GameEngine.java'

//
// DESCRIPTION
//	Game Engine
// 
// Author
//
//	Peter Pilgrim
//	Fri Jun 19 00:09:21 BST 1998
// 
// RCS HEADER ``GameEngine.java''
// 
//	$Author$
//	$Date$
//	$Source$
//	$Revision$    $State$    $Locker$
//

package xenon.gamekit;

import java.beans.*;				// for PropertyChangeListener
import java.awt.*;				// for Component, Graphics
import java.awt.event.*;			// for ActionEvent, ActionListener
import java.awt.image.*;			// for ImageProducer
import java.util.*;				// for Vector

import xenon.soundkit.*;			// for PPMCAudioPlayer

/**
 * An abstract class that implements the guts of an engine for running
 * an arcade video game.
 *
 * @author Peter Pilgrim, Wed Jun 09 21:44:21 BST 1999 
 * @version $Id$ 
 */
public abstract class GameEngine 
implements Runnable, CollisionDetector, ImageRenderer
{
    public final static String  START_ANIMATION_PROPERTY="start_animation";
    public final static String  STOP_ANIMATION_PROPERTY="stop_animation";
    public final static String  SESSION_FINISHED_PROPERTY="session_finished";


    public final static String	GAME_PAUSED_MESSAGE="THIS GAME IS PAUSED!";
    
    /** We are at the intermediate state, where no painting or
     * screen updating should be done
     */
    public final static int	GAME_STATE_INTERMEDIATE=0;

    /** We are at the start of the game */
    public final static int	GAME_STATE_PROLOGUE=1;

    /** We are in the game */
    public final static int	GAME_STATE_EPISODE=2;

    /** We are at the end of the game */
    public final static int	GAME_STATE_EPILOGUE=3;

    /** The standard number of lives in an arcade video game */
    public final static int	DEFAULT_NUM_LIVES=3;

    /** The standard starting level in an arcade video game */
    public final static int	DEFAULT_LEVEL=1;
    
    /** The default frame rate is 30 */
    public final static int	DEFAULT_FRAME_RATE=30;

    /** 
     * Optimum default frame rate is 25 
     *
     * This rate seems to avoid the deadly the ScreenUpdater
     * thread dead lock up
     */
    public final static int	OPTIMUM_FRAME_RATE=25;

    /** 
     * The minimum frame delay in milliseconds.
     * This rate seems to avoid the deadly the ScreenUpdater
     * thread dead lock up
     */
    public final static int	MINIMUM_FRAME_DELAY=5;
    

    /** the state of the game 
     * @see #GAME_STATE_PROLOGUE
     * @see #GAME_STATE_INTERMEDIATE
     * @see #GAME_STATE_EPISODE
     * @see #GAME_STATE_EPILOGUE
     */
    public int		gameState;

    /** the current score for the player */
    public int		score;

    /** the current level (sheet) */
    public int		level;

    /** the next level (sheet) */
    public int		nextLevel;

    /** the number of player lives */
    public int		lives;

    /** the default number of player lives for each game */
    public int		defaultLives=DEFAULT_NUM_LIVES;

    /** the default level for each game */
    public int		defaultLevel=DEFAULT_LEVEL;

    /** world map size width */
    public int		world_width;

    /** world map size height */
    public int		world_height;

    /** world current x coordinate */
    public int		world_xpos;

    /** world current y coordinate */
    public int		world_ypos;

    /** world current x start coordinate */
    public int		world_start_xpos;

    /** world current y start coordinate */
    public int		world_start_ypos;

    /** world current x player start coordinate */
    public int		player_start_xpos;

    /** world current y player start coordinate */
    public int		player_start_ypos;

    /** screen dimensions width */
    public int		screen_width;

    /** screen dimensions height */
    public int		screen_height;

    /** screen x offset */
    public int		screen_xoffset;

    /** screen y offset */
    public int		screen_yoffset;

    /** the animation frame clock */
    public int		frameClock;		// frame clock time
    
    /** the frame delay = 1000 millisec / frame_rate  */
    protected int	frameDelay;

    /** true if the level (sheet) completed */
    public boolean	levelCompleted;		

    /** true, if level/sheet finished. */
    public boolean	sessionDone;
    /** true, if the game is over */
    public boolean	gameOver;
    /** true, if the player has died. */
    public boolean	playerDied;
    
    /** background object. */
    public WorldObject	backgrdObject;	

    /** (hero) player object. */
    public WorldObject	playerObject;

    /** list of world animate objects. */
    public Vector	spriteList;
    
    /** true if the game debug mode is on */
    public boolean	debug=false;
    /** true if the game is paused */
    public boolean	paused=false;
    /** true if the quit key is paused */
    public boolean	quit_key_pressed=false;
  
    /** a font for displaying generic text */
    public Font		textFont = new Font( "Dialog", Font.BOLD, 14 );
    
    /** the game main loop thread */
    protected Thread	gameThread;		// game thread
    
    protected boolean	runningInApplet;	// applet boolean
    
    // Thread lock for reading
    // writing the off screen buffer. 
    // FIXME: Readers and writers lock
    protected Object		LOCK_OffScreen=new Object();
    

    /** the multichanneled audio player */
    protected PPMCAudioPlayer	ppmcAudioPlayer;

    /** the current collision detector */
    protected CollisionDetector	currentDetector;
    
    /** the double buffered container */
    protected DoubleBufferedContainer	bufferedContainer=null;
    
    /** the property change support for this class */
    protected PropertyChangeSupport  changeListeners=null; 
    

    /**
     * GameEngine - a generic abstract container implements
     * so of the framework for writing an arcade game.
     */
    public GameEngine()
    {
	this( 800, 600, 0,0, 0, 0, 800, 600 );
    }
    
    /**
     * GameEngine - a generic abstract container implements
     * so of the framework for writing an arcade game.
     *
     * @param screen_width the screen size
     * @param screen_height the screen height
     * @param world_xpos the starting world map x coordinate 
     * @param world_ypos the starting world map y coordinate 
     * @param player_xpos the starting world map x coordinate (for our hero)
     * @param player_ypos the starting world map y coordinate (for our hero)
     * @param world_width the world map width
     * @param world_height the world map height
     *
     */
    public GameEngine( 
	int screen_width, int screen_height,
	int world_xpos, int world_ypos,
	int player_xpos, int player_ypos,
	int world_width, int world_height )
    {
	// Screen Dimensions
	this.screen_width = screen_width;
	this.screen_height = screen_height;

	this.world_xpos   = world_xpos;
	this.world_ypos   = world_ypos;
	this.world_width  = world_width;
	this.world_height = world_height;

	this.world_start_xpos  = world_xpos;
	this.world_start_ypos  = world_ypos;
	this.player_start_xpos = player_xpos;
	this.player_start_ypos = player_ypos;

	this.frameClock = 0;

	// Get the sound player instance
	ppmcAudioPlayer = PPMCAudioPlayer.getInstance();

	// General set up
	playerObject = null;
	backgrdObject = null;
	spriteList = new Vector();

	setRunningInApplet( false );
	setFrameRate( DEFAULT_FRAME_RATE );
	
	//
	// Create the double buffered container
	//
	DoubleBufferedContainer doubleBuffer = 
	    new DoubleBufferedContainer( this, screen_width, screen_height );
	setDoubleBufferedContainer( doubleBuffer );
	
	// System.out.println( "GameEngine()" );
	// System.out.println( "doubleBuffer="+doubleBuffer.hashCode() );
	
	// Add the mouse tracker to pause the game, when pointer
	// exits the component window
	doubleBuffer.addMouseListener( new MouseTracker() );

	// Dont forget to register the default collision detector
	setCollisionDetector( this );
    }

    /** Gets the name of this game engine */
    public String getEngineName()
    {
	return GameEngine.class.getName();
    }
    
    /** registers a new collision detector */
    public void setCollisionDetector( CollisionDetector newDetector )
    {
	currentDetector = newDetector;
    }
    
    /** Gets the current collision detector */
    public CollisionDetector getCollisionDetector()
    {
	return currentDetector;
    }
    
    /** registers a new double buffered container */
    public void setDoubleBufferedContainer( 
	DoubleBufferedContainer newContainer )
    {
	bufferedContainer = newContainer;
    }
    
    /** Gets the current double buffered container */
    public DoubleBufferedContainer getDoubleBufferedContainer()
    {
	return bufferedContainer;
    }

    /** return true if game is running in an applet */
    public boolean isRunningInApplet()
    {
	return (runningInApplet);
    }
    
    /** Set flag if game is running in an applet */
    public void setRunningInApplet( boolean flag )
    {
	runningInApplet = flag;
    }
    
    /** Gets the frame delay time in milliseconds 
     *
     * @see #getFrameRate
     * @see #setFrameRate
     */
    public int getFrameDelay() 
    {
	return frameDelay;
    }
    
    /** Gets the frame rate, which sets the frame delay 
     * <pre>
     *	frame_delay =  1000 / frame_rate
     * <pre>
     *
     * where frame delay is measure in milliseconds
     *
     * @see #setFrameRate
     * @see #getFrameDelay
     */
    public int getFrameRate() 
    {
	return (1000/frameDelay);
    }
    
    /** sets the frame rate, which also sets the frame delay 
     * @param newRate the new frame rate
     *
     * @see #getFrameRate
     * @see #getFrameDelay
     */
    public void setFrameRate( int newRate ) 
    {
	frameDelay = 1000/newRate;
    }
    
    /** Gets reference to multi channel audio player */
    public PPMCAudioPlayer getMCAudioPlayer()
    {
	return ppmcAudioPlayer;
    }
    
    public boolean isPaused() 
    {
	return (paused);
    }
    
    public void setPaused( boolean newFlag ) 
    {
	paused = newFlag;
    }
    

    /** Sets the player animation object */
    public synchronized GameEngine setPlayerObject( WorldObject playerObject )
    {
	this.playerObject = playerObject;
	return (this);
    }
    
    /** Gets the player animation object */
    public synchronized WorldObject getPlayerObject()
    {
	return (playerObject);
    }
    
    /** Sets the background animation object */
    public synchronized GameEngine setBackgroundObject( WorldObject backgrdObject )
    {
	this.backgrdObject = backgrdObject;
	return (this);
    }
    
    /** Gets the background animation object */
    public synchronized WorldObject getBackgroundObject()
    {
	return (backgrdObject);
    }
    
    /** Set player died flag */
    public void setPlayerDied( boolean flag )
    {
	playerDied = flag;
    }
    
    /** Get player died flag */
    public boolean isPlayerDied()
    {
	return (playerDied);
    }
    
    /** Sets level completed flag */
    public void setLevelCompleted( boolean flag )
    {
	levelCompleted = flag;
    }
    
    /** Return true if the current level completed */
    public boolean isLevelCompleted()
    {
	return (levelCompleted);
    }

    /** Sets game over flag */
    public void setGameOver( boolean flag )
    {
	gameOver = flag;
    }
    
    /** Returns true if the current game is over */
    public boolean isGameOver()
    {
	return (gameOver);
    }
    
    /** Sets session done flag */
    public void setSessionDone( boolean flag )
    {
	sessionDone = flag;
    }
    
    /** Returns true if the session is finished */
    public boolean isSessionDone()
    {
	return (sessionDone);
    }

    /** set the game state */
    public synchronized void setGameState( int newState )
    {
	if ( newState != GAME_STATE_PROLOGUE &&
	     newState != GAME_STATE_EPISODE &&
	     newState != GAME_STATE_EPILOGUE &&
	     newState != GAME_STATE_INTERMEDIATE )
	    throw new IllegalArgumentException( 
		"unknown illegal game state:"+newState );
	gameState = newState;
    }
    
    public synchronized int getGameState() 
    {
	return (gameState);
    }
    
    /** Sets default number of lives when each game starts */
    public void setDefaultLives( int newLives )
    {
	defaultLives = newLives;
    }
    
    /** Gets number of lives when each game starts */
    public int getDefaultLives()
    {
	return (defaultLives);
    }

    /** Sets level when each game starts */
    public void setLevel( int newLevel )
    {
	level = newLevel;
    }
    
    /** Gets default level when each game starts */
    public int getLevel()
    {
	return (level);
    }

    /** Sets default level when each game starts */
    public void setDefaultLevel( int newLevel )
    {
	defaultLevel = newLevel;
    }
    
    /** Gets default level when each game starts */
    public int getDefaultLevel()
    {
	return (defaultLevel);
    }

    /** Sets next level when each game starts */
    public void setNextLevel( int newLevel )
    {
	nextLevel = newLevel;
    }
    
    /** Gets next level when each game starts */
    public int getNextLevel()
    {
	return (nextLevel);
    }

    /** Sets debug flag */
    public void setDebugMode( boolean flag )
    {
	debug = flag;
    }
    
    /** Returns true if the debug flag is on */
    public boolean isDebugMode()
    {
	return (debug);
    }
    
    /** Sets the current score */
    public void setScore( int newScore )
    {
	score = newScore;
    }
    
    /** Gets the current score */
    public int getScore()
    {
	return (score);
    }
    
    /** increments the players score 
     * <b>subclassers</b> should override this method to handle such things 
     * game bonuses. Also any collision handlers should call this method
     * instead of increment the score directly.
     *
     * @param points the number of points to be added to the score.
     */
    public synchronized void incrementScore( int points )
    {
	score += points;
    }
    
    /** Sets the game frame clock */
    public void getFrameClock( int newClock )
    {
	frameClock = newClock;
    }

    /** Gets the game frame clock */
    public int getFrameClock()
    {
	return frameClock;
    }
    
    
    /** startGame calls the system garbage collector resets the score
     * the number of lives and the level (sheet)
     */
    public void startGame()
    {
	// Garbage collect here
	System.gc();
	
	// Reset the score
	score = 0;
	lives = defaultLives;
	level = defaultLevel;
	playerDied = false;
	levelCompleted=false;
	gameOver = false;
	frameClock = 0;
	
	// Destroy all the sprites
	Enumeration etor = spriteList.elements();
	for ( int k=0; k<spriteList.size(); ++k ) {
	    WorldObject sprite = (WorldObject)etor.nextElement();
	    sprite.destroy();
	    // Trying to resolve AWT window lock-up issues with
	    // ScreenUpdater thread by sleeping for short interval.
	    try { Thread.sleep(7); } catch (InterruptedException e1) { }
	}

	spriteList.removeAllElements();

	setLevelCompleted( false );
	setPlayerDied( false );
	setGameOver(false );
    }
    
    /** Code to execute when the when the game ends */
    public void stopGame()
    {
	System.gc();
	setLevelCompleted( false );
	setPlayerDied(true);
	setGameOver(true);

	if ( playerObject != null ) {
	    playerObject.setActive(false);
	    playerObject.setVisible(false);
	}
    }
    
    /** Add the sprite to the list of animation objects
     * @param sprite the world object
     */
    public void addWorldObject( WorldObject sprite  )
    {
	if ( !sprite.isDestroyed() ) {
	    spriteList.addElement( sprite );
	}
    }
    
    /** Remove the sprite from the list of animation objects 
     * @param sprite the world object
     */
    public void removeWorldObject( WorldObject sprite  )
    {
	if ( !sprite.isDestroyed() ) {
	    spriteList.removeElement( sprite );
	}
    }
    
    /** Code to execute when the game starts the level
     * which explicitly calls the garbage collector
     * which draws all the objects on the screen 
     */
    public void startLevel()
    {	
	// Reset the starting coordinates
	world_xpos = world_start_xpos;
	world_xpos = world_start_ypos;
	
	if (playerObject != null ) {
	    playerObject.wx = player_start_xpos;
	    playerObject.wy = player_start_ypos;
	}
	
	setPlayerDied(false);
	setLevelCompleted(false);

    }
    
    /** Code to execute when the game ends the level.
     *
     * <p>
     * <ol> 
     * <li> which undraws all the <em>active</em> world objects on the screen 
     * <li> destroys all the sprite object by calling <code>WorldObject.destroy</code>
     * <li> remove all world objects from the sprite list
     * </ol>
     */
    public void endLevel()
    {
	Graphics offScreenGC = bufferedContainer.getOffScreenGC();

	// Undraw all the sprites
	Vector copy = (Vector)spriteList.clone();
	Enumeration etor = copy.elements();
	
	for ( int k=0; k<spriteList.size(); ++k ) {
	    WorldObject sprite = (WorldObject)etor.nextElement();
	    if (sprite.isActive() )
		sprite.undrawObject( offScreenGC );
	    // Trying to resolve AWT window lock-up issues with
	    // ScreenUpdater thread by sleeping for short interval.
	    try { Thread.sleep(7); } catch (InterruptedException e1) { }
	}

	// Trying to resolve AWT lockup with this synchronized 
	// graphics update
	// bufferedContainer.getToolkit().sync();
	
	// Destroy all the sprites
	etor = copy.elements();
	for ( int k=0; k<spriteList.size(); ++k ) {
	    WorldObject sprite = (WorldObject)etor.nextElement();
	    sprite.destroy();
	    // Trying to resolve AWT window lock-up issues with
	    // ScreenUpdater thread by sleeping for short interval.
	    try { Thread.sleep(7); } catch (InterruptedException e1) { }
	}

	// Clean up the sprite list
	spriteList.removeAllElements();

	// // Garbage collect here
	// if ( level % 3 == 0 ) System.gc();
    }

    /** Paint method ; 
     * <b>Subclasser</b> please override the <b>paintWorld()</b> instead!!
     * 
     * @see #paintWorld
     *
     * @param g the graphics context
     */
    public void renderImage( Graphics g )
    {
	paintWorld( g );
    }
    
    /** 
     * Paint method draw alls the active animation sprites int the World 
     *
     * @param g the graphics context
     */
    public void paintWorld( Graphics g )
    {
	// Paint the prologue
	// Paint the epilogue
	int state = getGameState();
	if ( state == GameEngine.GAME_STATE_INTERMEDIATE )
	    return;
	else if ( state == GameEngine.GAME_STATE_PROLOGUE )
	    paintPrologue(g);
	else if ( state == GameEngine.GAME_STATE_EPILOGUE )
	    paintEpilogue(g);
	
	// Draw the background first
	paintBackground(g);
	// Then the hero player object
	paintPlayer(g);
	// Draw the rest of the world object sprites
	paintSprites(g);
	// Draw top level messages
	paintMessages(g);
    }

    /** Paint the background 
     *
     * @param g the graphics context
     */
    protected void paintBackground( Graphics g )
    {
	if (backgrdObject != null ) {
	    // synchronized (LOCK_OffScreen) {
	    backgrdObject.undrawObject( g );
	    backgrdObject.drawObject( g );
	    // }
	}
    }
    
    /** Paint the hero player 
     *
     * @param g the graphics context
     */
    protected void paintPlayer( Graphics g )
    {
	if (playerObject != null ) {
	    // synchronized (LOCK_OffScreen) {
	    playerObject.undrawObject( g );
	    playerObject.drawObject( g );
	    // }
	}
    }
    
    /** Paint the rest of the players 
     *
     * @param g the graphics context
     */
    protected void paintSprites( Graphics g )
    {
	// Draw the sprites
	// Work with a cloned copy of the sprite list.
	Vector copySpriteList = (Vector)spriteList.clone();
	Enumeration etor = copySpriteList.elements();
	
	while ( etor.hasMoreElements() ) {
	    WorldObject sprite = (WorldObject)etor.nextElement();
	    if ( sprite.isActive() && sprite.isVisible() ) {
		// synchronized (LOCK_OffScreen) {
		sprite.undrawObject( g );
		sprite.drawObject( g );
		// }
	    }
	}
    }


    /** Paint the top layer messages 
     *
     * @param g the graphics context
     */
    protected void paintMessages( Graphics g )
    {
	if (paused) {
	    g.setFont( textFont );
	    g.setColor( Color.white );
	    GraphicsUtils.drawCenteredString( 
		g, GAME_PAUSED_MESSAGE, 2*screen_height / 3 );
	}
	if (levelCompleted) {
	    g.setFont( textFont );
	    g.setColor( Color.white );
	    GraphicsUtils.drawCenteredString( 
		g, "LEVEL "+level+" COMPLETED!", screen_height / 3 );
	}
	
    }
    
    /** a convenience method for <b>subclassers</b> to override which
     * is called <em>before</em> the actual game (ie the prologue).  
     *
     * @param g the graphics context
     */
    public void paintPrologue( Graphics g )
    {
    }

    /** a convenience method for <b>subclassers</b> to override which
     * is called <em>after</em> the actual game (ie the epilogue).  
     *
     * @param g the graphics context
     */
    public void paintEpilogue( Graphics g )
    {
    }

    /** 
     * gets reference to the game thread 
     */
    public Thread getGameThread()
    {
	return gameThread;
    }
    
    
    /** Main loop of the game */
    public void gameMainLoop()
    {
	Thread myThread = Thread.currentThread();

	while ( !sessionDone) {
	    setGameState( GAME_STATE_PROLOGUE );
	    updatePrologue();
	    // check if the session after the prologue finishes
	    if ( sessionDone )
		break;
	    startGame();
	    while ( !gameOver ) {
		//
		// INNER LOOP FOR GAME ANIMATION
		//
		setGameState( GAME_STATE_INTERMEDIATE );
		// BUGFIX: Put a delay loop seems to help lick the screen updater
		// bug. I think this may have to do with a race condition
		// between the repainter and the animation thread.
		// Suddenly setting the game state seems to interfer the 
		// `paintWorld()' method.
		try { Thread.sleep( 333 ); } catch ( InterruptedException e ){ }
		startLevel();
		setGameState( GAME_STATE_EPISODE );

		//
		// The game proper
		//
		long nfrtime;			// next frame time
		long realDelay;
		
		while ( !playerDied && !levelCompleted ) {
		    
		    nfrtime = frameDelay + System.currentTimeMillis();
		    
		    if ( myThread != getGameThread() ) {
			// NOTE: Break the game thread now!!!
			return;
		    }

		    if ( paused ) {
			// Handle the pause key being depressed
			while ( paused ) {
			    bufferedContainer.repaint();
			    if ( myThread != getGameThread() ) {
				// NOTE: Break the game thread now!!!
				return;
			    }
			    // Sleep for a bit.
			    try { Thread.sleep( 500 ); }
			    catch ( InterruptedException e ){ }
			}
		    }
			    
		    // Update before hook
		    updatePreHook();
		    
		    // Update the game's world objects
		    update();

		    // Update post hook
		    updatePostHook();
		    
		    // Check the collisions of any world objects
		    currentDetector.detectCollisions( this );

		    // Increment the frame clock and calculate the real
		    // frame delay take into account the time for animation
		    // frame processing, but leave a delay for other threads
		    // to get some processing time.
		    ++frameClock;		/* ! */
		    realDelay = nfrtime - System.currentTimeMillis();
		    if (realDelay < MINIMUM_FRAME_DELAY ) 
			realDelay = MINIMUM_FRAME_DELAY;


		    // DEBUG: if ( frameClock % 10 == 0 ) System.out.print('.');
		    // Trying to eradicate source of the ScreenUpdater bug.
		    // OLDE CODE: bufferedContainer.repaint( realDelay/2, 0, 0, screen_width, screen_height );
		    bufferedContainer.repaint();

		    // Sleep for a bit.
		    try { Thread.sleep( realDelay ); }
		    catch ( InterruptedException e ) { }
		}
		
		// When the player died and/or level completed
		if ( playerDied ) {
		    playerDied = false;
		    lives = lives - 1;
		    updatePlayerDiedHook();
		    if ( lives < 1 ) {
			gameOver = true;
		    }
		}
		else if ( levelCompleted ) {
		    bufferedContainer.repaint();
		    updateLevelCompletedHook( level );
		    setLevel( getNextLevel() );
		    try { Thread.sleep( 500 ); }
		    catch ( InterruptedException e ){ }
		}

		setGameState( GAME_STATE_INTERMEDIATE );
		// End the level
		endLevel();
	    }
	    stopGame();
	    setGameState( GAME_STATE_EPILOGUE );
	    updateEpilogue();
	}

	System.out.println( getEngineName()+" Session Done.");
	if ( changeListeners != null )
	    firePropertyChange( SESSION_FINISHED_PROPERTY, null, null );
	if ( !runningInApplet )
	    System.exit(0);
    }

    /** a convenience method for <b>subclassers</b> to override which
     * is called <em>before</em> the actual game has started.  
     */
    public void updatePrologue()
    {
    }

    /** a convenience method for <b>subclassers</b> to override which
     * is called <em>after</em> the actual game has finished.  
     */
    public void updateEpilogue()
    {
    }

    /** a convenience method for <b>subclassers</b> to override which
     * is called <em>before</em> the <code>update()</code> method for
     * each object is called 
     */
    public void updatePreHook()
    {
    }
    
    /** a convenience method for <b>subclassers</b> to override which is called
     * <em>after</em> the <code>update()</code> method for each object
     * is called 
     */
    public void updatePostHook()
    {
    }
    
    /** a convenience method for <b>subclassers</b> to override which is called
     * <em>after</em> the level (sheet) has been completed.
     *
     * For example the following code accomplishes this:
     * <code>setNextLevel( getLevel() + 1 ); </code>
     *
     *
     * @param currLevel the current level
     */
    public void updateLevelCompletedHook( int currLevel )
    {
	setNextLevel( currLevel + 1 );
    }
    
    /** a convenience method for <b>subclassers</b> to override which is called
     * <em>after</em> the player object has died.
     */
    public void updatePlayerDiedHook()
    {
    }
    
    /** a default update method for any generic game.  First the
     * background object is updated, followed by the player object,
     * and then rest of the world object sprite objects.  
     * <b>Subclasser</b> can override this method if they want to.
     */
    public void update()
    {
	if (backgrdObject != null) 
	    backgrdObject.update();
	
	if (playerObject != null) 
	    playerObject.update();
	
	// Work with a clone copy of the list
	Vector copySpriteList = (Vector)spriteList.clone();
	Enumeration etor = copySpriteList.elements();
	
	while ( etor.hasMoreElements() ) {
	    WorldObject sprite = (WorldObject)etor.nextElement();
	    if ( sprite.isActive() ) {
		sprite.update();
	    }
	}
    }
    
    /** check for collisions */
    public void detectCollisions( GameEngine engine )
    {
	CollisionMapDispatcher dispatcher = CollisionMapDispatcher.getInstance();
	
	if (playerObject != null) {
	    // Work with a clone copy of the list
	    Vector copySpriteList = (Vector)spriteList.clone();
	    Enumeration etor1 = copySpriteList.elements();
	    
	    while ( etor1.hasMoreElements() ) {
		WorldObject sprite = (WorldObject)etor1.nextElement();
		if ( playerObject.isActive() && sprite.isActive() ) {
		    if ( playerObject.hasCollided( sprite ) ) {
			dispatcher.fireCollisionEvent( playerObject, sprite );
			// Break the collision detection loop here
			// For some amusements this may not make always sense
			break;
		    }
		}
	    }
	}

	// 
	// Algorithm  N^2 / 2 is 50% better than N^2
	//
	Vector copySpriteList = (Vector)spriteList.clone();
	Enumeration etor1 = copySpriteList.elements();
	while ( etor1.hasMoreElements() ) {
	    WorldObject sprite1 = (WorldObject)etor1.nextElement();
	    if ( sprite1.isActive() ) {
		Enumeration etor2 = copySpriteList.elements();
		while ( etor2.hasMoreElements() ) {
		    WorldObject sprite2 = (WorldObject)etor2.nextElement();
		    if ( sprite1.isActive() && sprite2.isActive() && 
			 sprite1.hasCollided( sprite2 ) ) {
			dispatcher.fireCollisionEvent( sprite1, sprite2 );
			// Break the collision detection loop here
			// For some amusements this may not make always sense
			break;
		    }
		}
	    }
	}
    }

    /** implementation defined: threadable game engine runnable method */
    public void run()
    {
	gameMainLoop();	    
    }

    /** start the game animation */
    public void startGameAnimation() 
    {
	if ( gameThread != null ) 
	    stopGameAnimation();
	
	if ( changeListeners != null )
	    firePropertyChange( START_ANIMATION_PROPERTY, null, null );

	gameThread = new Thread(this);
	gameThread.start();
    }
    
    /** stop the game animation */
    public void stopGameAnimation() 
    {
	if ( gameThread != null ) {

	    if ( changeListeners != null )
		firePropertyChange( STOP_ANIMATION_PROPERTY, null, null );

	    gameThread.interrupt();
	    try { Thread.sleep( 250 ); } catch ( InterruptedException ie ){  }
	    gameThread = null;
	    try { Thread.sleep( 250 ); } catch ( InterruptedException ie ){  }
	}
    }
    
    /** 
     * add property change listener to this game engine
     * @param listener the listener to register
     */
    public void addPropertyChangeListener( PropertyChangeListener listener )
    {
	if ( changeListeners == null )
	    changeListeners = new PropertyChangeSupport(this);
	changeListeners.addPropertyChangeListener(listener);
    }

    /** 
     * remove property change listener from this game engine
     * @param listener the listener to unregister
     */
    public void removePropertyChangeListener( PropertyChangeListener listener )
    {
	if ( changeListeners != null ) 
	    changeListeners.removePropertyChangeListener(listener);
    }

    /** 
     * fire a property change event for this game engine
     * @param name the property name
     * @param oldValue the old value of the property
     * @param newValue the new value of the property
     */
    protected void firePropertyChange( 
	String name, Object oldValue, Object newValue )
    {
	if ( changeListeners != null ) 
	    changeListeners.firePropertyChange( name, oldValue, newValue );
    }
    

    // ======================================================================
    // I N N E R   C L A S S E S
    // ======================================================================

    protected class MouseTracker extends MouseAdapter {
	public void mouseExited( MouseEvent evt ) 
	{
	    // DEBUG: System.out.println( "MouseTracker.mouseExited()");
	    if ( getGameState() == GameEngine.GAME_STATE_EPISODE )
		setPaused(true);
	}

	public void mouseEntered( MouseEvent evt ) 
	{
	    // DEBUG: System.out.println( "MouseTracker.mouseEntered()");
	}
	
    }
}

// fini
