/**
 * package for classes that are part of a world
 */
package simogl.level;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import javax.media.opengl.GL;

import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;

import simogl.effect.fog.Fog;
import simogl.effect.light.Light;
import simogl.effect.particle.AbstractParticleEffect;
import simogl.effect.particle.custom.Rain;
import simogl.factory.MeshFactory;
import simogl.gameobjects.AbstractGameObject;
import simogl.geometry.AbstractMesh;
import simogl.geometry.MeshDL;
import simogl.gui.ResourcePanel;
import simogl.material.Material;
import simogl.resource.Food;
import simogl.resource.Gold;



/**
 * This class represents a world (level)
 * @author Frank Bruns
 *
 */
public abstract class AbstractLevel
{	
	public static ArrayList<AbstractGameObject> characters = null;
	public static ArrayList<AbstractGameObject> buildings = null;
	public static ArrayList<AbstractMesh> worldItemsSolid = null;
	public static ArrayList<AbstractMesh> worldItemsTransparent = null;
	public static ArrayList<AbstractParticleEffect> partEffects = null;
	public static ArrayList<Rain> rainSources = null;
	public static ArrayList<Light> lights = null;
	public static Gold gold = null;
	public static Food food = null;
	
	public Fog fog = null;	
	public SkyBox skyBox = null;
	public static Terrain terrain = null;
	public static MeshDL waterLayer = null;
	
	private static boolean rainEnabled = false;
	
	/**
	 * Creates a new level with the specified initial values for
	 * gold and food for the player
	 * @param gl
	 * @param amountGold initial amount of gold for the player
	 * @param amountFood initial amount of food for the player
	 */
	public AbstractLevel(GL gl, int amountGold, int amountFood)
	{
		characters = new ArrayList<AbstractGameObject>();
		buildings = new ArrayList<AbstractGameObject>();
		worldItemsSolid = new ArrayList<AbstractMesh>();
		worldItemsTransparent = new ArrayList<AbstractMesh>();
		partEffects = new ArrayList<AbstractParticleEffect>();
		rainSources = new ArrayList<Rain>();
		lights = new ArrayList<Light>();	
		gold = new Gold(amountGold,10000);
		food = new Food(amountFood,8000);
		
		// Init the resource panel with the resource values
		ResourcePanel.setLabelGold(Integer.toString(amountGold));
		ResourcePanel.setLabelFood(Integer.toString(amountFood));
		
		this.createTextures();
		this.createWorldContent(gl);
	}
	
	/**
	 * This is the central method that should be used to create all world
	 * content. This is the place where the world objects that have been defined
	 * as global class variables can be instaciated. Some world items that usually
	 * appear in every scene can be created by convinience methods, such as
	 * createSkybox(), createTerrain(), createWaterLayer()
	 * @param gl
	 */
	public abstract void createWorldContent(GL gl);
	
	/**
	 * This method must be overridden to initialize the textures for the 
	 * concrete world. The texture objects have to be global class variables,
	 * that can be similarily instanciated like this in your overridden
	 * createTextures()-method :
	 * <br>
	 * <code>
	 * this.texWall = TextureIO.newTexture(rsLoader.getFile("./textures/wall.png"), true);
	 * <br>				
	 * this.texSand = TextureIO.newTexture(rsLoader.getFile("./textures/sand.png"), true);
	 * </code> 
	 * <br>You will have to put a try-catch-block around the code in order to
	 * handle IOExceptions
	 *
	 */
	public abstract void createTextures();
	
	/**
	 * This method must be called to create the world's skybox
	 * @param gl
	 * @param north filepath+filename for northern texture
	 * @param south filepath+filename for southern texture
	 * @param east filepath+filename for eastern texture
	 * @param west filepath+filename for western texture
	 * @param top filepath+filename for top texture
	 * @param bottom filepath+filename for bottom texture
	 */
	public void createSkybox(GL gl, String north, String south, String east, String west, String top, String bottom)
	{
		try
		{
			this.skyBox = new SkyBox(gl,5f,5f,5f);
			this.skyBox.setTextureNorth(TextureIO.newTexture(new File(north), true));	
			this.skyBox.setTextureSouth(TextureIO.newTexture(new File(south), true));			
			this.skyBox.setTextureEast(TextureIO.newTexture(new File(east), true));
			this.skyBox.setTextureWest(TextureIO.newTexture(new File(west), true));
			this.skyBox.setTextureTop(TextureIO.newTexture(new File(top), true));
			this.skyBox.setTextureBottom(TextureIO.newTexture(new File(bottom), true));
			this.skyBox.createDisplayList(gl);
			this.skyBox.setPositionY(-2.5f);
		}
		catch(IOException ioexc)
		{
			System.out.println("Error while loading skybox textures");
		}
	}
	
