package de.rico.adventure.main;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Locale;

import javax.imageio.ImageIO;
import javax.media.opengl.GL;
import javax.media.opengl.GLDrawable;

import de.rico.adventure.actions.ActionScheduler;
import de.rico.adventure.actions.custom.SetChapter;
import de.rico.adventure.entity.character.NonPlayerCharacter;
import de.rico.adventure.enums.CustomCursor;
import de.rico.adventure.enums.State;
import de.rico.adventure.io.ChapterReader;
import de.rico.adventure.structure.Chapter;
import de.rico.adventure.structure.StateManager;
import de.rico.engine.camera.Camera;
import de.rico.engine.cursor.CursorManager;
import de.rico.engine.effect.ortho.ImageRenderer;
import de.rico.engine.effect.ortho.ListMenuRenderer;
import de.rico.engine.effect.ortho.OverlayImage;
import de.rico.engine.effect.ortho.RectangleRenderer;
import de.rico.engine.effect.ortho.TextRenderer;
import de.rico.engine.effect.particle.custom.Explosion;
import de.rico.engine.effect.sound.SampledSound;
import de.rico.engine.enums.Font;
import de.rico.engine.enums.ShadowType;
import de.rico.engine.game.AbstractBaseGame;
import de.rico.engine.geometry.Line3D;
import de.rico.engine.geometry.Point3D;
import de.rico.engine.geometry.collision.CollisionFinder;
import de.rico.engine.input.InputAction;
import de.rico.engine.input.InputManager;
import de.rico.engine.material.Color4f;
import de.rico.engine.util.FileFinder;
import de.rico.engine.util.Tools;
import extra.BossManager;

/**
 * This class is the main class for an adventure game, that is
 * built with the framework. It implements the singleton pattern.
 * @author Frank Bruns
 *
 */
public class ShootOut3DMain extends AbstractBaseGame
{
	private static final int MAGAZINE_SIZE = 8;
	private static final int ENERGY_MAX = 100;
	
	private int playerEnergyLeft = ENERGY_MAX;
	private int bossEnergyLeft = 9999;
	
	// singleton instance of the game
	private static ShootOut3DMain instance = new ShootOut3DMain("Borg Assault (by Frank Bruns)",50);

	private static Chapter chapter = null;
	private int levelNr = 1;
	private int numberOfLevels = FileFinder.getInstance().getFiles("./levels/",".chp").size();

	private static InputManager inputManager = null;

	private InputAction mouseBtnLeft = null;

	private InputAction mouseBtnMiddle = null;

	private InputAction mouseBtnRight = null;
	
	private InputAction mouseLeft = null;
	private InputAction mouseRight = null;
	private InputAction mouseUp = null;
	private InputAction mouseDown = null;

	private InputAction keyEsc = null;

	private InputAction keyF = null;
	
	private InputAction keySpace = null;

	private TextRenderer textRenderer = null;
	private ImageRenderer imageRenderer = null;
	private OverlayImage crosshair = null;

	private RectangleRenderer blackBarsRenderer = null;

	private ListMenuRenderer gameMenu = null;

	private String loadingInfo = "";

	// variable to backup the current state
	private State stateBackup = State.MENU;

	private ShadowType shadowType = ShadowType.NO_SHADOWS;
	
	private int shotsFired = 0;
	private int shotsHit = 0;
	private int shotsInMag = MAGAZINE_SIZE;
	private int enemiesLeft = -1;

	private SampledSound music = new SampledSound("./bg_music2.wav",true);
	
	private SampledSound soundShot = new SampledSound("./shot.wav",false);
	private SampledSound soundExplosion = new SampledSound("./explosion.wav",false);
	private SampledSound soundExplosionBoss = new SampledSound("./explosion_boss.wav",false);
	private SampledSound soundHit = new SampledSound("./hit.wav",false);
	private SampledSound soundShieldHit = new SampledSound("./shield_hit.wav",false);
	private SampledSound soundReloadMag = new SampledSound("./reload_mag.wav",false);
	private SampledSound soundBorgIntro = null; // initialized in initReosurces()
	
	private SampledSound soundReloadWarn = new SampledSound("./reload_warn.wav",true);	
	private boolean playsReloadWarnSound = false;
	
	private long timeToWin = 0L;
	
	private BossManager bossManager = null;
	
