/***************************************************************

Copyright 2001, Bjorn Carlin.

GAMEFORM LICENSE AGREEMENT
--------------------------

This license agreement covers the source code in the folders
"GLib Source" and "GEditor Source", and the libraries "GLib.jar"
and "GEditor.jar" ("GameForm"). It does not cover the example
code distributed with GameForm.

Permission to use GameForm, whole or in part, is granted for any
personal, non-commercial purposes, to any person understanding
and fully agreeing with this license agreement.

GameForm may NOT be used, whole or in part, by any commercial
company or for any commercial software or development of
commercial software, without prior written permission from the
copyright holder. 'Commercial software' includes, but is not
limited to, charging money for using or distributing the software,
or requiring a paid subscription or license to use the software.

GameForm is provided 'as-is', without any express or implied
warranty. The author shall not be held liable for any damages
caused by the use of GameForm.

GameForm may be freely distibuted in an unaltered state, and
with no part of the package missing. Especially this copyright
notice and license agreement must be present in all source files
of all distributed copies.


CONTACT INFORMATION
-------------------

GameForm home page:
	http://home.swipnet.se/carlin/gameform/

E-mail address to Bjorn Carlin:
	bjorn.carlin@swipnet.se

Snail mail address to Bjorn Carlin:
	Bjorn Carlin
	Gnejsvagen 18 B
	752 42 Uppsala
	Sweden

***************************************************************/


import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.applet.Applet;
import java.io.*;


/**
 * GameApplet encapsulates many of the commonly-used functions of
 * most game and game-like applets, including buffered graphics,
 * thread handling and key handling. It does not rely on any other
 * part of GameForm.
 * <p>
 * To initialize GameApplet, you will generally need to invoke init()
 * for general initialization, and set cFrameLength to the length
 * of a frame, to control the frame rate.
 * <p>
 * To use GameApplet's key event handling, invoke setKeys() during
 * initialization, and use isKeyActive() to determine if a key is
 * active during a game.
 * <p>
 * Many games want to have advanceFrame(), update() and paint()
 * marked as synchronize to keep the game thread and the updating
 * thread from stepping on each other's toes. That is, however,
 * left as an option for a subclass to implement.
 * <p>
 * A subclass should probably override at least the following
 * <ul>
 * <li>init()
 * <li>pauseGame()
 * <li>newGame()
 * <li>endGame()
 * <li>advanceFrame()
 * <li>update()
 * <li>paint()
 * </ul>
 *
 * @version 1.0 08/21/2001
 * @author Bjorn Carlin
 */


public abstract class GameApplet extends Applet implements Runnable, KeyListener
{
	// keys types
	protected static final int		kKeyContinous = 0;
	protected static final int		kKeyRepeat = 1;
	protected static final int		kKeyOnce = 2;
	
	// initialization constants
	protected static final int		kNoBuffer = 0;
	protected static final int		kDoubleBuffer = 1;
	protected static final int		kTripleBuffer = 2;
	protected static final int		kPixelBuffer = kTripleBuffer;
	
	// current game info
	protected boolean				cGameInProgress;
	protected boolean				cGamePaused;
	protected boolean				cGameOver;
	protected int					cFrame;
	
	// thread handling
	protected Thread				cGameThread;
	protected boolean				cStopGameThread;
	protected boolean				cGameThreadStopped;
	
	// timer
	protected long					cFirstFrameTime; // time of game start
	protected long					cPauseTime; // length of pause
	protected int					cFrameLength; // in milliseconds
	protected boolean				cBreakLoop; // set to indicate that the game should exit the current game loop
	
	// key handling
	protected int					cKeyRepeatDelay; // in frames, lower number = faster
	protected int					cKeyRepeatFrequency; // in frames, lower number = faster
	protected int					cKeyCount; // number of keys to listen for
	protected int[]					cKeyCode; // map of key codes to listen for
	protected int[]					cKeyType; // kKeyContinous, kKeyRepeat or kKeyOnce
	protected boolean[]				cKeyDown; // current key state
	protected int[]					cKeyDownFrame;
	
