package de.rico.adventure.script.trigger;

import java.util.ArrayList;
import java.util.List;

import javax.media.opengl.GL;

import de.rico.adventure.entity.character.AbstractCharacter;
import de.rico.engine.geometry.MeshDL;
import de.rico.engine.material.Color4f;

/**
 * This is the base class for every trigger in the game.
 * <br>
 * <br>
 * <b>Note:</b> By convention you have to define two methods for
 * each trigger  in your corresponding script file. The method names
 * have to be exactly like this:
 * scriptId+"onEnter()" and scriptId+"OnExit()". The script id value
 * has been specified in the constructor. Simply concat "OnEnter()" and
 * "OnExit()" to it.
 * <br>
 * <br>
 * <b>Example:</b> Image the specified script id parameter in the
 * constructor was "trigger". The two correponding methods would
 * be called <code>triggerOnEnter()</code> and <code>triggerOnExit()</code>
 * @author Frank Bruns
 *
 */
public abstract class AbstractTrigger
{
	private String scriptId = null;

	private float x = 0f;
	private float z = 0f;

	private Color4f meshColor = new Color4f();
	protected MeshDL visMesh = null; // visualisation mesh

	// characters that are to be taken into consideration
	// for firering the trigger
	private List<AbstractCharacter> candidates = null;
	private ArrayList<AbstractCharacter> candidatesToAdd = null;
	private ArrayList<AbstractCharacter> candidatesToRemove = null;

	// flags to signal that the according methods
	// have already been called
	private boolean calledEnterMethod = false;
	private boolean calledExitMethod = true;

	// flag to signal that the trigger is active (enabled)
	private boolean enabled = true;

	/**
	 * Creates a new trigger area that has no candidates for firering
	 * the trigger assigned initially.
	 * @param scriptId id for reference purposes in script files
	 * @param x x-coordinate of the trigger
	 * @param z z-coordinate of the trigger
	 */
	public AbstractTrigger(String scriptId, float x, float z)
	{
		this.scriptId = scriptId;
		this.x = x;
		this.z = z;
		this.candidates = new ArrayList<AbstractCharacter>();
		this.candidatesToAdd = new ArrayList<AbstractCharacter>();
		this.candidatesToRemove = new ArrayList<AbstractCharacter>();
	}

	/**
	 * Creates a new trigger area that reacts only to the
	 * specified character.
	 * @param scriptId id for reference purposes in script files
	 * @param x x-coordinate of the trigger
	 * @param z z-coordinate of the trigger
	 * @param c only character that can fire the trigger
	 */
	public AbstractTrigger(String scriptId, float x, float z, AbstractCharacter c)
	{
		this.scriptId = scriptId;
		this.x = x;
		this.z = z;
		this.candidates = new ArrayList<AbstractCharacter>();
		this.candidates.add(c);
		this.candidatesToAdd = new ArrayList<AbstractCharacter>();
		this.candidatesToRemove = new ArrayList<AbstractCharacter>();
	}

	/**
	 * Creates a new trigger area with a list of characters
	 * that can fire the trigger.
	 * @param scriptId id for reference purposes in script files
	 * @param x x-coordinate of the trigger
	 * @param z z-coordinate of the trigger
	 * @param candiates
	 */
	public AbstractTrigger(String scriptId, float x, float z, List<AbstractCharacter> candiates)
	{
		this.scriptId = scriptId;
		this.x = x;
		this.z = z;
		this.candidates = candiates;
		this.candidatesToAdd = new ArrayList<AbstractCharacter>();
		this.candidatesToRemove = new ArrayList<AbstractCharacter>();
	}

	/**
	 * Sets the color of the trigger visualisation mesh.
	 * @param color color of the trigger mesh
	 */
	public void setColor(Color4f color)
	{
		this.meshColor.setColor(color);
	}

	/**
	 * Returns the color of the trigger's visalisation mesh.
	 * @return color of the trigger's mesh
	 */
	public Color4f getColor()
	{
		return this.meshColor;
	}

	/**
	 * Adds a candiate for firering the trigger.
	 * @param c candiate for firering the trigger
	 */
	public void addCandidate(AbstractCharacter c)
	{
		this.candidatesToAdd.add(c);
	}

	/**
	 * Removes a candiate for firering the trigger from the list.
	 * @param c candiate for firering the trigger
	 */
	public void removeCandidate(AbstractCharacter c)
	{
		this.candidatesToRemove.add(c);
	}

	/**
	 * Returns a list with all the candiates for firering the trigger.
	 * @return candiates for firering the trigger
	 */
	public List<AbstractCharacter> getCandidates()
	{
		return this.candidates;
	}

	/**
	 * Returns the reference id for this entity which is used in a script file.
	 *
	 * @return reference id in a script file
	 * @throws NullPointerException Occurs when no id has been specified for the
	 *         entity.
	 */
	public String getScriptId()
	{
		return this.scriptId;
	}

