/*
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lwjgl.examples.spaceinvaders;

import java.util.ArrayList;

import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

import static org.lwjgl.opengl.GL11.*;

/**
 * The main hook of our game. This class with both act as a manager
 * for the display and central mediator for the game logic.
 *
 * Display management will consist of a loop that cycles round all
 * entities in the game asking them to move and then drawing them
 * in the appropriate place. With the help of an inner class it
 * will also allow the player to control the main ship.
 *
 * As a mediator it will be informed when entities within our game
 * detect events (e.g. alient killed, played died) and will take
 * appropriate game actions.
 *
 * <p>
 * NOTE:<br>
 * This game is a LWJGLized implementation of the Space Invaders game by Kevin
 * Glass. The original implementation is renderer agnostic and supports other
 * OpenGL implementations as well as Java2D. This version has been made specific
 * for LWJGL, and has added input control as well as sound (which the original doesn't,
 * at the time of writing).
 * You can find the original article here:<br>
 * <a href="http://www.cokeandcode.com/" target="_blank">http://www.cokeandcode.com</a>
 * </p>
 *
 * @author Kevin Glass
 * @author Brian Matzon
 */
public class Game {

	/** The normal title of the window */
	private String				WINDOW_TITLE					= "Space Invaders 104 (for LWJGL)";

	/** The width of the game display area */
	private int						width									= 800;

	/** The height of the game display area */
	private int						height								= 600;

	/** The loader responsible for converting images into OpenGL textures */
	private TextureLoader	textureLoader;

	/** The list of all the entities that exist in our game */
	private ArrayList<Entity>			entities							= new ArrayList<Entity>();

	/** The list of entities that need to be removed from the game this loop */
	private ArrayList<Entity>			removeList						= new ArrayList<Entity>();

	/** The entity representing the player */
	private ShipEntity		ship;

	/** List of shots */
	private ShotEntity[]	shots;

	/** The message to display which waiting for a key press */
	private Sprite				message;

	/** The sprite containing the "Press Any Key" message */
	private Sprite				pressAnyKey;

	/** The sprite containing the "You win!" message */
	private Sprite				youWin;

	/** The sprite containing the "You lose!" message */
	private Sprite				gotYou;

	/** Last shot index */
	private int						shotIndex;

	/** The speed at which the player's ship should move (pixels/sec) */
	private float					moveSpeed							= 300;

	/** The time at which last fired a shot */
	private long					lastFire;

	/** The interval between our players shot (ms) */
	private long					firingInterval				= 500;

	/** The number of aliens left on the screen */
	private int						alienCount;

	/** True if we're holding up game play until a key has been pressed */
	private boolean				waitingForKeyPress		= true;

	/** True if game logic needs to be applied this loop, normally as a result of a game event */
	private boolean				logicRequiredThisLoop;

	/** The time at which the last rendering looped started from the point of view of the game logic */
	private long					lastLoopTime					= getTime();

	/** True if the fire key has been released */
	private boolean				fireHasBeenReleased;

	/** The time since the last record of fps */
	private long					lastFpsTime;

	/** The recorded fps */
	private int						fps;

	private static long		timerTicksPerSecond		= Sys.getTimerResolution();

	/** True if the game is currently "running", i.e. the game loop is looping */
	public static boolean	gameRunning						= true;

	/** SoundManager to make sound with */
	private SoundManager	soundManager;

	/** Whether we're running in fullscreen mode */
	private boolean				fullscreen;

	/** ID of shot effect */
	private int						SOUND_SHOT;

	/** ID of hit effect */
	private int						SOUND_HIT;

	/** ID of start sound */
	private int						SOUND_START;

	/** ID of win sound */
	private int						SOUND_WIN;

	/** ID of loose sound */
	private int						SOUND_LOOSE;

  /** Mouse movement on x axis */
	private int	mouseX;

	/** Is this an application or applet */
	private static boolean isApplication;

	/**
	 * Construct our game and set it running.
	 * @param fullscreen
	 *
	 */
	public Game(boolean fullscreen) {
		this.fullscreen = fullscreen;
		initialize();
	}