	// cheat codes
	protected int[][]				cCheatCode;
	protected int[]					cCheatProgress;
	
	// buffer
	protected int[]					cBufferPixels;
	protected MemoryImageSource		cPixelBufferSource;
	protected Image					cPixelBuffer;
	
	protected Image					cBuffer;
	protected Graphics				cBufferGraphics;
	protected int					cBufferWidth;
	protected int					cBufferHeight;
	
	
	public void init() {
		init(kNoBuffer);
	}
	
	/**
	 * Initializes everything needed except keys and cheats.
	 * If you pass kNoBuffer, GameApplet will not allocate any
	 * buffer; if you pass kDoubleBuffer, GameApplet will allocate an
	 * Image object that can be used for flicker-free graphics, limited
	 * by the Graphics object; if you pass kTribleBuffer, GameApplet will
	 * allocate an Image object and an extra buffer providing pixel-for-pixel
	 * editing.
	 */
	public void init(int buffer) {
		addKeyListener(this);
		cBreakLoop = false;
		
		cGameInProgress = false;
		cGamePaused = false;
		cGameOver = false;
		
		cKeyCount = 0;
		cCheatCode = null;
		cCheatProgress = null;
		cFrameLength = 50; // 20 fps
		
		cBufferWidth = getBufferWidth();
		cBufferHeight = getBufferHeight();
		if (buffer == kTripleBuffer) {
			int						pixelCounter;
			
			cBufferPixels = new int[cBufferWidth * cBufferHeight];
			for (pixelCounter = 0; pixelCounter < cBufferWidth * cBufferHeight; pixelCounter++)
				cBufferPixels[pixelCounter] = 0xFF000000; // black with full alpha
			
			cPixelBufferSource = new MemoryImageSource(cBufferWidth, cBufferHeight, cBufferPixels, 0, cBufferWidth);
			cPixelBufferSource.setAnimated(true);
			cPixelBuffer = createImage(cPixelBufferSource);
		} else
			cPixelBuffer = null;
		
		if (buffer == kTripleBuffer || buffer == kDoubleBuffer) {
			cBuffer = createImage(cBufferWidth, cBufferHeight);
			cBufferGraphics = cBuffer.getGraphics();
			cBufferGraphics.setColor(Color.black);
			cBufferGraphics.fillRect(0, 0, cBufferWidth, cBufferHeight);
		} else
			cBuffer = null;
		
		/* note: requestFocus() for applets only work on some
		systems/browsers, others require the user to click
		the applet for it to gain keyboard focus. */
		requestFocus();
		
		repaint();
	}
	
	public void start() {
		cGameInProgress = false;
		
		cStopGameThread = false;
		cGameThreadStopped = false;
		cGameThread = new Thread(this);
		cGameThread.setPriority(Thread.MIN_PRIORITY);
		cGameThread.start();
	}
	
	public void stop() {
		cStopGameThread = true;
		cGameThread.interrupt();
		
		// wait at most 0.3 seconds for the game thread to stop
		for (int counter = 0; counter < 6; counter++)
			if (!cGameThreadStopped) {
				try {
					Thread.sleep(50);
				} catch (Exception e) {}
			}
	}
	
	/**
	 * Override if the buffer should not be the same width as the applet.
	 */
	public int getBufferWidth() {
		return getSize().width;
	}
	
	/**
	 * Override if the buffer should not be the same height as the applet.
	 */
	public int getBufferHeight() {
		return getSize().height;
	}
	
	/**
	 * Pauses or unpauses the game.
	 */
	public void pauseGame(boolean pause) {
		if (cGamePaused == pause || !cGameInProgress)
			return;
		
		cGamePaused = pause;
		
		// "trick" the timer that no time passes when pausing
		if (cGamePaused)
			cPauseTime = System.currentTimeMillis();
		else
			cFirstFrameTime += System.currentTimeMillis() - cPauseTime;
		
		if (cGamePaused)
			repaint();
	}
	