	/**
	 * Call this method to create the world's terrain from a heightmap file.
	 * @param gl
	 * @param mapPath path+filename to the height map file
	 * @param size pixel width/height of the height map file
	 * @param resolution resolution factor for parsing the height map file
	 * @param texture the texture that ought to be wrapped over the terrain
	 */
	public void createTerrain(GL gl,String mapPath, int size, int resolution, Texture texture)
	{
		terrain = new Terrain(gl,new File(mapPath),size,resolution);
		terrain.setPosition(0f,0f,0f);
		terrain.setMaterial(new Material(1f, 1f, 1f, 0.5f));
		if(texture != null) terrain.setTexture(texture);
	}
	
	/**
	 * Call this method, if you would like to add a rain effect to your level.
	 * The distance parameter is used to specifiy the distance between
	 * different rain sources. The higher the distance, the less the number
	 * of rain sources that get spread over the map, hence a better performance
	 * @param mapSize size of the level's height map
	 * @param distance distance between the rain sources
	 */ 
	public void createRain(int mapSize, int distance)
	{
		// the index i will be the offset for lines and rows
		for(int i=0; i<mapSize/distance; i++)
		{
			// place rain sources along x-axis
			for(int j=0; j<mapSize/distance; j++)
			{
				float x = j*distance + 100;
				float z = -i*distance - 100;
				rainSources.add(new Rain(x,500f,z,5,2500,500));
			}
			
			// place rain sources along negative z-axis
			for(int j=0; j<mapSize/distance; j++)
			{
				float z = -j*distance - 100;
				float x = i*distance + 100;
				rainSources.add(new Rain(x,500f,z,5,2500,500));
			}
		}
	}
	
	/**
	 * This method must be called to create the water layer of the world.
	 * @param gl
	 * @param size width/length of the water layer. Should be the same size as
	 * the terrain's height map pixel width/height
	 * @param yPos defines the sea level, i.e. the height level of the water
	 * surface
	 * @param texture a texture that ought to be wrapped onto the water surface
	 */
	public void createWaterLayer(GL gl, int size, float yPos, Texture texture)
	{
		waterLayer = MeshFactory.createSquare(gl,size,size);
		waterLayer.setMaterial(new Material(1f,1f,1f,0.5f));
		waterLayer.setPosition(0,yPos,-size);
		waterLayer.rotateAxxisX(+90f);
		if(texture != null) waterLayer.setTexture(texture);
		waterLayer.setTransparent(true);
		worldItemsTransparent.add(waterLayer);
	}	
	
	/**
	 * Static method to enable or disable the rain effect of the level
	 * @param enableRain enable rain? --> true=yes, false=no
	 */
	public static void setEnabledRain(boolean enableRain)
	{
		rainEnabled = enableRain; 	
	}
	
	/**
	 * Static method that determines whether the rain effect is enabled
	 * or disabled
	 * @return is rain enabled? --> true=yes, false=no
	 */
	public static boolean isEnabledRain()
	{
		return rainEnabled;
	}
	
	/**
	 * Draws the world to the screen
	 * @param gl
	 * @param elapsedTime time between two frames
	 */
	public void draw(GL gl, long elapsedTime)
	{	
		// draw skybox
		if(this.skyBox != null )this.skyBox.draw(gl);
		
		// draw fog
		if(this.fog != null) this.fog.draw(gl);
		
		// draw lights		
		for(Light light : lights)
		{
			light.draw(gl);
		}							
		
		// draw height map
		if(terrain != null)
		{
			terrain.draw(gl);
		}
		
		synchronized(this)
		{
			// draw buildings
			for(AbstractGameObject building : buildings)
			{
				building.getModel().getCurrentAnim().play(gl);
				building.getModel().getCurrentAnim().getCurrentFrame().getMesh().setDrawShadow(true, lights.get(0));
				building.getModel().getCurrentAnim().getCurrentFrame().getMesh().drawShadow(gl);
			}
			
			// draw characters
			for(AbstractGameObject character : characters)
			{
				character.getModel().getCurrentAnim().play(gl);
				character.getModel().getCurrentAnim().getCurrentFrame().getMesh().setDrawShadow(true, lights.get(0));
				character.getModel().getCurrentAnim().getCurrentFrame().getMesh().drawShadow(gl);
			}
			
			// Draw solid meshes first
			for(AbstractMesh item : worldItemsSolid)
			{
				item.draw(gl);
				item.setDrawShadow(true, lights.get(0));
				item.drawShadow(gl);
			}	
			
			// Draw transparent meshes afterwards
			for(AbstractMesh item : worldItemsTransparent)
			{
				item.draw(gl);
			}
			
			// draw different types of particle effects
			for(AbstractParticleEffect effect : partEffects)
			{
				effect.draw(gl, elapsedTime);
			}	
			
			
			// draw rain, if it is enabled
			if(rainEnabled == true)
			{
				for(Rain rainSource : rainSources)
				{
					rainSource.setEnabled(true);
					rainSource.draw(gl, elapsedTime);
				}
			}
			else
			{
				for(Rain rainSource : rainSources)
				{
					rainSource.setEnabled(false);
					rainSource.draw(gl, elapsedTime);
				}
			}
		}		
	}	
}
