package de.rico.adventure.structure;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import javax.media.opengl.GL;

import de.rico.adventure.entity.AbstractEntity;
import de.rico.adventure.entity.EntityDistanceToCameraComparator;
import de.rico.adventure.entity.character.AbstractCharacter;
import de.rico.adventure.entity.character.NonPlayerCharacter;
import de.rico.adventure.entity.character.PlayableCharacter;
import de.rico.adventure.entity.hotspot.AbstractInteractiveHotspot;
import de.rico.adventure.entity.hotspot.HotspotDistanceToCameraComparator;
import de.rico.adventure.entity.item.Item;
import de.rico.adventure.entity.prop.Prop;
import de.rico.adventure.enums.State;
import de.rico.adventure.main.ShootOut3DMain;
import de.rico.adventure.script.ScriptInterpreter;
import de.rico.adventure.script.trigger.AbstractTrigger;
import de.rico.editor.io.RoomIO;
import de.rico.engine.camera.Camera;
import de.rico.engine.camera.CameraPerspective;
import de.rico.engine.controller.ControllerIface;
import de.rico.engine.effect.light.LightIface;
import de.rico.engine.effect.light.ogllight.AbstractLight;
import de.rico.engine.effect.light.ogllight.DirectionalLight;
import de.rico.engine.effect.light.ogllight.PositionalLight;
import de.rico.engine.effect.light.ogllight.SpotLight;
import de.rico.engine.effect.particle.AbstractParticleEffect;
import de.rico.engine.effect.particle.ParticleEffectContainer;
import de.rico.engine.effect.texproj.TextureProjector;
import de.rico.engine.geometry.AbstractGroundMesh;
import de.rico.engine.geometry.AbstractMesh;
import de.rico.engine.geometry.MeshContainer;
import de.rico.engine.geometry.Point3D;
import de.rico.engine.input.InputManager;

/**
 * This class specifies a single room (game location) in the adventure game.
 *
 * @author Frank Bruns
 */
public class Room
{
	private String roomFile = null;
	private String scriptId = null;
	private String scriptFile = null;
	private Chapter assignedChapter = null;
	private boolean hasBeenEnteredAlready = false;

	private MeshContainer meshes = null;

	private AbstractGroundMesh ground = null;
	private ParticleEffectContainer effects = null;
	private PlayableCharacter player = null;

	private LinkedList<LightIface> lights = null;
	private LinkedList<LightIface> lightsToAdd = null;
	private LinkedList<LightIface> lightsToRemove = null;
	private PositionalLight shadowRefLight = null; // shadow reference light

	private ArrayList<NonPlayerCharacter> characters = null;
	private ArrayList<NonPlayerCharacter> charactersToAdd = null;
	private ArrayList<NonPlayerCharacter> charactersToRemove = null;

	private ArrayList<TextureProjector> texProjectors = null;
	private ArrayList<TextureProjector> texProjectorsToAdd = null;
	private ArrayList<TextureProjector> texProjectorsToRemove = null;

	private ArrayList<Prop> props = null;
	private ArrayList<Prop> propsToAdd = null;
	private ArrayList<Prop> propsToRemove = null;

	private ArrayList<Item> items = null;
	private ArrayList<Item> itemsToAdd = null;
	private ArrayList<Item> itemsToRemove = null;

	private ArrayList<AbstractTrigger> triggers = null;
	private ArrayList<AbstractTrigger> triggersToAdd = null;
	private ArrayList<AbstractTrigger> triggersToRemove = null;

	private ArrayList<AbstractInteractiveHotspot> hotspots = null;
	private ArrayList<AbstractInteractiveHotspot> hotspotsToAdd = null;
	private ArrayList<AbstractInteractiveHotspot> hotspotsToRemove = null;

	private ArrayList<EntryPoint> entryPoints = null;
	private ArrayList<EntryPoint> entryPointsToAdd = null;
	private ArrayList<EntryPoint> entryPointsToRemove = null;

	private ArrayList<ControllerIface> controllers = null;
	private ArrayList<ControllerIface> controllersToAdd = null;
	private ArrayList<ControllerIface> controllersToRemove = null;

	private LinkedHashMap<Integer, CameraPerspective> camPerspectives = null;
	private int initialCamPerspective = 0;

