package de.rico.adventure.entity.hotspot;

import javax.media.opengl.GL;

import de.rico.engine.camera.Camera;
import de.rico.engine.camera.culling.FrustumPlane;
import de.rico.engine.camera.culling.ViewingFrustum;
import de.rico.engine.geometry.Line3D;
import de.rico.engine.geometry.Point3D;
import de.rico.engine.geometry.collision.Ray;
import de.rico.engine.util.Tools;

/**
 * This class can be used as a cubical interactive hotspot.
 *
 * @author Frank Bruns
 */
public class CubicalHotpot extends AbstractInteractiveHotspot
{
	private float width = 0f;

	private float height = 0f;

	private float length = 0f;

	/**
	 * Creates a new cubical hot spot with the specified dimensions at the
	 * specified position. You can specify, if the hotspot needs an
	 * item to be usable.
	 * @param pos position of the hotspot
	 * @param width width of the hotspot
	 * @param height height of the hotspot
	 * @param length length of the hotspot
	 * @param needsItem needs item to use? --> true=yes, false=no
	 */
	public CubicalHotpot(Point3D pos, float width, float height, float length, boolean needsItem)
	{
		super(pos,needsItem);
		this.width = width;
		this.height = height;
		this.length = length;
	}

	/**
	 * Returns the height of the hotspot
	 *
	 * @return the height of the hotspot
	 */
	public float getHeight()
	{
		return this.height;
	}

	/**
	 * Sets the height of the hotspot.
	 *
	 * @param height the height to set
	 */
	public void setHeight(float height)
	{
		this.height = height;
	}

	/**
	 * Returns the length of the hotspot.
	 *
	 * @return the length of the hotspot
	 */
	public float getLength()
	{
		return this.length;
	}

	/**
	 * Sets the length of the hotspot
	 *
	 * @param length the length to set
	 */
	public void setLength(float length)
	{
		this.length = length;
	}

	/**
	 * Returns the width of the hotspot.
	 *
	 * @return the width of the hotspot
	 */
	public float getWidth()
	{
		return this.width;
	}

