/**
 * package for the OpenGL event listener implementations
 */
package simogl.gamecore;

import java.awt.event.KeyEvent;
import java.util.Random;

import javax.sound.midi.Sequence;
import javax.swing.JFrame;

import javax.media.opengl.*;
import javax.media.opengl.glu.GLU;


import simogl.effect.sound.MidiPlayer;
import simogl.effect.sound.SampledSound;
import simogl.gameobjects.AbstractGameObject;
import simogl.gameobjects.building.AbstractBuilding;
import simogl.gameobjects.building.CityHall;
import simogl.gameobjects.building.Farm;
import simogl.gameobjects.building.House;
import simogl.geometry.Triangle;
import simogl.gui.BuildingSelectionPanel;
import simogl.gui.InfoPanel;
import simogl.input.InputAction;
import simogl.input.InputManager;
import simogl.level.AbstractLevel;
import simogl.level.DemoLevel;
import simogl.level.SkyBox;
import simogl.camera.Camera;
import simogl.collision.ObjectPicker;

/**
 * This class is responsible for rendering the scene
 * 
 * @author Frank Bruns
 * 
 */
public class Renderer implements GLEventListener
{
	private GLCanvas canvas = null;

	private JFrame frame = null;

	private float aspect = 1.0f;

	private Camera cam = null;

	// private InputHandler input = null;
	private InputManager inputManager = null;

	private SampledSound soundThunder = null;
	private SampledSound soundRain = null;
	private SampledSound soundSun = null;
	private SampledSound soundSelect = null;
	private SampledSound soundCollision = null;
	
	private MidiPlayer midiPlayer = null;
	private Sequence midiFile = null;

	// Input actions
	private InputAction keyA = null;
	private InputAction keyD = null;
	private InputAction keyW = null;
	private InputAction keyS = null;
	private InputAction keyE = null;
	private InputAction keyQ = null;
	private InputAction keyC = null;
	private InputAction keyL = null;
	private InputAction keyV = null;
	private InputAction keySpace = null;
	private InputAction keyESC = null;
	private InputAction mouseLeft = null;
	private InputAction mouseRight = null;
	private InputAction mouseUp = null;
	private InputAction mouseDown = null;
	private InputAction mouseBtnRight = null;
	private InputAction mouseBtnLeft = null;
	private InputAction mouseWheelUp = null;
	private InputAction mouseWheelDown = null;

	private Updater updater = null;

	private long fpsHelpTime = 0L;
	private int fpsCounter = 0;
	private int currentFps = 0;
	private long currentTime = 0L;
	private long elapsedTime = 0L;
	
	private long cumuTimeForInhabRecalc = 0L;
	private long RECALC_INTERVAL = 10000L;
	
	private long cumuTimeRain = 0L;
	private long rainInterval = 0L;	
	private long cumuTimeSun = 0L;
	private long sunInterval = 0L;
	
	private Random random = null;

	ObjectPicker buildingsPicker = null;
	
	
	// game resources
	DemoLevel demoLevel = null;

	/**
	 * Creates the render thread (GL event listener)
	 * 
	 * @param canvas
	 *            the canvas on which to render
	 * @param frame
	 *            the main frame of the application
	 */
	public Renderer(GLCanvas canvas, JFrame frame)
	{
		this.canvas = canvas;
		this.frame = frame;
		this.random = new Random();
	}

	/**
	 * JOGL init method
	 */
	public void init(GLAutoDrawable gld)
	{
		final GL gl = gld.getGL();
		
		gl.setSwapInterval(0);
		

		// activate depth buffer
		gl.glEnable(GL.GL_DEPTH_TEST);

		// create InputManager object
		this.inputManager = new InputManager(canvas);
		this.createInputActions();

		this.midiPlayer = new MidiPlayer();
		this.midiFile = midiPlayer
				.getSequence("./resources/sounds/midis/bgmusic.mid");

		// create camera object
		this.cam = new Camera(1000f, 30f, -980f, 1.5f, 45f, 0.01f, 3000f);

		this.initGameResource(gl);
		this.buildingsPicker = new ObjectPicker(AbstractLevel.buildings,this.cam,2f);
		
		this.rainInterval = 45000 + this.random.nextInt(30000);
		this.sunInterval = 15000 + this.random.nextInt(20000);
		
		this.currentTime = System.currentTimeMillis();

		// create updater object
		this.updater = new Updater();
		this.updater.start();
	}