	private HotspotDistanceToCameraComparator hotspotComparator = null;
	private EntityDistanceToCameraComparator propComparator = null;


	private InputManager inputManager = null;

	/**
	 * Creates a new room with the specified input manager object.
	 * @param input input manager object
	 */
	public Room(InputManager input)
	{
		this.meshes = new MeshContainer();
		this.effects = new ParticleEffectContainer();

		this.lights = new LinkedList<LightIface>();
		this.lightsToAdd = new LinkedList<LightIface>();
		this.lightsToRemove = new LinkedList<LightIface>();

		this.texProjectors = new ArrayList<TextureProjector>();
		this.texProjectorsToAdd = new ArrayList<TextureProjector>();
		this.texProjectorsToRemove = new ArrayList<TextureProjector>();

		this.characters = new ArrayList<NonPlayerCharacter>();
		this.charactersToAdd = new ArrayList<NonPlayerCharacter>();
		this.charactersToRemove = new ArrayList<NonPlayerCharacter>();

		this.items = new ArrayList<Item>();
		this.itemsToAdd = new ArrayList<Item>();
		this.itemsToRemove = new ArrayList<Item>();

		this.props = new ArrayList<Prop>();
		this.propsToAdd = new ArrayList<Prop>();
		this.propsToRemove = new ArrayList<Prop>();

		this.triggers = new ArrayList<AbstractTrigger>();
		this.triggersToAdd = new ArrayList<AbstractTrigger>();
		this.triggersToRemove = new ArrayList<AbstractTrigger>();

		this.hotspots = new ArrayList<AbstractInteractiveHotspot>();
		this.hotspotsToAdd = new ArrayList<AbstractInteractiveHotspot>();
		this.hotspotsToRemove = new ArrayList<AbstractInteractiveHotspot>();

		this.entryPoints = new ArrayList<EntryPoint>();
		this.entryPointsToAdd = new ArrayList<EntryPoint>();
		this.entryPointsToRemove = new ArrayList<EntryPoint>();

		this.controllers = new ArrayList<ControllerIface>();
		this.controllersToAdd = new ArrayList<ControllerIface>();
		this.controllersToRemove = new ArrayList<ControllerIface>();

		this.camPerspectives = new LinkedHashMap<Integer, CameraPerspective>();

		this.hotspotComparator = new HotspotDistanceToCameraComparator();
		this.propComparator = new EntityDistanceToCameraComparator();
		this.inputManager = input;
	}

	/**
	 * Creates a new room with the specified parameters. The script
	 * id is used for referencingthis room in a chapter.
	 * @param roomFile path to the .room file for the room
	 * @param input input manager object
	 */
	public Room(String roomFile, InputManager input)
	{
		this(input);

		this.roomFile = roomFile;

		// load the room data from the .room file
		new RoomIO(this,false).importRoom(this.roomFile);
	}

	/**
	 * Sets the script id of the room.
	 * @param scriptId script id for the room
	 */
	public void setScriptId(String scriptId)
	{
		this.scriptId = scriptId;
	}

	/**
	 * Returns the script id (which is the room's id in the chapter, too)
	 * of the room.
	 * @return script id of the room
	 */
	public String getScriptId()
	{
		return this.scriptId;
	}

	/**
	 * Returns the script interpreter used by this room. This method
	 * actually is a shortcut to the <code>getChapter().getScriptInterpreter()</code>
	 * call.
	 * @return the script interpreter used by this room
	 */
	public ScriptInterpreter getScriptInterpreter()
	{
		return this.getChapter().getScriptInterpreter();
	}

	/**
	 * Sets the script file of the room.
	 * @param scriptFile script file for the room
	 */
	public void setScriptFile(String scriptFile)
	{
		this.scriptFile = scriptFile;
	}

	/**
	 * Returns the path to the script file that was assigned to
	 * this room.
	 * @return path to script for this room
	 */
	public String getScriptFile()
	{
		return this.scriptFile;
	}

	/**
	 * Determines whether or not the room has been entered already.
	 * @return has the room been entered already? --> true=yes, false=no
	 */
	public boolean hasBeenEnteredAlready()
	{
		return this.hasBeenEnteredAlready;
	}

	/**
	 * Sets the initial camera perspective of the room.
	 * @param initialCam nr of the initial camera perspective
	 */
	public void setInitialCamPerspective(int initialCam)
	{
		this.initialCamPerspective = initialCam;
	}

