/**
 * package for classes that are components of the world (meshes)
 */
package simogl.geometry;

import java.util.ArrayList;
import javax.media.opengl.GL;

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

import simogl.collision.BoundingBox;
import simogl.effect.light.Light;
import simogl.material.Material;


public abstract class AbstractMesh
{
	protected ArrayList<Face> faces = null;
		
	protected float posX = 0f;
	protected float posY = 0f;
	protected float posZ = 0f;

	protected float rotX = 0f;
	protected float rotY = 0f;
	protected float rotZ = 0f;
	
	protected float scaleX = 1f;
	protected float scaleY = 1f;
	protected float scaleZ = 1f;
	
	protected float lightPosX = 0f;
	protected float lightPosY = 0f;
	protected float lightPosZ = 0f;
	
	protected boolean enableFaceCulling = true;
	protected boolean showWireFrame = false;
	protected boolean showBoundingBox = false;
	protected boolean enableTransparency = false;
	protected boolean enableShadow = false;
	
	protected Material material = null;
	protected Texture texture = null;
	protected BoundingBox boundingBox = null; 	
	
	/**
	 * Creates a new mesh object
	 */
	public AbstractMesh()
	{
		this.faces = new ArrayList<Face>();		
	}
	
	/**
	 * Adds a face (two triangle) to the mesh.
	 * @param face face that shall be used as a face of the mesh
	 */
	public void addFace(Face face)
	{		
		this.faces.add(face);
	}
	
	/**
	 * Returns the faces of the mesh
	 * @return faces of the mesh
	 */
	public synchronized ArrayList<Face> getFaces()
	{
		return this.faces;
	}
	
	/**
	 * Returns the bounding box of the mesh
	 * @return bounding box of the mesh
	 */	
	public synchronized BoundingBox getBoundingBox()
	{
		return this.boundingBox;
	}
	
	/**
	 * Determines wheter the bounding box shall be visually shown by
	 * a box that is drawn around the mesh
	 * @param show shoe bounding box? --> yes = true, no = false
	 */
	public void showBoundingBox(boolean show)
	{
		this.showBoundingBox = show;
	}
	
	/**
	 * Sets the material of the mesh to the specified material
	 * @param material material for the mesh
	 */
	public void setMaterial(Material material)
	{
		this.material = material;
	}
	
	/**
	 * Returns the current material of the mesh
	 * @return material of the mesh
	 */
	public Material getMaterial()
	{
		return this.material;
	}

	/**
	 * Sets the position of the mesh with a Point3D object
	 *	@param position a Point3D object specifying a 3D position
	 */
	public void setPosition(Point3D position)
	{
		this.posX = position.getX();
		this.posY = position.getY();
		this.posZ = position.getZ();
	}
	
	/**
	 * Sets the position of the mesh by the three given parameters
	 * @param x x-coordinate
	 * @param y y-coordinate
	 * @param z z-coordinate
	 */
	public void setPosition(float x, float y, float z)
	{
		this.posX = x;
		this.posY = y;
		this.posZ = z;
	}
	
	/**
	 * Sets the x-coordinate of this mesh's position
	 * @param x x-coordinate
	 */
	public void setPositionX(float x)
	{
		this.posX = x;
	}
	
	/**
	 * Sets the y-coordinate of this mesh's position
	 * @param y y-coordinate
	 */
	public void setPositionY(float y)
	{
		this.posY = y;
	}
	
	/**
	 * Sets the z-coordinate of this mesh's position
	 * @param z z-coordinate
	 */
	public void setPositionZ(float z)
	{
		this.posZ = z;
	}
	
	/**
	 * Returns the current Position of the mesh
	 * @return current position
	 */
	public Point3D getPosition()
	{
		return new Point3D(this.posX, this.posY, this.posZ);
	}
	
	/**
	 * Returns the x-coordinate of the mesh's position
	 * @return x-coordinate
	 */
	public float getPositionX()
	{
		return this.posX;
	}
	
	/**
	 * Returns the y-coordinate of the mesh's position
	 * @return y-coordinate
	 */
	public float getPositionY()
	{
		return this.posY;
	}
	
	/**
	 * Returns the z-coordinate of the mesh's position
	 * @return z-coordinate
	 */
	public float getPositionZ()
	{
		return this.posZ;
	}
	
	/**
	 * Specifies the texture that shall be used for this mesh
	 * @param texture The texture that shall be used for this mesh
	 */
	public void setTexture(Texture texture)
	{
		this.texture = texture;
	}
	
	/**
	 * Returns the current texture of this mesh
	 * @return current texture
	 */
	public Texture getTexture()
	{
		return this.texture;
	}
	
	/**
	 * Rotates the mesh around its x-,y- and z-axxis.
	 * @param degX degrees of rotation of x-axxis
	 * @param degY degrees of rotation of y-axxis
	 * @param degZ degrees of rotation of z-axxis
	 */
	public void rotate(float degX, float degY, float degZ)
	{
		this.rotateAxxisX(degX);
		this.rotateAxxisY(degY);
		this.rotateAxxisZ(degZ);
	}
	