	/**
	 * Sets the script id of this trigger to the new string value.
	 * @param id new script id for the trigger
	 */
	public void setScriptId(String id)
	{
		this.scriptId = id;
	}

	/**
	 * Returns the trigger's x-coordinate.
	 * @return x-coordinate of the trigger
	 */
	public float getX()
	{
		return this.x;
	}

	/**
	 * Sets the trigger's x-coordinate.
	 * @param x new x-coordinate of the trigger
	 */
	public void setX(float x)
	{
		this.x = x;
	}

	/**
	 * Returns the trigger's z-coordinate.
	 * @return z-coordinate of the trigger
	 */
	public float getZ()
	{
		return this.z;
	}

	/**
	 * Sets the trigger's z-coordinate.
	 * @param z new z-coordinate of the trigger
	 */
	public void setZ(float z)
	{
		this.z = z;
	}

	/**
	 * Sets the position (x,z) of the trigger.
	 * @param x mew x-coordinate of the trigger
	 * @param z mew z-coordinate of the trigger
	 */
	public void setPosition(float x, float z)
	{
		this.setX(x);
		this.setZ(z);
	}

	/**
	 * Enables or disables the trigger. If the trigger is
	 * disabled it will not react to firering candidates anymore.
	 * @param enable set trigger enabled? --> true=yes, false=false
	 */
	public void setEnabled(boolean enable)
	{
		this.enabled = enable;
	}

	/**
	 * Determines whether or not the trigger is enabled.
	 * @return is trigger enabled? --> true=yes, false=no
	 */
	public boolean isEnabled()
	{
		return this.enabled;
	}

	/**
	 * Determines if the "on enter method" has already been called.
	 * @return on enter method has been called? --> true=yes, false=no
	 */
	public boolean hasBeenCalledEnterMethod()
	{
		return this.calledEnterMethod;
	}

	/**
	 * Sets a flag if the "on enter method" has been called.
	 * @param called has been called on enter method? --> true=yes, false=no
	 */
	public void setCalledEnterMethod(boolean called)
	{
		this.calledEnterMethod = called;
	}

	/**
	 * Determines if the "on exit method" has already been called.
	 * @return on exit method has been called? --> true=yes, false=no
	 */
	public boolean hasBeenCalledExitMethod()
	{
		return this.calledExitMethod;
	}

	/**
	 * Sets a flag if the "on exit method" has been called.
	 * @param called has been called on eexit method? --> true=yes, false=no
	 */
	public void setCalledExitMethod(boolean called)
	{
		this.calledExitMethod = called;
	}

	/**
	 * Determines if at least one candiate character is within the trigger area.
	 * @return is character on trigger? --> true=yes, false=no
	 */
	public abstract boolean isCandiateOnTrigger();


	/**
	 * Updates the trigger state. This method monitors, if a
	 * character has entered or leaved the trigger area and
	 * invokes the enter or exit method accordingly.
	 * @param elapsedTime time between two frames
	 */
	public void update(long elapsedTime)
	{
		// remove and add the candidates that have been set to be
		// removed or added
		this.candidates.removeAll(this.candidatesToRemove);
		this.candidates.addAll(this.candidatesToAdd);
		this.candidatesToRemove.clear();
		this.candidatesToAdd.clear();


		// update visual mesh
		if(this.visMesh != null)
		{
			this.visMesh.setPosition(this.getX(), 0f, this.getZ());

			// adapt color
			if(!this.getColor().equals(this.visMesh.getMaterial().getDiffuse()))
				this.visMesh.getMaterial().getDiffuse().setColor(this.getColor());

			// lift the trigger a little bit, if its color is not white
			// (i.e. it is selected in the editor)
			if(!this.getColor().equals(1f, 1f, 1f, 1f))
			{
				this.visMesh.setPosY(0.01f);
			}

			this.visMesh.update(elapsedTime);
		}


		if(this.enabled == false) return; // do nothing else


		// operations for checking enters and exits happening
		if(this.isCandiateOnTrigger() == true)
		{
			this.setCalledExitMethod(false);

			if(this.hasBeenCalledEnterMethod() == false)
			{
				// invoke the scripts onEnter() method for this trigger
				for(AbstractCharacter c : this.candidates)
					c.getRoom().getChapter().getScriptInterpreter().invokeMethod(this.scriptId+"OnEnter()");
				this.setCalledEnterMethod(true);
			}
		}
		else
		{
			this.setCalledEnterMethod(false);

			if(this.hasBeenCalledExitMethod() == false)
			{
				// invoke the scripts onExit() method for this trigger
				for(AbstractCharacter c : this.candidates)
					c.getRoom().getChapter().getScriptInterpreter().invokeMethod(this.scriptId+"OnExit()");

				this.setCalledExitMethod(true);
			}
		}
	}

	/**
	 * Draws the trigger area to the ground.
	 * @param gl
	 */
	public abstract void draw(GL gl);
}