	/**
	 * Returns the initial camera perspective of the room.
	 * @return nr of the initial camera perspective of the room
	 */
	public int getInitialCamPerspective()
	{
		return this.initialCamPerspective;
	}

	/**
	 * Returns the chapter this room belongs to. The assignment
	 * cannot be done manually. It gets assigned in the Chapter class
	 * when the room is assigned to a chapter.
	 * @return chapter this room belongs to
	 */
	public Chapter getChapter()
	{
		return this.assignedChapter;
	}

	/**
	 * Used to assign the chapter of the room in the Chapter class.
	 * @param chapter chapter this room belongs to
	 */
	protected void setChapter(Chapter chapter)
	{
		this.assignedChapter = chapter;
	}

	/**
	 * Adds a trigger to the room.
	 * @param trigger trigger to add to the room
	 */
	public void addTrigger(AbstractTrigger trigger)
	{
		this.triggersToAdd.add(trigger);
	}

	/**
	 * Removes a trigger from the room.
	 */
	public void removeTrigger(AbstractTrigger trigger)
	{
		this.triggersToRemove.add(trigger);
	}

	/**
	 * Removes a trigger from the room by its name.
	 */
	public void removeTrigger(String triggerName)
	{
		this.triggersToRemove.add(this.getTrigger(triggerName));
	}

	/**
	 * Returns the trigger that corresponds to the specified name
	 * (which is its script id).
	 * If none is found this method returns <code>null</code>.
	 * @param triggerName name of the trigger to return.
	 * @return trigger corresponding to the specified trigger name
	 */
	public AbstractTrigger getTrigger(String triggerName)
	{
		for(AbstractTrigger trigger : this.triggers)
		{
			if(triggerName.equals(trigger.getScriptId()))
				return trigger;
		}

		return null;
	}

	/**
	 * Returns the list of triggers of this room.
	 * @return triggers of this room
	 */
	public List<AbstractTrigger> getTriggers()
	{
		return this.triggers;
	}

	/**
	 * Removes every trigger from the room.
	 *
	 */
	public void removeAllTriggers()
	{
		for(int i=0; i<this.triggers.size(); i++)
			this.triggersToRemove.add(this.triggers.get(i));
	}

	/**
	 * Adds the specified light to the room.
	 *
	 * @param light light to add to the room
	 */
	public void addLight(LightIface light)
	{
		this.lightsToAdd.add(light);
	}

	/**
	 * Removes the specified light from the room
	 *
	 * @param light light to remove from the room
	 */
	public void removeLight(LightIface light)
	{
		this.lightsToRemove.add(light);
	}

	/**
	 * Returns the lights of the room.
	 *
	 * @return lights of the room
	 */
	public LinkedList<LightIface> getLights()
	{
		return this.lights;
	}

	/**
	 * Returns the light (child of AbstractLight only!) that has
	 * the specified light nr. If none was found it returns <code>null</code>.
	 * @param nr number of the light to seek
	 * @return the sought light with the specified light number
	 */
	public LightIface getLight(int nr)
	{
		for(LightIface light : this.lights)
		{
			if(light instanceof AbstractLight)
			{
				if(light instanceof DirectionalLight)
				{
					DirectionalLight dir = (DirectionalLight)light;
					if(dir.getLightNr() == nr)
						return dir;
				}
				if(light instanceof PositionalLight)
				{
					PositionalLight pos = (PositionalLight)light;
					if(pos.getLightNr() == nr)
						return pos;
				}
				if(light instanceof SpotLight)
				{
					SpotLight spot = (SpotLight)light;
					if(spot.getLightNr() == nr)
						return spot;
				}
			}
		}

		return null;
	}

	/**
	 * Removes every light from the room.
	 *
	 */
	public void removeAllLights()
	{
		for(int i=0; i<this.lights.size(); i++)
			this.lightsToRemove.add(this.lights.get(i));
	}

	/**
	 * Sets the positional light that ought to be used as a
	 * reference light for shadow casting techniques.
	 * @param refLight shadow reference light
	 */
	public void setShadowReferenceLight(PositionalLight refLight)
	{
		this.shadowRefLight = refLight;
	}