	/**
	 * JOGL reshape method
	 */
	public void reshape(GLAutoDrawable gld, int x, int y, int width, int height)
	{
		GL gl = gld.getGL();

		// adjust viewport and projection matrix
		gl.glViewport(x, y, width, height);
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();

		// calculate window aspect on reshape
		this.aspect = (float) width / (float) height;

		// In den Modelview Matrix -Modus wechseln
		gl.glMatrixMode(GL.GL_MODELVIEW);
	}

	/**
	 * JOGL display method
	 */
	public void display(GLAutoDrawable gld)
	{
		GL gl = gld.getGL();
		GLU glu = new GLU();

		// time between two frames in miliseconds
		this.elapsedTime = System.currentTimeMillis() - this.currentTime;
		this.currentTime += this.elapsedTime;

		// Farb- und Tiefenpuffer leeren
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glClearColor(0.8f, 0.8f, 0.8f, 1f);

		// Transformationen an der Modelview Matrix vornehmen
		gl.glLoadIdentity();

		// handle camera actions
		this.handleCamera(gl, glu, this.aspect, this.elapsedTime);

		// process keys
		this.processNonCameraKeys(gl);

		// Draw the demo level
		this.demoLevel.draw(gl, this.elapsedTime);
		
		//recalculate inhabitants
		this.recalculateNumberInhabitans(elapsedTime);
		
		// consider the rain effect
		this.doRandomRain(elapsedTime);

		// show the fps in the window's title bar
		this.showFrameRate();
		
		try
		{
			Thread.sleep(0);
		}
		catch(InterruptedException e) 
		{
			System.out.println("Problem with \"sleep\" in Display thread");
			e.printStackTrace();
		}
	}

	public void displayChanged(GLAutoDrawable gld, boolean arg1, boolean arg2)
	{
		/*---  wird nicht benötigt ---*/
	}

	/**
	 * This method calculates the actual frames per second by counting the times
	 * of calling the display-method. The calculated value is then put out in
	 * the application window's title bar.
	 * 
	 */
	private void showFrameRate()
	{
		this.fpsHelpTime += this.elapsedTime;
		this.fpsCounter++;

		if (this.fpsHelpTime > 1000)
		{
			this.currentFps = this.fpsCounter;
			this.frame.setTitle("Frank Bruns' Projekt: SimoGL   FPS: "
					+ this.currentFps);

			// Reset Values
			this.fpsCounter = 0;
			this.fpsHelpTime = 0L;
		}
	}