	/**
	 * Starts a new game and ends the current game, if any.
	 */
	public void newGame() {
		if (cGameInProgress)
			endGame();
		
		cFrame = -1;
		cFirstFrameTime = System.currentTimeMillis();
		cPauseTime = 0;
		
		cGameInProgress = true;
		cGamePaused = false;
		cGameOver = false;
		cBreakLoop = false;
	}
	
	/**
	 * Ends the current game, if any.
	 */
	public void endGame() {
		if (!cGameInProgress)
			return;
		
		cGameInProgress = false;
		cGamePaused = false;
		cGameOver = true;
		cBreakLoop = true;
		
		repaint();
	}
	
	/**
	 * Updates the game state. Invoked once for every passing frame.
	 */
	protected void advanceFrame() {}
	
	/**
	 * Continously invokes advanceFrame() and repaint() while a
	 * game is running.
	 */
	public void run() {
		int							sleepDuration;
		
		while (!cStopGameThread) {
			if (cGameInProgress && !cGamePaused) {
				do {
					cFrame++;
					
					advanceFrame();
					
					sleepDuration = (int) (cFrame * cFrameLength - (System.currentTimeMillis() - cFirstFrameTime));
				} while (sleepDuration <= 0 && !cBreakLoop);
				
				repaint();
				
				if (!cBreakLoop) {
					try {
						Thread.sleep(sleepDuration);
					} catch (Exception e) {}
				}
				cBreakLoop = false;
			} else {
				try {
					Thread.sleep(cFrameLength);
				} catch (Exception e) {}
			}
		}
		cGameThreadStopped = true;
	}
	
	/**
	 * Copies the buffer to theGraphics, at the specified coordinates.
	 */
	protected void copyBuffer(Graphics theGraphics, int left, int top) {
		if (cPixelBuffer != null && cBuffer != null)
			cBufferGraphics.drawImage(cPixelBuffer, 0, 0, null);
		
		if (cBuffer != null)
			theGraphics.drawImage(cBuffer, left, top, null);
	}
	
	/**
	 * Copies the buffer to screen in response to repaint() being invoked.
	 */
	public void update(Graphics theGraphics) {
		copyBuffer(theGraphics, 0, 0);
	}
	
	/**
	 * Copies the buffer to screen in response to the view being "damaged".
	 */
	public void paint(Graphics theGraphics) {
		copyBuffer(theGraphics, 0, 0);
	}
	
	/**
	 * Initializes keys that, when pressed in sequence, will cause
	 * cheatActivated() to be invoked.
	 */
	protected void setCheats(int[][] keyCode) {
		cCheatCode = keyCode;
		cCheatProgress = new int[cCheatCode.length];
	}
	
	/**
	 * Initializes key handling. Returns true if successful, false if
	 * unsuccessful, and takes the following arguments:
	 * <ul>
	 * <li>keyRepeatDelay is the number of frames between
	 * activations of repeating keys.
	 * <li>keyRepeatFrequency is the number of frames until key
	 * repetitions begin.
	 * <li>keyCode is an array of virtual key codes, see the
	 * constants in KeyEvent.java
	 * <li>keyType is an array of key types, kKeyContinous means the
	 * key is considered active every frame the key is held,
	 * kKeyRepeat means the key is considered active every
	 * keyRepeatFrequency frames, with a delay of keyRepeatDelay, and
	 * kKeyOnce means the key is considered active only the frame it
	 * was pressed.
	 * </ul>
	 */
	protected boolean setKeys(int keyRepeatDelay, int keyRepeatFrequency,
			int[] keyCode, int[] keyType) {
		if (keyCode == null || keyCode.length <= 0)
			return false;
		if (keyType == null || keyType.length <= 0)
			return false;
		if (keyCode.length != keyType.length)
			return false;
		
		cKeyRepeatDelay = keyRepeatDelay;
		cKeyRepeatFrequency = keyRepeatFrequency;
		
		cKeyCount = keyCode.length;
		
		cKeyCode = keyCode;
		cKeyType = keyType;
		
		cKeyDown = new boolean[cKeyCount];
		cKeyDownFrame = new int[cKeyCount];
		for (int keyCounter = 0; keyCounter < cKeyCount; keyCounter++)
			cKeyDownFrame[keyCounter] = -1;
		
		return true;
	}
	