	/**
	 * Returns the shadow reference light for this room. If none was
	 * assigned yet the method returns <code>null</code>.
	 * @return shadow reference light, or <code>null</code>.
	 */
	public PositionalLight getShadowReferenceLight()
	{
		return this.shadowRefLight;
	}

	/**
	 * Adds a texture projector to the room.
	 * @param proj texture projector to add
	 */
	public void addTexProjector(TextureProjector proj)
	{
		this.texProjectorsToAdd.add(proj);
	}

	/**
	 * Removes a texture projector from the room.
	 * @param proj texture projector to remove
	 */
	public void removeTexProjector(TextureProjector proj)
	{
		this.texProjectorsToRemove.add(proj);
	}

	/**
	 * Returns the texture projectors of the room.
	 * @return list with texture projectors
	 */
	public List<TextureProjector> getTexProjectors()
	{
		return this.texProjectors;
	}

	/**
	 * Adds a camera perspective to the room.
	 * @param cam camera perspective to add
	 * @param id reference id for the perspective
	 */
	public void addCameraPerspective(CameraPerspective cam, int id)
	{
		this.camPerspectives.put(id, cam);
	}

	/**
	 * Removes the camera perspective which corresponds to the
	 * specified id from the room.
	 * @param id id of the perspective to remove from the room
	 */
	public void removeCameraPerspective(int id)
	{
		this.camPerspectives.remove(id);
	}

	/**
	 * Removes all camera perspectives that were assigned to
	 * this room.
	 *
	 */
	public void removeAllCameraPerspectives()
	{
		this.camPerspectives.clear();
	}

	/**
	 * Retuns the map of camera perspectives that is assigned to
	 * this room.
	 * @return map of camera perspective of this room
	 */
	public LinkedHashMap<Integer, CameraPerspective> getCameraPerspectives()
	{
		return this.camPerspectives;
	}

	/**
	 * Adds an entry point to the room.
	 * @param ep entry point to add
	 */
	public void addEntryPoint(EntryPoint ep)
	{
		this.entryPointsToAdd.add(ep);
	}

	/**
	 * Removes the specified entry point from the room.
	 * @param ep entry point to remove
	 */
	public void removeEntryPoint(EntryPoint ep)
	{
		this.entryPointsToRemove.add(ep);
	}

	/**
	 * Removes all entry points from the room.
	 *
	 */
	public void removeAllEntryPoints()
	{
		this.entryPointsToRemove.addAll(this.entryPoints);
	}

	/**
	 * Returns the entry point that correspond to the
	 * specified script id. If none was found <code>null</code>
	 * is returned.
	 * @param scriptId script id of seeked entry point
	 * @return entry point that correspond to the script id
	 */
	public EntryPoint getEntryPoint(String scriptId)
	{
		for(EntryPoint ep : this.entryPoints)
		{
			if(scriptId.equals(ep.getScriptId()))
				return ep;
		}

		return null;
	}

	/**
	 * Returns the entry points of the room.
	 * @return the room's entry points
	 */
	public List<EntryPoint> getEntryPoints()
	{
		return this.entryPoints;
	}

	/**
	 * Adds an controller to the room.
	 * @param cntr controller to add
	 */
	public void addController(ControllerIface cntr )
	{
		this.controllersToAdd.add(cntr);
	}

	/**
	 * Removes the specified controller from the room.
	 * @param cntr controller to remove
	 */
	public void removeController(ControllerIface cntr)
	{
		this.controllersToRemove.add(cntr);
	}

	/**
	 * Removes all controllers from the room.
	 *
	 */
	public void removeControllers()
	{
		this.controllersToRemove.addAll(this.controllers);
	}


	/**
	 * Returns the controllers of the room.
	 * @return the room's controllers
	 */
	public List<ControllerIface> getControllers()
	{
		return this.controllers;
	}

	/**
	 * Assigns the playable character to the room.
	 *
	 * @param player playable character
	 */
	public void setPlayer(PlayableCharacter player)
	{
		this.player = player;
		if(this.meshes.getMeshes().contains(player.getMesh()) == false)
			this.meshes.add(player.getMesh());

		// assign player to this room
		this.player.setRoom(this);
	}