	/**
	 * Sets the width of the hotspot.
	 *
	 * @param width the width to set
	 */
	public void setWidth(float width)
	{
		this.width = width;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see de.rico.adventure.entity.hotspot.AbstractInteractiveHotspot#draw(javax.media.opengl.GL)
	 */
	@Override
	public void draw(GL gl)
	{
		// if the hotspot isn't enabled do nothing
		if(this.isEnabled() == false) return;

		gl.glPushAttrib(GL.GL_LIGHTING_BIT);
		{
			float leftX = this.getPosition().x - this.width / 2f;
			float rightX = this.getPosition().x + this.width / 2f;
			float topY = this.getPosition().y + this.height / 2f;
			float bottomY = this.getPosition().y - this.height / 2f;
			float frontZ = this.getPosition().z + this.length / 2f;
			float backZ = this.getPosition().z - this.length / 2f;

			gl.glDisable(GL.GL_LIGHTING);

			// use white color for the box
			float[] color = { 1f, 1f, 1f, 1f };
			gl.glColor4fv(color, 0);

			gl.glPushMatrix();
			{
				gl.glBegin(GL.GL_LINES);
				{
					// front square
					gl.glVertex3f(leftX, bottomY, frontZ);
					gl.glVertex3f(rightX, bottomY, frontZ);
					gl.glVertex3f(rightX, bottomY, frontZ);
					gl.glVertex3f(rightX, topY, frontZ);
					gl.glVertex3f(rightX, topY, frontZ);
					gl.glVertex3f(leftX, topY, frontZ);
					gl.glVertex3f(leftX, topY, frontZ);
					gl.glVertex3f(leftX, bottomY, frontZ);

					// back square
					gl.glVertex3f(leftX, bottomY, backZ);
					gl.glVertex3f(rightX, bottomY, backZ);
					gl.glVertex3f(rightX, bottomY, backZ);
					gl.glVertex3f(rightX, topY, backZ);
					gl.glVertex3f(rightX, topY, backZ);
					gl.glVertex3f(leftX, topY, backZ);
					gl.glVertex3f(leftX, topY, backZ);
					gl.glVertex3f(leftX, bottomY, backZ);

					// left square
					gl.glVertex3f(leftX, bottomY, frontZ);
					gl.glVertex3f(leftX, topY, frontZ);
					gl.glVertex3f(leftX, topY, frontZ);
					gl.glVertex3f(leftX, topY, backZ);
					gl.glVertex3f(leftX, topY, backZ);
					gl.glVertex3f(leftX, bottomY, backZ);
					gl.glVertex3f(leftX, bottomY, backZ);
					gl.glVertex3f(leftX, bottomY, frontZ);

					// right square
					gl.glVertex3f(rightX, bottomY, frontZ);
					gl.glVertex3f(rightX, topY, frontZ);
					gl.glVertex3f(rightX, topY, frontZ);
					gl.glVertex3f(rightX, topY, backZ);
					gl.glVertex3f(rightX, topY, backZ);
					gl.glVertex3f(rightX, bottomY, backZ);
					gl.glVertex3f(rightX, bottomY, backZ);
					gl.glVertex3f(rightX, bottomY, frontZ);
				}
				gl.glEnd();
			}
			gl.glPopMatrix();

			gl.glEnable(GL.GL_LIGHTING);
		}
		gl.glPopAttrib();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see de.rico.adventure.entity.hotspot.AbstractInteractiveHotspot#isMouseOverHotspot(int,
	 *      int)
	 */
	@Override
	public boolean isMouseOverHotspot(int screenX, int screenY)
	{
		return this.getMouseOverHotspot(screenX, screenY) != null? true:false;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see de.rico.adventure.entity.hotspot.AbstractInteractiveHotspot#getMouseOverHotspot(int,
	 *      int)
	 */
	@Override
	public Point3D getMouseOverHotspot(int screenX, int screenY)
	{
		// if the hotspot isn't enabled do nothing
		if(this.isEnabled() == false) return null;

		Line3D line = Tools.createLineFromScreenCoords(screenX, screenY);

		// create ray from the specified line
		Ray ray = new Ray(line.getStartPoint(), Point3D.subtract(line
				.getEndPoint(), line.getStartPoint()));

		float leftX = this.getPosition().x - this.width / 2f;
		float rightX = this.getPosition().x + this.width / 2f;
		float topY = this.getPosition().y + this.height / 2f;
		float bottomY = this.getPosition().y - this.height / 2f;
		float frontZ = this.getPosition().z + this.length / 2f;
		float backZ = this.getPosition().z - this.length / 2f;


		/*
		 * The following code was taken from the book "3D Math Primer for
		 * Graphics and Game Development" by Fletcher Dunn and Ian Parberry at
		 * page 307. That book itself refers to another source: "Fast Ray-Box
		 * Intersection," by Woo in Graphics Gems I, page 395.
		 */

		// Check for point inside box, trivial reject, and determine parametric
		// distance to each front face
		boolean inside = true;
		float xt = 0f;
		float xn = 0f;
		if (ray.getOrigin().x < leftX)
		{
			xt = leftX - ray.getOrigin().x;
			if (xt > ray.getDirection().x) return null;
			xt /= ray.getDirection().x;
			inside = false;
			xn = -1f;
		}
		else if (ray.getOrigin().x > rightX)
		{
			xt = rightX - ray.getOrigin().x;
			if (xt < ray.getDirection().x) return null;
			xt /= ray.getDirection().x;
			inside = false;
			xn = 1.0f;
		}
		else
		{
			xt = -1f;
		}

		float yt, yn;
		if (ray.getOrigin().y < bottomY)
		{
			yt = bottomY - ray.getOrigin().y;
			if (yt > ray.getDirection().y) return null;
			yt /= ray.getDirection().y;
			inside = false;
			yn = -1f;
		}
		else if (ray.getOrigin().y > topY)
		{
			yt = topY - ray.getOrigin().y;
			if (yt < ray.getDirection().y) return null;
			yt /= ray.getDirection().y;
			inside = false;
			yn = 1.0f;
		}
		else
		{
			yt = -1f;
		}
		float zt = 0f;
		float zn = 0f;
		if (ray.getOrigin().z < backZ)
		{
			zt = backZ - ray.getOrigin().z;
			if (zt > ray.getDirection().z) return null;
			zt /= ray.getDirection().z;
			inside = false;
			zn = -1f;
		}
		else if (ray.getOrigin().z > frontZ)
		{
			zt = frontZ - ray.getOrigin().z;
			if (zt < ray.getDirection().z) return null;
			zt /= ray.getDirection().z;
			inside = false;
			zn = 1.0f;
		}
		else
		{
			zt = -1f;
		}
		// Inside box?
		if (inside)
		{
			return ray.getPointAtDistance(0f);
		}
		// Select farthest plane - this is
		// the plane of intersection.
		int which = 0;
		float t = xt;
		if (yt > t)
		{
			which = 1;
			t = yt;
		}

		if (zt > t)
		{
			which = 2;
			t = zt;
		}
		switch (which)
		{
			case 0: // intersect with yz plane
			{
				float y = ray.getPointAtDistance(t).y;
				if (y < bottomY || y > topY) return null;
				float z = ray.getPointAtDistance(t).z;
				if (z < backZ || z > frontZ) return null;
			}
				break;
			case 1: // intersect with xz plane
			{
				float x = ray.getPointAtDistance(t).x;
				if (x < leftX || x > rightX) return null;
				float z = ray.getPointAtDistance(t).z;
				if (z < backZ || z > frontZ) return null;
			}
				break;
			case 2: // intersect with xy plane
			{
				float x = ray.getPointAtDistance(t).x;
				if (x < leftX || x > rightX) return null;
				float y = ray.getPointAtDistance(t).y;
				if (y < bottomY || y > topY) return null;

			}
				break;
		}

		// return point of intersection
		Point3D intersect = ray.getPointAtDistance(t);
		if(intersect.isWithinFrustum(Camera.getInstance().getFrustum()))
			return intersect;
		else return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see de.rico.adventure.entity.hotspot.AbstractInteractiveHotspot#isWithinFrustum(de.rico.engine.camera.culling.ViewingFrustum)
	 */
	@Override
	public boolean isWithinFrustum(ViewingFrustum frustum)
	{
		// if the hotspot isn't enabled do nothing
		if(this.isEnabled() == false) return false;

		// organize the planes in an array to be able to use
		// a simple loop for treating them
		FrustumPlane[] planes = new FrustumPlane[6];
		planes[0] = frustum.getFrontPlane();
		planes[1] = frustum.getBackPlane();
		planes[2] = frustum.getLeftPlane();
		planes[3] = frustum.getRightPlane();
		planes[4] = frustum.getTopPlane();
		planes[5] = frustum.getBottomPlane();

		float leftX = this.getPosition().x - this.width / 2f;
		float rightX = this.getPosition().x + this.width / 2f;
		float topY = this.getPosition().y + this.height / 2f;
		float bottomY = this.getPosition().y - this.height / 2f;
		float frontZ = this.getPosition().z + this.length / 2f;
		float backZ = this.getPosition().z - this.length / 2f;

		// check box with the different planes
		for (int i = 0; i < planes.length; i++)
		{
			// the sequence in which the points of the box are
			// checked is significant, because I check them
			// something like this:
			// the method can possibly return a TRUE value faster
			// if you at first check a point from one side of the
			// bounding box and then a point from the other side,
			// because it is likely that all the other points
			// of the same side are also within the frustum and
			// probably would return the same value than the
			// tested point.
			// I do the same with points from the top and the
			// bottom of the box.

			// check box point (left/bottom/front)
			if (planes[i].getA() * leftX + planes[i].getB() * bottomY
					+ planes[i].getC() * frontZ + planes[i].getD() > 0) return true;

			// check box point (right/bottom/back)
			if (planes[i].getA() * rightX + planes[i].getB()
					* bottomY + planes[i].getC() * backZ
					+ planes[i].getD() > 0) return true;

			// check box point (left/top/front)
			if (planes[i].getA() * leftX + planes[i].getB() * topY
					+ planes[i].getC() * frontZ + planes[i].getD() > 0) return true;

			// check box point (right/top/back)
			if (planes[i].getA() * rightX + planes[i].getB() * topY
					+ planes[i].getC() * backZ + planes[i].getD() > 0) return true;

			// check box point (left/top/back)
			if (planes[i].getA() * leftX + planes[i].getB() * topY
					+ planes[i].getC() * backZ + planes[i].getD() > 0) return true;

			// check box point (right/bottom/front)
			if (planes[i].getA() * rightX + planes[i].getB()
					* bottomY + planes[i].getC() * frontZ
					+ planes[i].getD() > 0) return true;

			// check box point (left/bottom/back)
			if (planes[i].getA() * leftX + planes[i].getB() * bottomY
					+ planes[i].getC() * backZ + planes[i].getD() > 0) return true;

			// check box point (right/top/front)
			if (planes[i].getA() * rightX + planes[i].getB() * topY
					+ planes[i].getC() * frontZ + planes[i].getD() > 0) return true;
		}

		// return false if none of the planes caused a return of true
		return false;
	}

}