	/**
	 * Rotates the mesh around its x-axxis
	 * @param degrees Specifies the amount of degrees the mesh shall be rotated
	 */
	public void rotateAxxisX(float degrees)
	{
		this.rotX = degrees%360f;
	}
	
	/**
	 * Rotates the mesh around its y-axxis
	 * @param degrees Specifies the amount of degrees the mesh shall be rotated
	 */
	public void rotateAxxisY(float degrees)
	{
		this.rotY = degrees%360f;
	}
	
	/**
	 * Rotates the mesh around its z-axxis
	 * @param degrees Specifies the amount of degrees the mesh shall be rotated
	 */
	public void rotateAxxisZ(float degrees)
	{
		this.rotZ = degrees%360f;
	}
	
	/**
	 * Returns the degrees of roation of this mesh as an instance of Point3D.
	 * The instance's method getX() returns the degree of rotation around the
	 * x-axxis, getY() return the roations around the y-axxis and thus getZ()
	 * returns the rotation around the z-axxis. 
	 * @return Point3D instance containig the rotational degrees of the three axxises
	 */
	public Point3D getRotation()
	{
		return new Point3D(this.rotX, this.rotY, this.rotZ);
	}
	
	/**
	 * Returns the degree of rotation around the x-axxis of the mesh
	 * @return rotation around x-axxis
	 */
	public float getRoationX()
	{
		return this.rotX;
	}
	
	/**
	 * Returns the degree of rotation around the y-axxis of the mesh
	 * @return rotation around y-axxis
	 */
	public float getRoationY()
	{
		return this.rotY;
	}
	
	/**
	 * Returns the degree of rotation around the z-axxis of the mesh
	 * @return rotation around z-axxis
	 */
	public float getRoationZ()
	{
		return this.rotZ;
	}
	
	/**
	 * Scales the mesh to the specified x-, y-, z-values along its axxises
	 * <br>
	 * <b>Note: </b>Do not provide a paramter which is equal to 0!
	 * @param x scale factor along the x-axxis
	 * @param y scale factor along the y-axxis
	 * @param z scale factor along the z-axxis
	 */
	public void scale(float x, float y, float z)
	{
		// Prevent a parameter from being 0
		if(x == 0) this.scaleX = 1;
		else this.scaleX = x;
		
		if(y == 0) this.scaleY = 1;
		else this.scaleY = y;
		
		if(z == 0) this.scaleZ = 1;
		else this.scaleZ = z;
	}
	
	/**
	 * Specifies if face culling shall applied to the mesh.
	 * @param enable enable face culling? --> true = yes, false = no
	 */
	public void setEnabledFaceCulling(boolean enable)
	{
		this.enableFaceCulling = enable;
	}
	
	/**
	 * Sets whether the mesh shall be transparent or not transparent.
	 * <b>Attention: </b>In order to archieve a correct blending result,
	 * the material's alpha value has to be lesser that 1.0f
	 * @param transparency transparent? --> true = yes, false = no
	 */
	public void setTransparent(boolean transparency)
	{
		this.enableTransparency = transparency;
	}
	
	/**
	 * Determines whether the mesh is transparent or solid
	 * @return is transparent? --> true = yes, false = no
	 */
	public boolean isTransparent()
	{
		return this.enableTransparency;
	}
	
	/**
	 * Sets whether the shadow of the mesh ought to be drawn.
	 * You have to pass over the GL.GL_LIGHT0 object of the scene, in order
	 * to be able to draw the shadow according to the lights position.
	 * @param useShadow draw shadow? --> true = yes, false = no
	 * @param light0 the GL.LIGHT_0 object of the scene
	 */
	public void setDrawShadow(boolean useShadow, Light light0)
	{
		this.enableShadow = useShadow;
		this.lightPosX = light0.getPosition().getX();
		this.lightPosY = light0.getPosition().getY();
		this.lightPosZ = light0.getPosition().getZ();
	}
	
	/**
	 * Determines whether the mesh shall be rendered as a wireframe model
	 * @param wireFrameSwitch true = wireframe on, false = wireframe off
	 */
	public void showWireFrame(boolean wireFrameSwitch)
	{
		this.showWireFrame = wireFrameSwitch;
	}
	