	/**
	 * Assigns the playable character to the room. The player
	 * will be positioned and rotated according to the entry point
	 * which corresponds to the specified entry point script id
	 *
	 * @param player playable character
	 * @param entryPointId script id of the entry point to take as
	 * reference for positioning and rotating the player
	 */
	public void setPlayer(PlayableCharacter player, String entryPointId)
	{
		this.player = player;
		if(this.meshes.getMeshes().contains(player.getMesh()) == false)
			this.meshes.add(player.getMesh());

		// set player to the entry point location
		EntryPoint ep = this.getEntryPoint(entryPointId);
		player.getMesh().setPosition(ep.getPosition());
		player.getMesh().setRotation(ep.getRotation());

		// assign player to this room
		this.player.setRoom(this);
	}

	/**
	 * Returns the playable character from this room.
	 *
	 * @return playable character of the room
	 */
	public PlayableCharacter getPlayer()
	{
		return this.player;
	}

	/**
	 * Removes the playable character from the room.
	 */
	public void removePlayer()
	{
		if (this.player == null) return;

		// dereference this room from the character
		this.player.setRoom(null);
		this.player = null;
	}

	/**
	 * Adds a non player character to the room.
	 *
	 * @param character non player character to add to the room
	 */
	public void addCharacter(NonPlayerCharacter character)
	{
		this.charactersToAdd.add(character);
		this.meshes.add(character.getMesh());

		// assign room
		character.setRoom(this);
	}

	/**
	 * Removes a non player character from the room.
	 *
	 * @param character character to remove from the room
	 */
	public void removeCharacter(NonPlayerCharacter character)
	{
		this.charactersToRemove.add(character);
		this.meshes.remove(character.getMesh());
	}

	/**
	 * Returns the non player characters that are assigned to the room.
	 *
	 * @return characters in the room.
	 */
	public ArrayList<NonPlayerCharacter> getCharacters()
	{
		return this.characters;
	}

	/**
	 * Returns the nonplayer character with the specified id
	 * @param scriptId
	 * @return the NPC with the specified id
	 */
	public NonPlayerCharacter getCharacter(String scriptId)
	{
		for(NonPlayerCharacter c : this.characters)
		{
			if(scriptId.equals(c.getScriptId()))
			{
				return c;
			}
		}

		return null;
	}

	/**
	 * Removes every non player character from the room.
	 *
	 */
	public void removeAllCharacters()
	{
		for(int i=0; i<this.characters.size(); i++)
			this.charactersToRemove.add(this.characters.get(i));
	}

	/**
	 * Adds an item to the room.
	 *
	 * @param item item to add to the room
	 */
	public void addItem(Item item)
	{
		this.itemsToAdd.add(item);
		this.meshes.add(item.getMesh());
	}

	/**
	 * Removes an item from the room.
	 *
	 * @param item item to remove from the room
	 */
	public void removeItem(Item item)
	{
		this.itemsToRemove.add(item);
		this.meshes.remove(item.getMesh());
	}

	/**
	 * Returns the items that are assigned to the room.
	 *
	 * @return items in the room.
	 */
	public ArrayList<Item> getItems()
	{
		return this.items;
	}

	/**
	 * Removes every item from the room.
	 *
	 */
	public void removeAllItems()
	{
		for(int i=0; i<this.items.size(); i++)
			this.itemsToRemove.add(this.items.get(i));
	}

	/**
	 * Adds a prop, a passive mesh which is only used
	 * for constructing the setting of the room. <br>
	 * <b>Note:</b> If you're adding a ground mesh you will be able to
	 * access it by the <code>getGround()</code> method.
	 *
	 * @param prop prop for the setting
	 */
	public void addProp(Prop prop)
	{
		this.propsToAdd.add(prop);
		this.meshes.add(prop.getMesh());

		// make it easier to access the ground mesh by
		// creating an explicit reference object
		if (prop.getMesh() instanceof AbstractGroundMesh)
			this.ground = (AbstractGroundMesh) prop.getMesh();
	}

	public void removeProp(Prop prop)
	{
		this.propsToRemove.add(prop);
		this.meshes.remove(prop.getMesh());
	}

	/**
	 * Returns the props of this room.
	 * @return props of this room
	 */
	public ArrayList<Prop> getProps()
	{
		return this.props;
	}

	/**
	 * Removes every prop from the room.
	 *
	 */
	public void removeAllProps()
	{
		for(int i=0; i<this.props.size(); i++)
			this.propsToRemove.add(this.props.get(i));
	}