	/**
	 * Private construcor according to the singleton pattern.
	 * @param title tile for the game's window bar
	 * @param maxFps maximum framerate to fix the game to
	 */
	private ShootOut3DMain(String title, int maxFps)
	{
		super(title, maxFps);			
	}

	/**
	 * Returns the singleton instance of the game.
	 * @return the game's singlton instance
	 */
	public static ShootOut3DMain getInstance()
	{
		return instance;
	}

	/**
	 * Sets the text that gets displayed as the loading info.
	 * @param text loading info text
	 */
	public void setLoadingInfoText(String text)
	{
		this.loadingInfo = text;
	}


	/**
	 * Returns the current shadow type used in the game.
	 * @return currrent shadow type
	 */
	public ShadowType getShadowType()
	{
		return this.shadowType;
	}

	/**
	 * Sets the shadow type to use in the game.
	 * @param type value from the ShadowType enumeration
	 */
	public void setShadowType(ShadowType type)
	{
		this.shadowType = type;
	}

	/**
	 * This method can be used to display information on screen,
	 * while the render thread actually is busy with some work.
	 * This method draws things with AWT's Graphics2D object and is
	 * independent from the OpenGL render thread.
	 * @param backgroundImage path to image file that ought to be
	 * displayed during the loading process
	 */
	public void showLoadingInfo(final String backgroundImage)
	{
		Thread loadingInfoThread = new Thread(new Runnable()
		{
			public void run()
			{
				BufferedImage img = null;

				if(backgroundImage != null)
				{
					try
					{
						img = ImageIO.read(new File(backgroundImage));
					}
					catch (IOException e)
					{
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}

				Graphics2D g = (Graphics2D)ShootOut3DMain.getInstance().getOGLCanvas().getGraphics().create();
				g.setStroke(new BasicStroke(2));

				while(StateManager.getInstance().equals(State.LOADING))
				{
					if(img != null)
						g.drawImage(
				    			img, 0, 0,
				    			Camera.getInstance().getViewPort().getWidth(),
				    			Camera.getInstance().getViewPort().getHeight(), null);

					g.setBackground(Color.BLACK);
					g.setColor(Color.BLACK);

			    	g.fillRect(2,
							Camera.getInstance().getViewPort().getHeight()-35,
							Camera.getInstance().getViewPort().getWidth()-2,20);


			    	g.setColor(Color.WHITE);
			    	g.drawString(ShootOut3DMain.this.loadingInfo, 10, Camera.getInstance().getViewPort().getHeight()-40);

			    	try
					{
			    		Thread.sleep(100); // put the thread asleep
					}
					catch (InterruptedException e)
					{
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}

				g.dispose();
			}
		});

		// start the thread
		loadingInfoThread.start();
	}

	/* (non-Javadoc)
	 * @see de.rico.engine.game.AbstractBaseGame#initResources(javax.media.opengl.GLDrawable, javax.media.opengl.GL, int, int)
	 */
	@Override
	public void initResources(GLDrawable gld, GL gl, int width, int height)
	{
		//Log.setLogFile("test_adventure.log");

		// create all the neccessary controls
		inputManager = new InputManager(this.getOGLCanvas());
		
		this.bossManager = new BossManager();
		
		this.textRenderer = new TextRenderer(Font.HELVETICA_18);
		
		this.imageRenderer = new ImageRenderer();
		this.crosshair = new OverlayImage("./crosshair.png");

		// create rectangle renderer for black bars
		this.blackBarsRenderer = new RectangleRenderer(new Color4f(0f,0f,0f,1f));

		this.mouseBtnLeft = new InputAction("left click",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		this.mouseBtnMiddle = new InputAction("middle press");
		this.mouseBtnRight = new InputAction("right click",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		inputManager.assignMouse(this.mouseBtnLeft,
				InputManager.MOUSE_BUTTON_1);
		inputManager.assignMouse(this.mouseBtnMiddle, InputManager.MOUSE_BUTTON_2);
		inputManager.assignMouse(this.mouseBtnRight,
				InputManager.MOUSE_BUTTON_3);

		this.keyEsc = new InputAction("Quit",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		inputManager.assignKey(this.keyEsc, KeyEvent.VK_ESCAPE);
		
		this.keySpace = new InputAction("Next Level",InputAction.DETECT_INITAL_PRESS_ONLY);
		this.inputManager.assignKey(this.keySpace, KeyEvent.VK_SPACE);
		

		// for (de)activating fps count rendering
		this.keyF = new InputAction("show fps",InputAction.DETECT_INITAL_PRESS_ONLY);
		inputManager.assignKey(this.keyF, KeyEvent.VK_F);
		
		this.mouseLeft = new InputAction("mouse left");
		this.inputManager.assignMouse(this.mouseLeft, InputManager.MOUSE_MOVE_LEFT);
		this.mouseRight = new InputAction("mouse right");
		this.inputManager.assignMouse(this.mouseRight, InputManager.MOUSE_MOVE_RIGHT);
		this.mouseUp = new InputAction("mouse up");
		this.inputManager.assignMouse(this.mouseUp, InputManager.MOUSE_MOVE_UP);
		this.mouseDown = new InputAction("mouse down");
		this.inputManager.assignMouse(this.mouseDown, InputManager.MOUSE_MOVE_DOWN);

		// init game cursors, make mouse pointer invisible first
		//inputManager.setCursor(InputManager.INVISIBLE_CURSOR);		

		if(Locale.getDefault().getCountry().equalsIgnoreCase("de"))
			this.soundBorgIntro = new SampledSound("./borg_intro_german.wav",false);
		else this.soundBorgIntro = new SampledSound("./borg_intro_english.wav",false);

		// init menus
		this.gameMenu = new ListMenuRenderer(
				0,0,20,40,
				new Color4f(0.5f,0.7f,0.3f, 1f),
				new Color4f(1f,0f,0f,1f),
				new Color4f(0f,0f,0f,0.65f), Font.TIMES_ROMAN_24);
		this.gameMenu.add("Start new game", "new");	
		this.gameMenu.add("Restart current level", "restart");
		//this.gameMenu.add("Options", "options");
		//this.gameMenu.add("Load a game", "load");
		//this.gameMenu.add("Save the game", "save");
		this.gameMenu.add("Quit game", "quit");
	}

	/* (non-Javadoc)
	 * @see de.rico.engine.game.AbstractBaseGame#update(long)
	 */
	@Override
	public void update(long elapsedTime)
	{				
		//this.handleCameraControls(elapsedTime);
		
		// show/hide fps
		if(this.keyF.isPressed())
		{
			if(ShootOut3DMain.getInstance().isSetDisplayFPS() == false)
				ShootOut3DMain.getInstance().setDisplayFPS(true);
			else ShootOut3DMain.getInstance().setDisplayFPS(false);
		}

		// press ESC key to enter and leave the game menu
		if(this.keyEsc.isPressed())
		{
			if(StateManager.getInstance().equals(State.MENU))
				StateManager.getInstance().setState(this.stateBackup);
			else
			{
				this.stateBackup = StateManager.getInstance().getState();
				StateManager.getInstance().setState(State.MENU);
			}
		}		

		if(StateManager.getInstance().equals(State.MENU))
		{
			this.inputManager.setEnabledRelativeMouseMode(false);
			this.inputManager.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
			
			if(this.mouseBtnLeft.isPressed())
			{
				this.mouseBtnLeft.reset();

				// process event from the menu items
				String selectedEntry = this.gameMenu.getEntryId(
						inputManager.getMouseX(),
						inputManager.getMouseY());
				if(selectedEntry == null) return; // nothing selected

				// handles selections in the main menu
				this.handleMainMenu(selectedEntry);

				// this method handles selections while in options menu
				this.handleOptionsMenu(selectedEntry);									
			}						
			
			this.keySpace.reset();

			return; // do nothing else, especially don't update world
		}


		// update active chapter data
		if(chapter != null) chapter.update(elapsedTime);

		ActionScheduler.getInstance().update(elapsedTime);

		// player input handling
		// -----------------------

		// consume mouse clicks, that occured during loading
		if(this.stateBackup.equals(State.LOADING))
		{
			this.mouseBtnLeft.reset();
			this.stateBackup = State.MENU;
		}

		
		if(StateManager.getInstance().equals(State.NORMAL))
		{			
			if(this.enemiesLeft == -1)
			{				
				this.music.play();
				this.soundBorgIntro.play();																	
			}			
					
			this.bossManager.update(elapsedTime);
			
			this.enemiesLeft = 0;
			for(NonPlayerCharacter npc : chapter.getActiveRoom().getCharacters())
			{								
				if(npc.getScriptId().contains("_boss"))
					this.enemiesLeft++;				
				else if(npc.isEnabled() && npc.getScriptId().contains("_bomb"))
					this.enemiesLeft++;
			}			
			
			// only count time while enemies available
			if(this.enemiesLeft > 0)
				this.timeToWin += elapsedTime;
			
			ArrayList<NonPlayerCharacter> bombs = new ArrayList<NonPlayerCharacter>();
			for(NonPlayerCharacter b : chapter.getActiveRoom().getCharacters())
			{
				if(b.isEnabled() == true && b.getScriptId().contains("_bomb"))
					bombs.add(b);
			}
			
			for(NonPlayerCharacter b : bombs)
			{
				//b.getMesh().moveTo(
					//Camera.getInstance().getPosition().x,
					//Camera.getInstance().getPosition().z, 0.0035f, 0.01f, elapsedTime);
				b.getMesh().moveDownGlobal(b.getBombSpeed(), elapsedTime);				
				
				// check for crash on ground				
				if(b.isEnabled() && b.getMesh().getPosY() < 0)
				{
					// explode on ground
					this.playerEnergyLeft-= b.getBombDamage();
					this.enemiesLeft--;
					
					this.soundExplosion.play();
					
					Explosion explosion1 = new Explosion(
							b.getMesh().getPosX(),
							b.getMesh().getPosY(),
							b.getMesh().getPosZ(),
							1f,50,1000,false);
					explosion1.setTexture("./explosion4.png");
					
					Explosion explosion2 = new Explosion(
							b.getMesh().getPosX(),
							b.getMesh().getPosY(),
							b.getMesh().getPosZ(),
							0.65f,10,1250,false);
					explosion2.setTexture("./explosion2.png");
					
					Explosion explosion3 = new Explosion(
							b.getMesh().getPosX(),
							b.getMesh().getPosY(),
							b.getMesh().getPosZ(),
							0.65f,5,1500,false);
					explosion3.setTexture("./explosion5.png");
										
					
					chapter.getActiveRoom().addParticleEffect(explosion1);
					chapter.getActiveRoom().addParticleEffect(explosion2);
					chapter.getActiveRoom().addParticleEffect(explosion3);
					//chapter.getActiveRoom().removeCharacter(b);
					
					// make bomb invisible
					b.getMesh().setDrawingEnabled(false);
					b.setEnabled(false);
				}
			}
			
			bombs.clear();
			
			
			this.inputManager.setEnabledRelativeMouseMode(true);
			this.inputManager.setCursor(InputManager.INVISIBLE_CURSOR);
			Camera.getInstance().rotateDown(this.mouseDown.getAmount());
			Camera.getInstance().rotateUp(this.mouseUp.getAmount());
			Camera.getInstance().rotateRight(this.mouseRight.getAmount());
			Camera.getInstance().rotateLeft(this.mouseLeft.getAmount());
			
			
			
			if(this.playerEnergyLeft <= 0)
			{
				StateManager.getInstance().setState(State.LEVEL_LOST);
			}
			
			if(this.bossEnergyLeft <= 0)
			{
				StateManager.getInstance().setState(State.LEVEL_COMPLETE);
			}
		}		
		
		// fire your guns
		if (this.mouseBtnLeft.isPressed())
		{
			if(StateManager.getInstance().equals(State.NORMAL))
			{
				if(this.shotsInMag > 0)
				{
					this.shotsFired++;
					this.shotsInMag--;
					
					this.soundShot.play();									
					
					
					// check for hits among targets
					for(NonPlayerCharacter target : this.chapter.getActiveRoom().getCharacters())
					{						
						
						boolean collision = target.getMesh().isMouseOverMesh(
								this.inputManager.getMouseX(),
								this.inputManager.getMouseY());
						
						boolean occluded = target.getMesh().isOccluded();
						
						if(collision && !occluded)
						{
							if(target.isEnabled() == true)
							{
								target.decreaseHitPoints(1);
								this.soundHit.play();
								
								this.shotsHit++;
								
								// Sparks for a hit
								Point3D pos = target.getMesh().getMouseOverMesh(
										this.inputManager.getMouseX(),
										this.inputManager.getMouseY());
								
								float sparkSize = 0;
								if(target.getScriptId().contains("_boss"))
									sparkSize = 2.75f;
								else sparkSize = 0.65f;
								
								Explosion sparks = new Explosion(pos.x, pos.y, pos.z,
										sparkSize,3,1000,false);
								sparks.setTexture("./explosion5.png");
								chapter.getActiveRoom().addParticleEffect(sparks);
								
								
								if(target.getHitPoints()<=0)
								{
									if(target.getScriptId().contains("_boss"))
										this.soundExplosionBoss.play();
									else this.soundExplosion.play();
									
									float explSizeFactor = 1f;
									int timeFactor = 1;
									if(target.getScriptId().contains("_boss"))
										explSizeFactor = 15.0f;
									if(target.getScriptId().contains("_boss"))
										timeFactor = 5;
									
									Explosion explosion1 = new Explosion(
											target.getMesh().getPosX(),
											target.getMesh().getPosY(),
											target.getMesh().getPosZ(),
											1f*explSizeFactor,10,1250*timeFactor,false);
									explosion1.setTexture("./explosion1.png");
									
									Explosion explosion4 = new Explosion(
											target.getMesh().getPosX(),
											target.getMesh().getPosY(),
											target.getMesh().getPosZ(),
											0.75f*explSizeFactor,30,1000*timeFactor,false);
									explosion4.setTexture("./explosion4.png");
									
									Explosion explosion5 = new Explosion(
											target.getMesh().getPosX(),
											target.getMesh().getPosY(),
											target.getMesh().getPosZ(),
											1f*explSizeFactor,15,1500*timeFactor,false);
									explosion5.setTexture("./explosion5.png");
									
									chapter.getActiveRoom().addParticleEffect(explosion1);
									chapter.getActiveRoom().addParticleEffect(explosion4);
									chapter.getActiveRoom().addParticleEffect(explosion5);																
									//chapter.getActiveRoom().removeCharacter(target);
									
									// make bomb invisible
									target.getMesh().setDrawingEnabled(false);
									target.setEnabled(false);
									
									this.enemiesLeft--;
									
									if(target.getScriptId().contains("_boss"))
										this.bossEnergyLeft = target.getHitPoints();
							}
							else if(target.getScriptId().contains("_boss"))
								this.soundShieldHit.play();														
							}													
						}
					}
				}
				else // no rounds in mag
				{
					if(this.playsReloadWarnSound == false)
					{
						this.soundReloadWarn.play();
						this.playsReloadWarnSound = true;
					}
				}
			}
			
			if(StateManager.getInstance().equals(State.CINEMATIC))
			{
				// Skip cinematic
				ActionScheduler.getInstance().removeFirst();												
				// skip intro sound
				this.soundBorgIntro.stop();
			}
		}
		
		if(StateManager.getInstance().equals(State.LEVEL_COMPLETE))
		{
			if(this.levelNr < this.numberOfLevels)
			{
				if(this.keySpace.isPressed())
				{
						this.resetLevelValues();
					
						this.levelNr++;
						this.setActiveChapter("./levels/level"+this.levelNr+".chp");
				}
			}						
		}
		
		if(StateManager.getInstance().equals(State.LEVEL_LOST))
		{
			
			
		}
		
		if(this.mouseBtnRight.isPressed())
		{
			if(StateManager.getInstance().equals(State.NORMAL))
			{
				//reload the mag			
				this.soundReloadMag.play();
				this.soundReloadWarn.stop();
				this.playsReloadWarnSound = false;
				this.shotsInMag = MAGAZINE_SIZE;
			}			
		}

		if(this.mouseBtnMiddle.isPressed())
		{
			
		}	


		// consume any input action that was not processed
		this.mouseBtnLeft.reset();
		this.mouseBtnRight.reset();
		this.keySpace.reset();

		// if the game state turns out to be null, set it to NORMAL
		// mode to avoid appilcation crashes
		if(StateManager.getInstance().getState() == null)
			StateManager.getInstance().setState(State.NORMAL);
	}

	/**
	 * Helper method to handle selections in the main menu.
	 * @param selectedEntry A string representing the selected entry
	 */
	private void handleMainMenu(String selectedEntry)
	{
		if(selectedEntry.equals("new"))
		{
			this.resetLevelValues();
			this.levelNr = 1;
			this.setActiveChapter("./levels/level"+this.levelNr+".chp"); // load start chapter
		}
		if(selectedEntry.equals("restart"))
		{
			this.resetLevelValues();
			this.setActiveChapter("./levels/level"+this.levelNr+".chp"); // load start chapter
		}
		if(selectedEntry.equals("options")) // create options menu content
		{
			this.gameMenu.clear(); // clear current menu content
			// set the options menu content
			switch(this.shadowType)
			{
				case NO_SHADOWS : this.gameMenu.add("Shadow type: No shadows", "no shadows"); break;
				case SHADOW_MATRIX : this.gameMenu.add("Shadow type: Shadow matrix", "shadow matrix"); break;
				case SHADOW_MAP : this.gameMenu.add("Shadow type: Shadow map (experimental!)", "shadow map"); break;
			}
			this.gameMenu.add("Back to main menu", "back to main");
		}
		if(selectedEntry.equals("quit"))
		{
			this.stop(); // quit the game savely
		}

		// used to return to the main menu
		if(selectedEntry.equals("back to main"))
		{
			this.gameMenu.clear(); // clear current menu cotent
			// restore main menu content
			this.gameMenu.add("Start new game", "new");
			this.gameMenu.add("Options", "options");
			this.gameMenu.add("Load a game", "load");
			this.gameMenu.add("Save the game", "save");
			this.gameMenu.add("Quit game", "quit");
		}
	}

	/**
	 * Resets some important values to be able to start the
	 * level with fresh values.
	 */
	private void resetLevelValues()
	{
		this.soundBorgIntro.stop();
		this.soundReloadWarn.stop();
		this.music.stop();
		this.timeToWin = 0L;
		this.enemiesLeft = -1;
		this.bossEnergyLeft = 9999;
		this.playerEnergyLeft = ENERGY_MAX;
		this.shotsInMag = MAGAZINE_SIZE;
		this.shotsFired = 0;
		this.shotsHit = 0;
		this.bossManager.reset();		
	}

	/**
	 * Helper method to handle selections in the options menu
	 * @param selectedEntry A string representing the selected entry
	 */
	private void handleOptionsMenu(String selectedEntry)
	{
		if(selectedEntry.equals("no shadows"))
		{
			this.gameMenu.clear(); // clear menu and set new content
			// set to next shadow type
			this.shadowType = ShadowType.SHADOW_MATRIX;
			this.gameMenu.add("Shadow type: Shadow matrix", "shadow matrix");
			this.gameMenu.add("Back to main menu", "back to main");
		}
		if(selectedEntry.equals("shadow matrix"))
		{
			this.gameMenu.clear(); // clear menu and set new content
			// set to next shadow type
			this.shadowType = ShadowType.SHADOW_MAP;
			this.gameMenu.add("Shadow type: Shadow map (experimental!)", "shadow map");
			this.gameMenu.add("Back to main menu", "back to main");
		}
		if(selectedEntry.equals("shadow map"))
		{
			this.gameMenu.clear(); // clear menu and set new content
			// set to next shadow type
			this.shadowType = ShadowType.NO_SHADOWS;
			this.gameMenu.add("Shadow type: No shadows", "no shadows");
			this.gameMenu.add("Back to main menu", "back to main");
		}
	}

	/* (non-Javadoc)
	 * @see de.rico.engine.game.AbstractBaseGame#draw(javax.media.opengl.GL)
	 */
	@Override
	public void draw(GL gl)
	{
		if(chapter != null) chapter.draw(gl);
		
		if(this.enemiesLeft >= 0)
			this.bossManager.draw(gl);

		// show the main menu
		if(StateManager.getInstance().equals(State.MENU))
		{
			this.gameMenu.draw(
					inputManager.getMouseX(),
					inputManager.getMouseY());

		}

		if(StateManager.getInstance().equals(State.NORMAL))
		{
			this.textRenderer.drawString(
					"Shots fired: "+this.shotsFired, 
					Camera.getInstance().getViewPort().getWidth()-150, 50);
			this.textRenderer.drawString(
					"Shots in magazine: "+this.shotsInMag, 
					Camera.getInstance().getViewPort().getWidth()-200, 75);
			this.textRenderer.drawString(
					"Accuracy: "+this.getAccuracy()+"%", 
					Camera.getInstance().getViewPort().getWidth()-200, 100);
			this.textRenderer.drawString(
					"Enemies left: "+this.enemiesLeft, 20, 50);
						
			if(this.timeToWin < 1000)
			{
				this.textRenderer.drawString(
						"Your time: "+this.timeToWin, 20, 75);
			}
			else
			{
				String time = ""+this.timeToWin;
				String sec = time.substring(0, time.length()-3);
				String msec = time.substring(time.length()-3, time.length()-1);
				this.textRenderer.drawString(
					"Your time: "+sec+"."+msec+" seconds", 20, 75);
			}
			
			this.textRenderer.drawString(
					"Energy left: "+this.playerEnergyLeft, 20, 100);
			
			
			if(this.shotsInMag <= 0)
			{
				this.textRenderer.drawString(
						"RELOAD!!!",
						Camera.getInstance().getViewPort().getWidth()-150, 145,
						new Color4f(1f,0f,0f,1f));
			}
			
			int midX = Camera.getInstance().getViewPort().getWidth()/2 - this.crosshair.getWidth()/2;
			int midY = Camera.getInstance().getViewPort().getHeight()/2 - this.crosshair.getHeight()/2;
			this.imageRenderer.drawImage(midX, midY, this.crosshair);
		}
		if(StateManager.getInstance().equals(State.CINEMATIC)
				|| StateManager.getInstance().equals(State.NO_INPUT_CINEMATIC))
		{
			// draw black bars, to emphasize the cinematic character of
			// the CINEMATIC mode
			// top bar
			this.blackBarsRenderer.drawRectangle(
					0, 0,
					Camera.getInstance().getViewPort().getWidth(),
					(int)(Camera.getInstance().getViewPort().getHeight()*0.1f));

			// bottom bar
			this.blackBarsRenderer.drawRectangle(
					0,
					Camera.getInstance().getViewPort().getHeight()
					-(int)(Camera.getInstance().getViewPort().getHeight()*0.1f),
					Camera.getInstance().getViewPort().getWidth(),
					(int)(Camera.getInstance().getViewPort().getHeight()*0.1f));
		}
		if(StateManager.getInstance().equals(State.SHOW_SCREEN))
		{
			// do nothing special
		}
		if(StateManager.getInstance().equals(State.MAP))
		{
			// do nothing special
		}
				
		if(StateManager.getInstance().equals(State.LEVEL_COMPLETE))
		{
			// draw black bars
			// top bar
			this.blackBarsRenderer.drawRectangle(
					0, 0,
					Camera.getInstance().getViewPort().getWidth(),
					(int)(Camera.getInstance().getViewPort().getHeight()*0.1f));

			// bottom bar
			this.blackBarsRenderer.drawRectangle(
					0,
					Camera.getInstance().getViewPort().getHeight()
					-(int)(Camera.getInstance().getViewPort().getHeight()*0.1f),
					Camera.getInstance().getViewPort().getWidth(),
					(int)(Camera.getInstance().getViewPort().getHeight()*0.1f));
			
			int midX = Camera.getInstance().getViewPort().getWidth()/2 - this.crosshair.getWidth()/2;
			int midY = Camera.getInstance().getViewPort().getHeight()/2 - this.crosshair.getHeight()/2;
			this.textRenderer.setFont(Font.TIMES_ROMAN_24);			
			
			String textWin = null;
			
			if(this.levelNr == this.numberOfLevels)
				textWin = "Congratulations - You have detroyed the last Borg cube";
			else textWin = "Victory - You have defeated this Borg cube!";			
			Color4f col = new Color4f(1f,1f,0f,1f);			
			int x = midX - this.textRenderer.getStringWidth(textWin)/2;
			this.textRenderer.drawString(textWin, x, midY, col);
			
			if(this.timeToWin < 1000)
			{
				this.textRenderer.drawString("Your time: "+this.timeToWin, x, midY+40);
			}
			else
			{
				String time = ""+this.timeToWin;
				String sec = time.substring(0, time.length()-3);
				String msec = time.substring(time.length()-3, time.length()-1);
				this.textRenderer.drawString("Your time: "+sec+"."+msec+" seconds", x, midY+40);
			}
			
			this.textRenderer.drawString("Energy left: "+this.playerEnergyLeft, x, midY+40+30);
			
			this.textRenderer.drawString("Shots fired: "+this.shotsFired, x, midY+40+30+30);
			
			this.textRenderer.drawString("Accuracy: "+this.getAccuracy()+"%", x, midY+40+30+30+30);
			
			if(this.levelNr < this.numberOfLevels)
			{
				col = new Color4f(0f,1f,0f,1f);
				this.textRenderer.drawString("Hit <spacebar> to advance to next level", x, midY+40+30+30+30+50, col);
			}
			
			this.textRenderer.setFont(Font.HELVETICA_18);	
		}
		
		if(StateManager.getInstance().equals(State.LEVEL_LOST))
		{
			// draw black bars
			// top bar
			this.blackBarsRenderer.drawRectangle(
					0, 0,
					Camera.getInstance().getViewPort().getWidth(),
					(int)(Camera.getInstance().getViewPort().getHeight()*0.1f));

			// bottom bar
			this.blackBarsRenderer.drawRectangle(
					0,
					Camera.getInstance().getViewPort().getHeight()
					-(int)(Camera.getInstance().getViewPort().getHeight()*0.1f),
					Camera.getInstance().getViewPort().getWidth(),
					(int)(Camera.getInstance().getViewPort().getHeight()*0.1f));
			
			
			int midX = Camera.getInstance().getViewPort().getWidth()/2 - this.crosshair.getWidth()/2;
			int midY = Camera.getInstance().getViewPort().getHeight()/2 - this.crosshair.getHeight()/2;
			
			this.textRenderer.setFont(Font.TIMES_ROMAN_24);		
			String textLose = "Game Over - The Borg have destroyed you!";						
			Color4f col = new Color4f(1f,1f,0f,1f);						
			int x = midX - this.textRenderer.getStringWidth(textLose)/2;
			this.textRenderer.drawString(textLose, x, midY, col);
			this.textRenderer.setFont(Font.HELVETICA_18);
		}
		


		// draw action scheduler stuff
		ActionScheduler.getInstance().draw(gl);

		// reset cursor to normal
		CursorManager.getInstance().setCursor(CustomCursor.NORMAL);
	}

	/**
	 * Sets the active chapter of the game. It gets loaded from
	 * from the specified file path.
	 * @param path path to the chapter file (.chp) to load the
	 * chapter from
	 */
	public void  setActiveChapter(String path)
	{
		StateManager.getInstance().setState(State.LOADING);
		chapter = new ChapterReader().readChapter(path, inputManager);

		this.stateBackup = State.LOADING;
		StateManager.getInstance().setState(State.NORMAL);


		// run garbage collection
		System.gc();

		this.resetElapsedTime(); // needs to be resetted
	}

	/**
	 * Returns the currently active chapter of the game.
	 * @return active chapter of the game
	 */
	public Chapter getActiveChapter()
	{
		return chapter;
	}


	/**
	 * Returns the current shot accuracy
	 * @return the current shot accuracy
	 */
	private float getAccuracy()
	{
		if(this.shotsFired == 0) return 0f;
		
		return Tools.round((((float)this.shotsHit*100f) / (float)this.shotsFired),2);
	}
	
	/**
	 * Helper method to check if a bomb has crashed onto the
	 * ground mesh
	 * @param bomb bomb to check for impact
	 * @return has bomb crashed? --> true=yes, false=no
	 * @deprecated doesn't work correctly
	 */
	private boolean impactOnGround(NonPlayerCharacter bomb)
	{
		Point3D start = new Point3D(
				bomb.getMesh().getPosX(),
				bomb.getMesh().getPosY(),
				bomb.getMesh().getPosZ()); // line start

		Point3D end = new Point3D(start.x, -1000f, start.z); // line end

		Line3D lineDown = new Line3D(start, end);
		
		Point3D intersect = CollisionFinder.getPointOfIntersection(
				chapter.getActiveRoom().getGround(), lineDown);
		
		if (intersect != null) return true;
		else return false;
	}
	
	/**
	 * The main method of the shoot out game.
	 * @param args argument string --> args[0] must be the path to the
	 * xml file that specifies the games start chapter.
	 */
	public static void main(String[] args)
	{
		// workaround for a bug in the Java2d pipeline
		System.setProperty("sun.java2d.noddraw", "true" );
		
		ShootOut3DMain.getInstance().initCamera(0f, 10f, 30f, 0f, 0f, 0.1f, 500f);
		//Adventure.getInstance().setDisplayFPS(true);
		ShootOut3DMain.getInstance().enableBackFaceCulling();
		ShootOut3DMain.getInstance().start(ShootOut3DMain.getInstance());
	}
}