	/**
	 * Get the high resolution time in milliseconds
	 *
	 * @return The high resolution time in milliseconds
	 */
	public static long getTime() {
		// we get the "timer ticks" from the high resolution timer
		// multiply by 1000 so our end result is in milliseconds
		// then divide by the number of ticks in a second giving
		// us a nice clear time in milliseconds
		return (Sys.getTime() * 1000) / timerTicksPerSecond;
	}

	/**
	 * Sleep for a fixed number of milliseconds.
	 *
	 * @param duration The amount of time in milliseconds to sleep for
	 */
	public static void sleep(long duration) {
		try {
			Thread.sleep((duration * timerTicksPerSecond) / 1000);
		} catch (InterruptedException inte) {
		}
	}

	/**
	 * Intialise the common elements for the game
	 */
	public void initialize() {
		// initialize the window beforehand
		try {
			setDisplayMode();
			Display.setTitle(WINDOW_TITLE);
			Display.setFullscreen(fullscreen);
			Display.create();

			// grab the mouse, dont want that hideous cursor when we're playing!
			if (isApplication) {
				Mouse.setGrabbed(true);
			}

			// enable textures since we're going to use these for our sprites
			glEnable(GL_TEXTURE_2D);

			// disable the OpenGL depth test since we're rendering 2D graphics
			glDisable(GL_DEPTH_TEST);

			glMatrixMode(GL_PROJECTION);
			glLoadIdentity();

			glOrtho(0, width, height, 0, -1, 1);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			glViewport(0, 0, width, height);

			textureLoader = new TextureLoader();

      // create our sound manager, and initialize it with 7 channels
      // 1 channel for sounds, 6 for effects - this should be enough
      // since we have a most 4 shots on screen at any one time, which leaves
      // us with 2 channels for explosions.
			soundManager = new SoundManager();
			soundManager.initialize(8);

      // load our sound data
			SOUND_SHOT   = soundManager.addSound("shot.wav");
			SOUND_HIT    = soundManager.addSound("hit.wav");
			SOUND_START  = soundManager.addSound("start.wav");
			SOUND_WIN    = soundManager.addSound("win.wav");
			SOUND_LOOSE  = soundManager.addSound("loose.wav");
		} catch (LWJGLException le) {
			System.out.println("Game exiting - exception in initialization:");
			le.printStackTrace();
			Game.gameRunning = false;
      return;
		}

		// get our sprites
		gotYou = getSprite("gotyou.gif");
		pressAnyKey = getSprite("pressanykey.gif");
		youWin = getSprite("youwin.gif");

		message = pressAnyKey;

		// setup 5 shots
		shots = new ShotEntity[5];
		for (int i = 0; i < shots.length; i++) {
			shots[i] = new ShotEntity(this, "shot.gif", 0, 0);
		}

		// setup the initial game state
		startGame();
	}

	/**
   * Sets the display mode for fullscreen mode
	 */
	private boolean setDisplayMode() {
    try {
		// get modes
		DisplayMode[] dm = org.lwjgl.util.Display.getAvailableDisplayModes(width, height, -1, -1, -1, -1, 60, 60);

      org.lwjgl.util.Display.setDisplayMode(dm, new String[] {
          "width=" + width,
          "height=" + height,
          "freq=" + 60,
          "bpp=" + org.lwjgl.opengl.Display.getDisplayMode().getBitsPerPixel()
         });
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      System.out.println("Unable to enter fullscreen, continuing in windowed mode");
    }

		return false;
	}

	/**
	 * Start a fresh game, this should clear out any old data and
	 * create a new set.
	 */
	private void startGame() {
		// clear out any existing entities and intialise a new set
		entities.clear();
		initEntities();
	}