	/**
	 * Adds a particle the effect to the room's particle effect
	 * container.
	 * @param effect particle effect to add to the room
	 */
	public void addParticleEffect(AbstractParticleEffect effect)
	{
		this.effects.add(effect);
	}

	/**
	 * Removes the specified particle effect from the room's
	 * particle container.
	 * @param effect particle effect to remove from the room.
	 */
	public void removeParticleEffect(AbstractParticleEffect effect)
	{
		this.effects.remove(effect);
	}

	/**
	 * Returns a list containing the particle effects of the room.
	 * @return particle effects of the room
	 */
	public List<AbstractParticleEffect> getParticleEffects()
	{
		return this.effects.getEffects();
	}

	/**
	 * Adds an interactive hotspot to the room.
	 * @param hs interactive hotspot to add
	 */
	public void addHotspot(AbstractInteractiveHotspot hs)
	{
		this.hotspotsToAdd.add(hs);
	}

	/**
	 * Removes the specified hotspot from the room.
	 * @param hs interactive hotspot to remove
	 */
	public void removeHotspot(AbstractInteractiveHotspot hs)
	{
		this.hotspotsToRemove.add(hs);
	}

	/**
	 * Removes the hotspot from the room which corresponds to
	 * the specified script id.
	 * @param scriptId script id of the hotspot to remove
	 */
	public void removeHotspot(String scriptId)
	{
		this.hotspotsToRemove.add(this.getHotspot(scriptId));
	}

	/**
	 * Returns a hotspot by its script id. If none was found this
	 * method returns <code>null</code>.
	 * @param scriptId seeked hotspot's script id
	 * @return hotspot that corresponds to the specified script id
	 */
	public AbstractInteractiveHotspot getHotspot(String scriptId)
	{
		for(AbstractInteractiveHotspot hs : this.hotspots)
		{
			if(scriptId.equals(hs.getScriptId()))
				return hs;
		}

		return null;
	}

	/**
	 * Returns the first found hotspot that lies under the specified
	 * screen coordinates. if none was found this method
	 * returns <code>null</code>.
	 * @param screenX screen x-coordinate
	 * @param screenY screen y-coordinate
	 * @return hotspot under the screen coordinates
	 */
	public AbstractInteractiveHotspot getHotspot(int screenX, int screenY)
	{
		for(AbstractInteractiveHotspot hot : this.hotspots)
		{
			if(hot.isMouseOverHotspot(screenX, screenY) == true)
				return hot;
		}

		return null;
	}

	/**
	 * Returns a list containing the hotspots of the room.
	 * @return list of hotspots
	 */
	public List<AbstractInteractiveHotspot> getHotspots()
	{
		return this.hotspots;
	}

	/**
	 * Returns the ground mesh, if one has been added to the room, else
	 * <code>null</code>.
	 *
	 * @return the room's ground mesh, else <code>null</code>.
	 */
	public AbstractGroundMesh getGround()
	{
		return this.ground;
	}

	/**
	 * Returns the mesh container with meshes of this room.
	 *
	 * @return mesh container of the room
	 */
	public MeshContainer getMeshes()
	{
		return this.meshes;
	}

	/**
	 * Returns the entity that corresponds to the specified mesh
	 * object. If none is found the method returns <code>null</code>.
	 * @param mesh mesh that belongs to the seeked entity.
	 * @return entity that is based on the specified mesh
	 */
	public AbstractEntity getEntityByMesh(AbstractMesh mesh)
	{
		ArrayList<AbstractEntity> entities = new ArrayList<AbstractEntity>();
		entities.addAll(this.characters);
		entities.addAll(this.items);
		entities.addAll(this.props);

		AbstractEntity seekedEntity = null;

		for(AbstractEntity entity : entities)
		{
			if(mesh == entity.getMesh())
			{
				seekedEntity = entity;
				break;
			}
		}

		return seekedEntity;
	}