	/**
	 * Helper method to create and initialze all the input actions of the
	 * application
	 * 
	 */
	private void createInputActions()
	{
		// Create input actions for camera movement
		this.keyA = new InputAction("Move cam left");
		this.keyD = new InputAction("Move cam right");
		this.keyW = new InputAction("Move cam forward");
		this.keyS = new InputAction("Move cam back");
		this.keyE = new InputAction("Move cam up");
		this.keyQ = new InputAction("Move cam down");
		this.mouseRight = new InputAction("Rotate cam left");
		this.mouseLeft = new InputAction("Rotate cam right");
		this.mouseUp = new InputAction("Rotate cam up");
		this.mouseDown = new InputAction("Rotate cam down");
		this.mouseBtnRight = new InputAction("Switch to mouse look");
		this.mouseWheelUp = new InputAction("Zoom out 2D");
		this.mouseWheelDown = new InputAction("Zoom in 2D");

		// assign camera input actions to keys and/or mouse
		this.inputManager.assignKey(this.keyA, KeyEvent.VK_A);
		this.inputManager.assignKey(this.keyD, KeyEvent.VK_D);
		this.inputManager.assignKey(this.keyW, KeyEvent.VK_W);
		this.inputManager.assignKey(this.keyS, KeyEvent.VK_S);
		this.inputManager.assignKey(this.keyE, KeyEvent.VK_E);
		this.inputManager.assignKey(this.keyQ, KeyEvent.VK_Q);
		this.inputManager.assignMouse(this.mouseRight,
				InputManager.MOUSE_MOVE_LEFT);
		this.inputManager.assignMouse(this.mouseLeft,
				InputManager.MOUSE_MOVE_RIGHT);
		this.inputManager.assignMouse(this.mouseUp, InputManager.MOUSE_MOVE_UP);
		this.inputManager.assignMouse(this.mouseDown,
				InputManager.MOUSE_MOVE_DOWN);
		this.inputManager.assignMouse(this.mouseBtnRight,
				InputManager.MOUSE_BUTTON_3);
		this.inputManager.assignMouse(this.mouseWheelUp,
				InputManager.MOUSE_WHEEL_UP);
		this.inputManager.assignMouse(this.mouseWheelDown,
				InputManager.MOUSE_WHEEL_DOWN);

		// create other input actions
		this.mouseBtnLeft = new InputAction("Select an object");
		this.keyC = new InputAction("Switch camera projection mode",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		this.keyV = new InputAction("Switch camera free look mode",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		this.keyL = new InputAction("Switch animation.",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		this.keySpace = new InputAction("Construct Building",
				InputAction.DETECT_INITAL_PRESS_ONLY);
		this.keyESC = new InputAction("Exit programme",
				InputAction.DETECT_INITAL_PRESS_ONLY);

		// assign input actions to keys and/or mouse
		this.inputManager.assignMouse(this.mouseBtnLeft,
				InputManager.MOUSE_BUTTON_1);
		this.inputManager.assignKey(this.keyC, KeyEvent.VK_C);
		this.inputManager.assignKey(this.keyL, KeyEvent.VK_L);
		this.inputManager.assignKey(this.keyV, KeyEvent.VK_V);
		this.inputManager.assignKey(this.keySpace, KeyEvent.VK_SPACE);
		this.inputManager.assignKey(this.keyESC, KeyEvent.VK_ESCAPE);
	}

	/**
	 * Handles the actions concerning the camera
	 * 
	 * @param gl
	 * @param glu
	 * @param aspect
	 *            window aspect (width/height)
	 * @param elapsedTime
	 *            time between two frames
	 */
	private void handleCamera(GL gl, GLU glu, float aspect, long elapsedTime)
	{
		if (this.keyW.isPressed()) this.cam.moveForward(0.1f, elapsedTime);
		if (this.keyS.isPressed()) this.cam.moveBackward(0.1f, elapsedTime);
		if (this.keyA.isPressed()) this.cam.moveLeft(0.1f, elapsedTime);
		if (this.keyD.isPressed()) this.cam.moveRight(0.1f, elapsedTime);
		if (this.keyE.isPressed()) this.cam.moveUp(0.1f, elapsedTime);
		if (this.keyQ.isPressed()) this.cam.moveDown(0.1f, elapsedTime);
		if (this.keyC.isPressed()) this.cam.switchProjectionMode();
		if (this.keyV.isPressed()) this.cam.toggleFreeLook();
		
		// use free mouse look, if mouse button 2 is pressed
		if (this.mouseBtnRight.isPressed())
		{
			if (this.mouseDown.isPressed()) this.cam.rotateDown(0.2f, elapsedTime);
			if (this.mouseUp.isPressed()) this.cam.rotateUp(0.2f, elapsedTime);
			if (this.mouseRight.isPressed()) this.cam.rotateLeft(0.2f, elapsedTime);
			if (this.mouseLeft.isPressed())	this.cam.rotateRight(0.2f, elapsedTime);
		}
		// check for zoom function
		if (this.mouseWheelUp.isPressed()) this.cam.zoomOut2D(1f);
		if (this.mouseWheelDown.isPressed()) this.cam.zoomIn2D(1f);

		this.cam.use(gl, glu, aspect);
	}

	/**
	 * This method is used to process any key that is not responsible for camera
	 * movement
	 * 
	 */
	private void processNonCameraKeys(GL gl)
	{
		if (this.keyESC.isPressed()) System.exit(0); // close program

		if (this.keyL.isPressed())
		{
			this.soundRain.stop();
		}

		// pick a building
		if (this.mouseBtnLeft.isPressed())
		{
			// set boundig box of formerly selected building to invisible
			for(AbstractGameObject b : AbstractLevel.buildings)
			{
				AbstractBuilding building = (AbstractBuilding)b;
				building.setVisibleBox(false);
			}
			
			
			AbstractBuilding go = (AbstractBuilding)this.buildingsPicker.getObjectInFront(gl, this.inputManager.getMouseX(),
					this.inputManager.getMouseY());
			if(go != null) // if a building got picked
			{
				// show bounding box to visualize the selection
				go.setVisibleBox(true);
				
				this.soundSelect.play(false);
				InfoPanel.setLabelBuildingName(go.getName());
				InfoPanel.setLabelInhabitants(Integer.toString(go.getNumberInhabitants()));
				InfoPanel.setLabelTaxes(Integer.toString(go.getTaxes()));
				InfoPanel.setLabelRunningCost(Integer.toString(go.getRegularCosts()));
			}
			else
			{
				// set boundig box of formerly selected building to invisible
				for(AbstractGameObject b : AbstractLevel.buildings)
				{
					AbstractBuilding building = (AbstractBuilding)b;
					building.setVisibleBox(false);
				}
				
				// clear the panel from values
				InfoPanel.setLabelBuildingName("n/a");
				InfoPanel.setLabelInhabitants("n/a");
				InfoPanel.setLabelTaxes("n/a");
				InfoPanel.setLabelRunningCost("n/a");
			}
			
			this.mouseBtnLeft.reset();
			
		}		

		if (this.keySpace.isPressed())
		{
			switch (BuildingSelectionPanel.getSelectedButtonID())
			{
			case AbstractBuilding.HOUSE:
			{
				this.constructBuilding(AbstractBuilding.HOUSE);
				break;
			}
			case AbstractBuilding.FARM:
			{
				this.constructBuilding(AbstractBuilding.FARM);
				break;
			}
			case AbstractBuilding.CITY_HALL:
			{
				this.constructBuilding(AbstractBuilding.CITY_HALL);
				break;
			}
			}
		}
	}

	/**
	 * Helper method to create a game object
	 * 
	 * @param constant
	 *            the constant for the certain building, e.g.
	 *            AbstractBuilding.HOUSE or AbstractBuilding.FARM
	 */
	private void constructBuilding(int constant)
	{
		boolean isPlain = true;
		// check triangle for being plane
		for (Triangle trig : cam.getCollisionTrigs())
		{
			float y1 = 0f;
			float y2 = 0f;
			float y3 = 0f;

			y1 = trig.getVertex1().getY();
			y2 = trig.getVertex2().getY();
			y3 = trig.getVertex3().getY();

			if (y1 != y2 || y1 != y3 || y2 != y3)
			{
				isPlain = false;
				this.soundCollision.play(false);
				break;
			}
		}

		// only build building, if the ground level is plain
		if (isPlain == true)
		{
			switch (constant)
			{
			case AbstractBuilding.HOUSE:
			{
				House house = new House(cam.getPosition().getX(), cam
						.getPosition().getY()
						- cam.getHeightOverGround(), cam.getPosition().getZ());
				house.addToScene();				
				break;
			}
			case AbstractBuilding.FARM:
			{
				Farm farm = new Farm(cam.getPosition().getX(), cam
						.getPosition().getY()
						- cam.getHeightOverGround(), cam.getPosition().getZ());
				farm.addToScene();
				break;
			}
			case AbstractBuilding.CITY_HALL:
			{
				CityHall cityHall = new CityHall(cam.getPosition().getX(), cam
						.getPosition().getY()
						- cam.getHeightOverGround(), cam.getPosition().getZ());
				cityHall.addToScene();
				break;
			}
			}
		}
	}
	
	/**
	 * This method recalculates the number of inhabitants
	 * by adding inhabitants to building on a random basis and by 
	 * subtracting inhabitants on a random basis, if the people are
	 * starving to death.
	 * @param elapsedTime time between two frames
	 */
	private void recalculateNumberInhabitans(long elapsedTime)
	{		
		// cumulate time to see, if the time intervall is exceeded
		this.cumuTimeForInhabRecalc += elapsedTime;
		
		if(this.cumuTimeForInhabRecalc >= RECALC_INTERVAL)
		{
			// reset cumulated time
			this.cumuTimeForInhabRecalc = 0L;
						
			for(AbstractGameObject b : AbstractLevel.buildings)
			{
				AbstractBuilding building = (AbstractBuilding)b;
				if(building.getType() == AbstractBuilding.HOUSE)
				{					
					// add an inhabitant to a house on a random basis and if
					// the food supply is greater than 0
					if(AbstractLevel.food.getCurrentAmount() > 0
							&& Math.random() < 0.45d)
					{
						building.increaseNumberInhabitants(1);					
					}
					
					// subtract inhabitants randomly, but with a little
					// probability to happen
					if(Math.random() < 0.15d) building.decreaseNumberInhabitants(1);
					
					// subtract inhabitants due to little food supply and
					// applying a random factor with a good probability to occur
					if(AbstractLevel.food.getCurrentAmount() <= 0 && Math.random() < 0.5d)
					{
						building.decreaseNumberInhabitants(1);
					}
				}
			}			
		}		
	}
	
	/**
	 * This method enables or disables the rain effect of the level
	 * on a random basis.
	 * @param elapsedTime time between two frames
	 */
	private void doRandomRain(long elapsedTime)
	{
		this.cumuTimeRain += elapsedTime;
		this.cumuTimeSun += elapsedTime;
		
		// if rain is not enabled, perhaps enable rain,
		// else reset the cumulated rain timer to 0
		if(AbstractLevel.isEnabledRain() == false)
		{
			this.cumuTimeSun = 0L;
			if(this.cumuTimeRain >= this.rainInterval)
			{
				this.cumuTimeRain = 0L;
				
				// change rain intervall for next time
				this.rainInterval = 45000 + this.random.nextInt(30000);
				
				SkyBox.setMaterialAmbDiff(0.75f,0.75f,0.75f,1f);
				AbstractLevel.lights.get(0).setColorAmbient(0.7f,0.7f,0.7f,1f);
				AbstractLevel.setEnabledRain(true);
				this.soundSun.stop();
				this.soundThunder.play(false);
				this.soundRain.play(true);
			}
		}
		else
		{
			this.cumuTimeRain = 0L;			
			if(this.cumuTimeSun >= this.sunInterval)
			{
				this.cumuTimeSun = 0L;
				
				// change sun intervall for next time
				this.sunInterval = 15000 + this.random.nextInt(20000);
				
				SkyBox.setMaterialAmbDiff(1f,1f,1f,1f);
				AbstractLevel.lights.get(0).setColorAmbient(1f,1f,1f,1f);				
				AbstractLevel.setEnabledRain(false);
				this.soundRain.stop();
				this.soundSun.play(false);				
			}			
		}
		
	}



	/**
	 * Helper method initialize the game resources.
	 * 
	 * @param gl
	 */
	private void initGameResource(GL gl)
	{
		this.demoLevel = new DemoLevel(gl, 250, 120);
		this.midiPlayer.play(this.midiFile, true);
		
		this.soundSelect = new SampledSound("./resources/sounds/samples/select.wav");
		this.soundCollision = new SampledSound("./resources/sounds/samples/collision.wav");
		this.soundThunder = new SampledSound("./resources/sounds/samples/thunder.wav");
		this.soundRain = new SampledSound("./resources/sounds/samples/rain.wav");
		this.soundSun = new SampledSound("./resources/sounds/samples/sun.wav");
	}
}