	/**
	 * This method actives the render options for a mesh. By calling this
	 * method in the mesh's draw()-method before the actuall OpenGL rendering
	 * operation, things like material properties, transparency or the texture
	 * would be applied to the mesh, if activated.
	 * <br>
	 * <b>Note: </b>You neccessarily have to call the disableRenderOptions()-
	 * method after the OpenGL render operation took place for the mesh.
	 * Otherwise weird behaviour could be the consequence.
	 * @param gl
	 */
	public void enableRenderOptions(GL gl)
	{
		// Consider face culling
		/*
		if(this.enableFaceCulling == false)
		{
			gl.glDisable(GL.GL_CULL_FACE);
		}
		else gl.glEnable(GL.GL_CULL_FACE);
		*/
		
		
		// Show mesh as wireframe model ?
		if(this.showWireFrame == true)
		{
			gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		}			
		
		// assign material
		if(this.material != null)
		{
			gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT_AND_DIFFUSE, this.material.getMaterialColorAmbDiff(),0);
			gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, this.material.getMaterialColorSpecular(),0);
			gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_EMISSION, this.material.getMaterialColorEmission(),0);
		}
		else
		{
			float[] ambient = {0.0f,0.0f,0.0f,1f};
			gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT_AND_DIFFUSE, ambient,0);
			float[] specular = {0.0f,0.0f,0.0f,1f};
			gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, specular,0);
			float[] emission = {0.0f,0.0f,0.0f,1f};
			gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_EMISSION, emission,0);
		}
		
		// consider transparency of the mesh, must be called after
		// the material and texture assignment, because blending
		// doesn't use the correct material or texture values otherwise
		if(this.enableTransparency == true)
		{				
			gl.glEnable(GL.GL_BLEND);
			gl.glDepthMask(false);
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
		}

		// apply texture		
		if(this.texture != null)
		{				
			this.texture.enable();			
			this.texture.bind();
		}		
		else gl.glDisable(GL.GL_TEXTURE_2D);
		
		// draw bounding box, if enabled
		if(this.showBoundingBox == true)
		{				
			gl.glPushMatrix();
				this.performTransformations(gl);				
				this.boundingBox.draw(gl);
			gl.glPopMatrix();
		}
	}
	
	/**
	 * This method must be called after the OpenGL rendering operation took
	 * place. It disables the activated rendering options.
	 * @param gl
	 */
	public void disableRenderOptions(GL gl)
	{
		gl.glDepthMask(true);
		gl.glDisable(GL.GL_BLEND);		
		//gl.glDisable(GL.GL_CULL_FACE);
		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
	}
	
	/**
	 * This method performs all translations, rotations and scalings
	 * for the mesh. It should be usually called within a glPush/glPopMatrix()
	 * block. 
	 * @param gl
	 */
	public void performTransformations(GL gl)
	{
		gl.glTranslatef(this.posX, this.posY, this.posZ);
		gl.glRotatef(this.rotX, 1, 0, 0);
		gl.glRotatef(this.rotY, 0, 1, 0);
		gl.glRotatef(this.rotZ, 0, 0, 1);
		gl.glScalef(this.scaleX, this.scaleY, this.scaleZ);	
	}
	
	/**
	 * Call this method to draw the mesh
	 * @param gl
	 */
	public abstract void draw(GL gl);
	
	/**
	 * This method contains only the drawing operation fitting to the
	 * special kind of mesh. It is useful for drawing shadows, too.
	 * @param gl
	 */
	public abstract void specializedDraw(GL gl);
	
	/**
	 * Call this method to draw the shadow of a mesh
	 * @param gl
	 */
	public void drawShadow(GL gl)
	{
		if(this.enableShadow == true)
		{			
			gl.glEnable(GL.GL_STENCIL_TEST);
			gl.glClear(GL.GL_STENCIL_BUFFER_BIT);
			gl.glClearStencil(0);
			// increase Stencil, if pixel gets written
			gl.glStencilOp(GL.GL_KEEP,GL.GL_KEEP,GL.GL_INCR);
			// only draw, if referential value is 0
			gl.glStencilFunc(GL.GL_EQUAL, 0, 1);
			
			// compute tangens of Alpha and Beta
			float tanA = (this.posX-this.lightPosX)/(this.lightPosY-this.posY);
			float tanB = (this.posZ-this.lightPosZ)/(this.lightPosY-this.posY);		
			
			// Compute shadow matrix and multiply modelview
			float a = this.posY+0.05f;
			float [] sm = {
			1, 0, 0, 0,
			tanA, 0, tanB, 0,
			0, 0, 1, 0,
			-a*tanA, a, -tanB*a, 1, };
			
			
			gl.glEnable(GL.GL_BLEND);
			gl.glDepthMask(false);
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
			
			
			gl.glPushMatrix();
				gl.glMultMatrixf(sm,0);
				gl.glDisable(GL.GL_LIGHTING);
				gl.glDisable(GL.GL_TEXTURE_2D);
		
				gl.glColor4f(0f,0f,0f,0.4f);
				this.specializedDraw(gl);
				gl.glColor3f(1f,1f,1f);
				
				gl.glEnable(GL.GL_TEXTURE_2D);
				gl.glEnable(GL.GL_LIGHTING);
			gl.glPopMatrix();			
			
			gl.glDisable(GL.GL_STENCIL_TEST);
			
			gl.glDepthMask(true);
			gl.glDisable(GL.GL_BLEND);	
		}
	}
	
	/**
	 * Deletes the mesh
	 * @param gl
	 */
	public abstract void deleteMesh(GL gl);
}