	/**
	 * Updates everything in the room.
	 *
	 * @param elapsedTime time between two frames
	 */
	public void update(long elapsedTime)
	{
		// sort hotspots and props according to the distance from the camera
		// to reach correct overlapping of the hotspots
		Collections.sort(this.hotspots, this.hotspotComparator);
		Collections.sort(this.props, this.propComparator);

		// remove lights that have been set to be removed
		if (this.lightsToRemove.size() > 0)
		{
			// remove from list
			this.lights.removeAll(this.lightsToRemove);

			// destroy the lights
			for(int i=0; i<this.lightsToRemove.size(); i++)
				this.lightsToRemove.get(i).destroy();

			this.lightsToRemove.clear();
		}

		// add lights that have been set to be added
		if (this.lightsToAdd.size() > 0)
		{
			this.lights.addAll(this.lightsToAdd);
			this.lightsToAdd.clear();
		}

		// remove texture projectors that have been set to be
		// removed
		if(this.texProjectorsToRemove.size() > 0)
		{
			this.texProjectors.removeAll(this.texProjectorsToRemove);
			this.texProjectorsToRemove.clear();
		}

		// add texture projectors that have been set to be
		// added
		if(this.texProjectorsToAdd.size() > 0)
		{
			this.texProjectors.addAll(this.texProjectorsToAdd);
			this.texProjectorsToAdd.clear();
		}


		// remove characters that have been set to be removed
		if (this.charactersToRemove.size() > 0)
		{
			// remove from list
			this.characters.removeAll(this.charactersToRemove);

			//destroy the character meshes
			for(int i=0; i<this.charactersToRemove.size(); i++)
				this.charactersToRemove.get(i).getMesh().destroy();

			this.charactersToRemove.clear();
		}

		// add characters that have been set to be added
		if (this.charactersToAdd.size() > 0)
		{
			this.characters.addAll(this.charactersToAdd);
			this.charactersToAdd.clear();
		}

		// remove items that have been set to be removed
		if (this.itemsToRemove.size() > 0)
		{
			// remove from list
			this.items.removeAll(this.itemsToRemove);

			// destroy the items
			for(int i=0; i<this.itemsToRemove.size(); i++)
				this.itemsToRemove.get(i).getMesh().destroy();

			this.itemsToRemove.clear();
		}

		// add items that have been set to be added
		if (this.itemsToAdd.size() > 0)
		{
			this.items.addAll(this.itemsToAdd);
			this.itemsToAdd.clear();
		}

		// remove props that have been set to be removed
		if (this.propsToRemove.size() > 0)
		{
			// remove from list
			this.props.removeAll(this.propsToRemove);

			// destroy the props
			for(int i=0; i<this.propsToRemove.size(); i++)
				this.propsToRemove.get(i).destroy();

			this.propsToRemove.clear();
		}

		// add props that have been set to be added
		if (this.propsToAdd.size() > 0)
		{
			this.props.addAll(this.propsToAdd);
			this.propsToAdd.clear();
		}

		// remove triggers that have been set to be removed
		if (this.triggersToRemove.size() > 0)
		{
			// remove from list
			this.triggers.removeAll(this.triggersToRemove);

			this.triggersToRemove.clear();
		}

		// add triggers that have been set to be added
		if (this.triggersToAdd.size() > 0)
		{
			this.triggers.addAll(this.triggersToAdd);
			this.triggersToAdd.clear();
		}

		// remove hotspots that have been set to be removed
		if (this.hotspotsToRemove.size() > 0)
		{
			// remove from list
			this.hotspots.removeAll(this.hotspotsToRemove);

			this.hotspotsToRemove.clear();
		}

		// add hotspots that have been set to be added
		if (this.hotspotsToAdd.size() > 0)
		{
			this.hotspots.addAll(this.hotspotsToAdd);
			this.hotspotsToAdd.clear();
		}


		// remove entry points that have been set to be removed
		if (this.entryPointsToRemove.size() > 0)
		{
			// remove from list
			this.entryPoints.removeAll(this.entryPointsToRemove);

			this.entryPointsToRemove.clear();
		}

		// add entry points that have been set to be added
		if (this.entryPointsToAdd.size() > 0)
		{
			this.entryPoints.addAll(this.entryPointsToAdd);
			this.entryPointsToAdd.clear();
		}

		// remove controllers that have been set to be removed
		if (this.controllersToRemove.size() > 0)
		{
			// remove from list
			this.controllers.removeAll(this.controllersToRemove);

			this.controllersToRemove.clear();
		}

		// add controllers that have been set to be added
		if (this.controllersToAdd.size() > 0)
		{
			this.controllers.addAll(this.controllersToAdd);
			this.controllersToAdd.clear();
		}


		// update every trigger in the room
		for(AbstractTrigger trigger : this.triggers)
			trigger.update(elapsedTime);

		// update the controllers
		for(ControllerIface cntrl : this.controllers)
			cntrl.update(elapsedTime);

		this.meshes.update(elapsedTime);
		this.effects.update(elapsedTime);
	}