	/**
	 * Evaluates if a key is active, meaning pressed or in a state
	 * of repeated presses. This is the preferred method of
	 * determining whether a key is pressed. See setKeys() for
	 * more information.
	 */
	public boolean isKeyActive(int keyIndex) {
		if (keyIndex < 0 || keyIndex >= cKeyCount)
			return false;
		
		if (cKeyDownFrame[keyIndex] == cFrame)
			return true;
		
		if (!cKeyDown[keyIndex])
			return false;
		
		if (cKeyType[keyIndex] == kKeyContinous)
			return true;
		else if (cKeyType[keyIndex] == kKeyRepeat) {
			if (cFrame - cKeyDownFrame[keyIndex] > cKeyRepeatDelay) // is repeating and
				if ((cFrame - cKeyDownFrame[keyIndex] - cKeyRepeatDelay) % cKeyRepeatFrequency == 0) // frequency matches with frame
					return true;
		} else if (cKeyType[keyIndex] == kKeyOnce)
			return false;
		
		return false;
	}
	
	/**
	 * Responds to a cheat being typed. Override to add custom cheat
	 * functionality. Initialize cheats with setCheats().
	 */
	protected void cheatActivated(int cheatID) {}
	
	/**
	 * Sees if a cheat has been (partially) typed.
	 */
	protected void checkCheats(int keyEventCode) {
		int							cheatCounter;
		boolean						setAnyCheat;
		
		setAnyCheat = false;
		
		for (cheatCounter = 0; cheatCounter < cCheatCode.length; cheatCounter++) {
			if (keyEventCode == cCheatCode[cheatCounter][cCheatProgress[cheatCounter]]) {
				cCheatProgress[cheatCounter]++;
				if (cCheatProgress[cheatCounter] >= cCheatCode[cheatCounter].length) {
					cheatActivated(cheatCounter);
					cCheatProgress[cheatCounter] = 0;
				}
				setAnyCheat = true;
			}
		}
		
		if (!setAnyCheat)
			for (cheatCounter = 0; cheatCounter < cCheatProgress.length; cheatCounter++)
				cCheatProgress[cheatCounter] = 0;
	}
	
	public void keyPressed(KeyEvent theEvent) {
		int							keyEventCode;
		int							keyCounter;
		
		keyEventCode = theEvent.getKeyCode();
		
		if (cCheatCode != null)
			checkCheats(keyEventCode);
		
		for (keyCounter = 0; keyCounter < cKeyCount; keyCounter++)
			if (keyEventCode == cKeyCode[keyCounter]) {
				if (!cKeyDown[keyCounter])
					cKeyDownFrame[keyCounter] = cFrame + 1; // active next frame
				cKeyDown[keyCounter] = true;
				return;
			}
		
		if (cGameInProgress && keyEventCode == KeyEvent.VK_P)
			pauseGame(!cGamePaused);
		else if (cGameInProgress && keyEventCode == KeyEvent.VK_ESCAPE)
			pauseGame(true);
	}
	
	public void keyReleased(KeyEvent theEvent) {
		int							keyEventCode;
		int							keyCounter;
		
		keyEventCode = theEvent.getKeyCode();
		
		for (keyCounter = 0; keyCounter < cKeyCount; keyCounter++)
			if (keyEventCode == cKeyCode[keyCounter]) {
				cKeyDown[keyCounter] = false;
				if (cGamePaused)
					cKeyDownFrame[keyCounter] = cFrame - 10;
				return;
			}
	}
	
	// Required for implementing KeyListener
	public void keyTyped(KeyEvent theEvent) {}
}