	/**
	 * Initialise the starting state of the entities (ship and aliens). Each
	 * entitiy will be added to the overall list of entities in the game.
	 */
	private void initEntities() {
		// create the player ship and place it roughly in the center of the screen
		ship = new ShipEntity(this, "ship.gif", 370, 550);
		entities.add(ship);

		// create a block of aliens (5 rows, by 12 aliens, spaced evenly)
		alienCount = 0;
		for (int row = 0; row < 5; row++) {
			for (int x = 0; x < 12; x++) {
				Entity alien = new AlienEntity(this, 100 + (x * 50), (50) + row * 30);
				entities.add(alien);
				alienCount++;
			}
		}
	}

	/**
	 * Notification from a game entity that the logic of the game
	 * should be run at the next opportunity (normally as a result of some
	 * game event)
	 */
	public void updateLogic() {
		logicRequiredThisLoop = true;
	}

	/**
	 * Remove an entity from the game. The entity removed will
	 * no longer move or be drawn.
	 *
	 * @param entity The entity that should be removed
	 */
	public void removeEntity(Entity entity) {
		removeList.add(entity);
	}

	/**
	 * Notification that the player has died.
	 */
	public void notifyDeath() {
		if (!waitingForKeyPress) {
			soundManager.playSound(SOUND_LOOSE);
		}
		message = gotYou;
		waitingForKeyPress = true;
	}

	/**
	 * Notification that the player has won since all the aliens
	 * are dead.
	 */
	public void notifyWin() {
		message = youWin;
		waitingForKeyPress = true;
		soundManager.playSound(SOUND_WIN);
	}

	/**
	 * Notification that an alien has been killed
	 */
	public void notifyAlienKilled() {
		// reduce the alient count, if there are none left, the player has won!
		alienCount--;

		if (alienCount == 0) {
			notifyWin();
		}

		// if there are still some aliens left then they all need to get faster, so
		// speed up all the existing aliens
		for ( Entity entity : entities ) {
			if ( entity instanceof AlienEntity ) {
				// speed up by 2%
				entity.setHorizontalMovement(entity.getHorizontalMovement() * 1.02f);
			}
		}

		soundManager.playEffect(SOUND_HIT);
	}

	/**
	 * Attempt to fire a shot from the player. Its called "try"
	 * since we must first check that the player can fire at this
	 * point, i.e. has he/she waited long enough between shots
	 */
	public void tryToFire() {
		// check that we have waiting long enough to fire
		if (System.currentTimeMillis() - lastFire < firingInterval) {
			return;
		}

		// if we waited long enough, create the shot entity, and record the time.
		lastFire = System.currentTimeMillis();
		ShotEntity shot = shots[shotIndex++ % shots.length];
		shot.reinitialize(ship.getX() + 10, ship.getY() - 30);
		entities.add(shot);

		soundManager.playEffect(SOUND_SHOT);
	}

	/**
	 * Run the main game loop. This method keeps rendering the scene
	 * and requesting that the callback update its screen.
	 */
	private void gameLoop() {
		while (Game.gameRunning) {
			// clear screen
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();

			// let subsystem paint
			frameRendering();

			// update window contents
			Display.update();
		}

		// clean up
		soundManager.destroy();
		Display.destroy();
	}