	/**
	 * Draws the room and its content.
	 *
	 * @param gl
	 */
	public void draw(GL gl)
	{
		// bind the lights
		for (LightIface light : this.lights)
			light.bind(gl);
		{

			// bind projected textures
			for(TextureProjector texProj : this.texProjectors)
				texProj.bind(gl);
			{
				switch(ShootOut3DMain.getInstance().getShadowType())
				{
					// draw without shadow casting
					case NO_SHADOWS : this.meshes.draw(gl); break;
					// draw with shadow if reference light exists
					case SHADOW_MATRIX : if(this.shadowRefLight != null) this.meshes.drawWithShadowMatrix(gl, this.shadowRefLight);break;
					// draw with shadow map if reference light exists
					case SHADOW_MAP : if(this.shadowRefLight != null) this.meshes.drawWithShadowMap(gl, this.shadowRefLight, 512, 50f, 70f);break;
				}


				// draw paticle effects
				this.effects.draw(gl);
			}
			// release the projected textures; needs to be
			// done in reverse order
			int sizeTexProj = this.texProjectors.size();
			for (int i=sizeTexProj-1; i>=0; i--)
				this.texProjectors.get(i).release(gl);

		}
		// release the lights; needs to be done in reverse order
		int size = this.lights.size();
		for (int i=size-1; i>=0; i--)
			this.lights.get(i).release(gl);

		// only draw inventory and entity names, if in NORMAL or
		// CLOSE_UP state
		if(StateManager.getInstance().equals(State.NORMAL)
				|| StateManager.getInstance().equals(State.MAP)
				|| StateManager.getInstance().equals(State.CLOSE_UP))
		{
			// draw the entities' names, if in NORMAL state

			for (NonPlayerCharacter c : this.characters)
			{
				if (c.isEnabled() == true
						&& c.getMesh().getBoundingVolume().isWithinFrustum(
								Camera.getInstance().getFrustum())
						&& c.getMesh().isOccluded() == false)
				{
					// draw character name
					Point3D posOnMesh = c.getMesh().getMouseOverMesh(
							this.inputManager.getMouseX(),
							this.inputManager.getMouseY());
					if (posOnMesh != null && posOnMesh.isOccluded() == false)
					{
						c.setScreenName("Amour left: "+c.getHitPoints());
						c.drawScreenName(this.inputManager.getMouseX()-32, this.inputManager
								.getMouseY()-32);
						break;
					}
				}
			}

			for (Item i : this.items)
			{
				if (i.isEnabled() == true
						&& i.getMesh().getBoundingVolume().isWithinFrustum(
								Camera.getInstance().getFrustum())
						&& i.getMesh().isOccluded() == false)
				{
					// draw item name and set context sensitive cursor
					Point3D posOnMesh = i.getMesh().getMouseOverMesh(
							this.inputManager.getMouseX(),
							this.inputManager.getMouseY());
					if (posOnMesh != null && posOnMesh.isOccluded() == false)
					{
						i.drawScreenName(this.inputManager.getMouseX(), this.inputManager
								.getMouseY());
						break;
					}
				}
			}

			for (AbstractInteractiveHotspot hs : this.hotspots)
			{
				if (hs.isEnabled() == true
						&& hs.isWithinFrustum(Camera.getInstance().getFrustum()))
				{
					// draw hotspot name
					Point3D mouseOnHSpot = hs.getMouseOverHotspot(
							this.inputManager.getMouseX(),
							this.inputManager.getMouseY());
					if (mouseOnHSpot != null
							&& mouseOnHSpot.isOccluded() == false)
					{
						// only draw name, if the hotspot is visible
						if(mouseOnHSpot.isOccluded() == false)
						{
							hs.drawScreenName(this.inputManager.getMouseX(), this.inputManager
								.getMouseY());
							break;
						}
					}
				}
			}
		}


		// signal that the room is not a virgin anymore ;-)
		this.hasBeenEnteredAlready = true;
	}
}