	/**
	 * Notification that a frame is being rendered. Responsible for
	 * running game logic and rendering the scene.
	 */
	public void frameRendering() {
		//SystemTimer.sleep(lastLoopTime+10-SystemTimer.getTime());
		Display.sync(60);

		// work out how long its been since the last update, this
		// will be used to calculate how far the entities should
		// move this loop
		long delta = getTime() - lastLoopTime;
		lastLoopTime = getTime();
		lastFpsTime += delta;
		fps++;

		// update our FPS counter if a second has passed
		if (lastFpsTime >= 1000) {
			Display.setTitle(WINDOW_TITLE + " (FPS: " + fps + ")");
			lastFpsTime = 0;
			fps = 0;
		}

		// cycle round asking each entity to move itself
		if (!waitingForKeyPress && !soundManager.isPlayingSound()) {
			for ( Entity entity : entities ) {
				entity.move(delta);
			}
		}

		// cycle round drawing all the entities we have in the game
		for ( Entity entity : entities ) {
			entity.draw();
		}

		// brute force collisions, compare every entity against
		// every other entity. If any of them collide notify
		// both entities that the collision has occured
		for (int p = 0; p < entities.size(); p++) {
			for (int s = p + 1; s < entities.size(); s++) {
				Entity me = entities.get(p);
				Entity him = entities.get(s);

				if (me.collidesWith(him)) {
					me.collidedWith(him);
					him.collidedWith(me);
				}
			}
		}

		// remove any entity that has been marked for clear up
		entities.removeAll(removeList);
		removeList.clear();

		// if a game event has indicated that game logic should
		// be resolved, cycle round every entity requesting that
		// their personal logic should be considered.
		if (logicRequiredThisLoop) {
			for ( Entity entity : entities ) {
				entity.doLogic();
			}

			logicRequiredThisLoop = false;
		}

		// if we're waiting for an "any key" press then draw the
		// current message
		if (waitingForKeyPress) {
			message.draw(325, 250);
		}

		// resolve the movemfent of the ship. First assume the ship
		// isn't moving. If either cursor key is pressed then
		// update the movement appropraitely
		ship.setHorizontalMovement(0);

    // get mouse movement on x axis. We need to get it now, since
    // we can only call getDX ONCE! - secondary calls will yield 0, since
    // there haven't been any movement since last call.
    mouseX = Mouse.getDX();

    // we delegate input checking to submethod since we want to check
    // for keyboard, mouse & controller
		boolean leftPressed   = hasInput(Keyboard.KEY_LEFT);
		boolean rightPressed  = hasInput(Keyboard.KEY_RIGHT);
		boolean firePressed   = hasInput(Keyboard.KEY_SPACE);

		if (!waitingForKeyPress && !soundManager.isPlayingSound()) {
			if ((leftPressed) && (!rightPressed)) {
				ship.setHorizontalMovement(-moveSpeed);
			} else if ((rightPressed) && (!leftPressed)) {
				ship.setHorizontalMovement(moveSpeed);
			}

			// if we're pressing fire, attempt to fire
			if (firePressed) {
				tryToFire();
			}
		} else {
			if (!firePressed) {
				fireHasBeenReleased = true;
			}
			if ((firePressed) && (fireHasBeenReleased) && !soundManager.isPlayingSound()) {
				waitingForKeyPress = false;
				fireHasBeenReleased = false;
				startGame();
				soundManager.playSound(SOUND_START);
			}
		}

		// if escape has been pressed, stop the game
		if ((Display.isCloseRequested() || Keyboard.isKeyDown(Keyboard.KEY_ESCAPE)) && isApplication) {
			Game.gameRunning = false;
		}
	}

	/**
	 * @param direction
	 * @return
	 */
	private boolean hasInput(int direction) {
    switch(direction) {
    	case Keyboard.KEY_LEFT:
        return
          Keyboard.isKeyDown(Keyboard.KEY_LEFT) ||
          mouseX < 0;

      case Keyboard.KEY_RIGHT:
        return
          Keyboard.isKeyDown(Keyboard.KEY_RIGHT) ||
          mouseX > 0;

      case Keyboard.KEY_SPACE:
        return
          Keyboard.isKeyDown(Keyboard.KEY_SPACE) ||
          Mouse.isButtonDown(0);
    }
		return false;
	}

	/**
	 * The entry point into the game. We'll simply create an
	 * instance of class which will start the display and game
	 * loop.
	 *
	 * @param argv The arguments that are passed into our game
	 */
	public static void main(String argv[]) {
		isApplication = true;
		System.out.println("Use -fullscreen for fullscreen mode");
		new Game((argv.length > 0 && "-fullscreen".equalsIgnoreCase(argv[0]))).execute();
		System.exit(0);
	}

	/**
	 *
	 */
	public void execute() {
		gameLoop();
	}

	/**
	 * Create or get a sprite which displays the image that is pointed
	 * to in the classpath by "ref"
	 *
	 * @param ref A reference to the image to load
	 * @return A sprite that can be drawn onto the current graphics context.
	 */
	public Sprite getSprite(String ref) {
		return new Sprite(textureLoader, ref);
	}
}
