package de.rico.editor.io;

import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import de.rico.adventure.entity.AbstractEntity;
import de.rico.adventure.entity.character.NonPlayerCharacter;
import de.rico.adventure.entity.hotspot.AbstractInteractiveHotspot;
import de.rico.adventure.entity.hotspot.CubicalHotpot;
import de.rico.adventure.entity.hotspot.SphericalHotspot;
import de.rico.adventure.entity.item.Item;
import de.rico.adventure.entity.prop.Prop;
import de.rico.adventure.main.ShootOut3DMain;
import de.rico.adventure.script.trigger.AbstractTrigger;
import de.rico.adventure.script.trigger.CircleTriggerArea;
import de.rico.adventure.script.trigger.RectTriggerArea;
import de.rico.adventure.structure.EntryPoint;
import de.rico.adventure.structure.Room;
import de.rico.editor.WorldEditor;
import de.rico.editor.gui.EditorMenu;
import de.rico.engine.camera.CameraPerspective;
import de.rico.engine.effect.light.LightIface;
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.custom.Explosion;
import de.rico.engine.effect.particle.custom.Fire;
import de.rico.engine.effect.particle.custom.Smoke;
import de.rico.engine.effect.sound.Mp3Sound;
import de.rico.engine.effect.water.WaterLayer;
import de.rico.engine.enums.BoundingVolume;
import de.rico.engine.geometry.GroundPlane;
import de.rico.engine.geometry.Point3D;
import de.rico.engine.geometry.SkyBox;
import de.rico.engine.geometry.Terrain;
import de.rico.engine.geometry.collision.BoundingBox;
import de.rico.engine.geometry.collision.BoundingCylinder;
import de.rico.engine.geometry.collision.BoundingSphere;
import de.rico.engine.geometry.modelloader.Animation;
import de.rico.engine.geometry.modelloader.md2.ModelMD2;
import de.rico.engine.geometry.modelloader.ms3d.ModelMS3DAscii;
import de.rico.engine.geometry.modelloader.threeds.Model3DS;
import de.rico.engine.material.Color4f;
import de.rico.engine.path.GridNode;

/**
 * This class can be used to export the current room to
 * a file and to import a room from a file.
 * @author Frank Bruns
 *
 */
public class RoomIO
{
	private Room room = null;
	private boolean forEditor = true;

	/**
	 * Creates a new RoomIO object with the specified (empty) room
	 * object that is going to be filled or saved.
	 * @param room room object to operate on
	 * @param forEditor is room going to be loaded into the editor? --> true=yes, false=no
	 */
	public RoomIO(Room room, boolean forEditor)
	{
		this.room = room;
		this.forEditor = forEditor;
	}

	/**
	 * Subtracts the base folder path from the specified absolute
	 * file path to return a path relative to the base folder.
	 * @param absPath absolute file path
	 * @return path relative to the base folder path
	 */
	private String getRelativeFilePath(String absPath)
	{
		String base = WorldEditor.getBaseFolder();

		// subtract the base string from the absPath
		if(base != null && absPath.startsWith(base))
		{
			// replace '\' with '/' for better relative pathes
			absPath = absPath.replace('\\', '/');

			int length = base.length();
			absPath = "./"+absPath.substring(length+1);
		}

		return absPath;
	}

	/**
	 * Loads the room information from the specified file.
	 * @param path filepath + name of the file to load from
	 */
	public void importRoom(String path)
	{
		// interrupt render thread (this thread) for a short time to allow
		// the resource loading display thread to start safely
		try
		{
			Thread.sleep(500);
		}
		catch (InterruptedException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		Document document = null;
		SAXReader reader = new SAXReader();
		reader.setEncoding("ISO-8859-1");
        try
		{
			document = reader.read(path);
			ShootOut3DMain.getInstance().setLoadingInfoText("Parsing room file: "+path);
		}
		catch (DocumentException e)
		{
			System.out.println("Error while parsing file.");
			e.printStackTrace();
		}

		// read the information from the loaded file
		Node roomScriptIdNode = document.selectSingleNode("//room/@scriptid");
		String roomScriptId = "give me unique script id!";
		if(roomScriptIdNode != null)
			roomScriptId = document.selectSingleNode("//room/@scriptid").getText();

		this.room.setScriptId(roomScriptId);

		Node roomScriptFileNode = document.selectSingleNode("//room/@scriptfile");
		String roomScriptFile = "assign an existing script file!";
		if(roomScriptFileNode != null)
			roomScriptFile = document.selectSingleNode("//room/@scriptfile").getText();

		this.room.setScriptFile(roomScriptFile);

		// read in light data, if available
		Node lightsNode = document.selectSingleNode("//room/lights");
		if(lightsNode != null)
		{
			// load directional light information
			this.loadDirectionalLights(lightsNode);

			// load positional light information
			this.loadPositionalLights(lightsNode);

			// load spot light information
			this.loadSpotLights(lightsNode);
		}

		// load camera perspectives
		this.loadCameraPerspectives(document);

		// load interactive hotspots
		this.loadHotspots(document);

		// load the triggers
		this.loadTriggers(document);


		// read in the sky box data at first, if available
		this.loadSkyBox(document);

		// insert the ground , if available
		this.loadGround(document);


		// read in the other meshes, if available

		// start with reading in the items
		Node itemsNode = document.selectSingleNode("//room/items");
		if(itemsNode != null)
		{
			List<Node> itemNodes = itemsNode.selectNodes("item");
			for(Node itemNode : itemNodes)
			{
				// insert 3DS model
				this.loadModel3ds(itemNode, "item");

				// insert MD2 model
				this.loadModelMd2(itemNode, "item");

				// insert Milkshape 3D Ascii model
				this.loadMs3dAscii(itemNode, "item");
			}
		}

		// reading in characters
		Node charsNode = document.selectSingleNode("//room/characters");
		if(charsNode != null)
		{
			List<Node> charNodes = charsNode.selectNodes("character");
			for(Node charNode : charNodes)
			{
				// insert 3DS model
				this.loadModel3ds(charNode, "character");

				// insert MD2 model
				this.loadModelMd2(charNode, "character");

				// insert Milkshape 3D Ascii model
				this.loadMs3dAscii(charNode, "character");
			}
		}

		// finally read in the props
		Node propsNode = document.selectSingleNode("//room/props");
		if(propsNode != null)
		{
			// load water layers
			this.loadWaterLayers(propsNode);

			// load particle effects, if available
			this.loadParticleEffects(propsNode);

			// load props based on 3d models
			List<Node> propNodes = propsNode.selectNodes("prop");
			for(Node propNode : propNodes)
			{
				// insert 3DS model
				this.loadModel3ds(propNode, "prop");

				// insert MD2 model
				this.loadModelMd2(propNode, "prop");

				// insert Milkshape 3D Ascii model
				this.loadMs3dAscii(propNode, "prop");
			}
		}

		// read in the entry points of the room
		Node entryPointsNode = document.selectSingleNode("//room/entrypoints");

		List<Node> entryPointNodes = new ArrayList<Node>();
		if(entryPointsNode != null)
			entryPointNodes = entryPointsNode.selectNodes("entrypoint");

		for(Node epNode : entryPointNodes)
		{
			String scriptId = epNode.selectSingleNode("@scriptid").getText();

			// get position and rotation data
			String x = epNode.selectSingleNode("position/@x").getText();
			String y = epNode.selectSingleNode("position/@y").getText();
			String z = epNode.selectSingleNode("position/@z").getText();
			Point3D pos = new Point3D(new Float(x),new Float(y), new Float(z));

			String rotX = epNode.selectSingleNode("rotation/@x").getText();
			String rotY = epNode.selectSingleNode("rotation/@y").getText();
			String rotZ = epNode.selectSingleNode("rotation/@z").getText();
			Point3D rot = new Point3D(new Float(rotX),new Float(rotY), new Float(rotZ));

			this.room.addEntryPoint(new EntryPoint(scriptId,pos,rot));
		}

		// call the room's update() method once, because the
		// add operations only become effective in the room's
		// update method
		this.room.update(0L);



		// set the shadow ref light, if available
		if(lightsNode != null)
		{
			Node shadowRefNode = lightsNode.selectSingleNode("@shadowref");
			if(shadowRefNode != null)
			{
				String shadowRef = shadowRefNode.getText();
				PositionalLight ref = (PositionalLight)this.room.getLight(new Integer(shadowRef));
				this.room.setShadowReferenceLight(ref);
			}
		}

		// run garbage collection after the scene has been loaded
		System.gc();
	}

	/**
	 * Helper method to load particle effects from the room file
	 * @param propsNode dom4j node
	 */
	private void loadParticleEffects(Node propsNode)
	{
		List<Node> particleNodes = propsNode.selectNodes("particle");
		for(Node particleNode : particleNodes)
		{
			//String scriptId = particleNode.selectSingleNode("@scriptid").getText();
			String type = particleNode.selectSingleNode("@type").getText();

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: particle effect - "+type);

			String size = particleNode.selectSingleNode("particlesize").getText();
			String number = particleNode.selectSingleNode("numberparticles").getText();
			String maxAge = particleNode.selectSingleNode("maxage").getText();

			String interval = null;
			// explosions don't need an interval
			if("explosion".equals(type) == false)
				interval = particleNode.selectSingleNode("interval").getText();

			String tex = particleNode.selectSingleNode("texture").getText();

			String x = particleNode.selectSingleNode("position/@x").getText();
			String y = particleNode.selectSingleNode("position/@y").getText();
			String z = particleNode.selectSingleNode("position/@z").getText();

			if("smoke".equals(type))
			{
				Smoke smoke = new Smoke(
						new Float(x),
						new Float(y),
						new Float(z),
						new Float(size),
						new Integer(number),
						new Integer(maxAge),
						new Integer(interval));

				smoke.setTexture(tex);

				this.room.addParticleEffect(smoke);
			}
			if("fire".equals(type))
			{
				Fire fire = new Fire(
						new Float(x),
						new Float(y),
						new Float(z),
						new Float(size),
						new Integer(number),
						new Integer(maxAge),
						new Integer(interval));

				fire.setTexture(tex);

				this.room.addParticleEffect(fire);
			}
			if("explosion".equals(type))
			{
				Explosion explosion = new Explosion(
						new Float(x),
						new Float(y),
						new Float(z),
						new Float(size),
						new Integer(number),
						new Integer(maxAge),
						new Boolean(particleNode.selectSingleNode("@loop").getText()));

				explosion.setTexture(tex);

				this.room.addParticleEffect(explosion);
			}
		}
	}


	/**
	 * Saves the room information to the specified file.
	 * @param path filepath + name for the saved file
	 */
	public void exportRoom(String path)
	{
		System.out.println("Trying to create file");
		Document document = DocumentHelper.createDocument();

		// create the xml file hierarchy and its content
		Element root = document.addElement("room");
		{
			// save the rooms script id and link to beanshell file
			root.addAttribute("scriptid", this.room.getScriptId());
			
			if(this.room.getScriptFile() != null)
				root.addAttribute("scriptfile", this.getRelativeFilePath(this.room.getScriptFile()));

			// save lights, if available
			if(this.room.getLights().size() > 0)
			{
				Element lightNode = root.addElement("lights");

				// save shadow ref light, if available
				if(this.room.getShadowReferenceLight() != null)
				{
					lightNode.addAttribute(
							"shadowref",
							""+this.room.getShadowReferenceLight().getLightNr());
				}

				for(LightIface light : this.room.getLights())
				{
					if(light instanceof DirectionalLight)
					{
						this.saveDirectionalLight(lightNode,light);
						continue; // leave iteration round
					}

					// spot light stands before positional light,
					// because otherwise every spot light would be
					// recognized as a positional light and the loop
					// would skip further processing in this round
					if(light instanceof SpotLight)
					{
						this.saveSpotLight(lightNode, light);
						continue; // leave iteration round
					}

					if(light instanceof PositionalLight)
					{
						this.savePositionalLight(lightNode, light);

					}
				}
			}

			// save camera perspective, if available
			if(this.room.getCameraPerspectives().size() > 0)
			{
				Element camNode = root.addElement("camera");

				this.saveCameraPerspectives(camNode);
			}

			// save hotspot data, if available
			if(this.room.getHotspots().size() > 0)
			{
				Element hotspotsNode = root.addElement("hotspots");

				this.saveHotspots(hotspotsNode);
			}

			// save trigger data, if available
			if(this.room.getTriggers().size() > 0)
			{
				Element triggersNode = root.addElement("triggers");

				this.saveTriggers(triggersNode);
			}


			// save mesh data
			if(this.room.getMeshes().getMeshes().size() > 0)
			{
				// save props first
				Element propsNode = root.addElement("props");
				for(Prop prop : this.room.getProps())
				{
					if(prop.getMesh() instanceof SkyBox)
					{
						// save skybox data
						this.saveSkyBox(propsNode);
					}
					if(prop.getMesh() instanceof Model3DS)
					{
						this.saveModel3ds(propsNode, prop);
					}
					if(prop.getMesh() instanceof ModelMD2)
					{
						this.saveModelMd2(propsNode, prop);
					}
					if(prop.getMesh() instanceof ModelMS3DAscii)
					{
						this.saveModelMs3dAscii(propsNode, prop);
					}
					if(prop.getMesh() instanceof Terrain)
					{
						this.saveHMTerrain(propsNode, prop);
					}
					if(prop.getMesh() instanceof GroundPlane)
					{
						this.saveGroundPlane(propsNode, prop);
					}
					if(prop.getMesh() instanceof WaterLayer)
					{
						this.saveWaterLayer(propsNode, prop);
					}
				}

				// save particle effects
				this.saveParticleEffects(propsNode);


				// save items
				if(this.room.getItems().size() > 0)
				{
					Element itemsNode = root.addElement("items");
					for(Item item : this.room.getItems())
					{
						if(item.getMesh() instanceof Model3DS)
						{
							this.saveModel3ds(itemsNode, item);
						}
						if(item.getMesh() instanceof ModelMD2)
						{
							this.saveModelMd2(itemsNode, item);
						}
						if(item.getMesh() instanceof ModelMS3DAscii)
						{
							this.saveModelMs3dAscii(itemsNode, item);
						}
					}

				}

				// save characters
				if(this.room.getCharacters().size() > 0)
				{
					Element charsNode = root.addElement("characters");
					for(NonPlayerCharacter npc : this.room.getCharacters())
					{
						if(npc.getMesh() instanceof Model3DS)
						{
							this.saveModel3ds(charsNode, npc);
						}
						if(npc.getMesh() instanceof ModelMD2)
						{
							this.saveModelMd2(charsNode, npc);
						}
						if(npc.getMesh() instanceof ModelMS3DAscii)
						{
							this.saveModelMs3dAscii(charsNode, npc);
						}
					}
				}
			}

			// save entry points
			if(this.room.getEntryPoints().size() > 0)
			{
				this.saveEntryPoints(root);
			}
		}


		// save the xml tree into a file
        XMLWriter writer = null;
		try
		{
			OutputFormat format = OutputFormat.createPrettyPrint();
			format.setEncoding("ISO-8859-1");
			writer = new XMLWriter(
					new FileWriter(path),format);
			writer.write(document);
	        writer.close();
	        System.out.println("XML file successfully created.");
		}
		catch (IOException e)
		{
			System.out.println("Error while generating xml file");
			e.printStackTrace();
		}
	}

	/**
	 * Helper method to save the room's particle effects to the
	 * XML file.
	 * @param propsNode dom4j node
	 */
	private void saveParticleEffects(Element propsNode)
	{
		for(AbstractParticleEffect effect : this.room.getParticleEffects())
		{
			Element effectNode = propsNode.addElement("particle");

			if(effect instanceof Smoke)
				effectNode.addAttribute("type", "smoke");
			else if(effect instanceof Fire)
				effectNode.addAttribute("type", "fire");
			else if(effect instanceof Explosion)
			{
				Explosion e = (Explosion)effect;
				effectNode.addAttribute("type", "explosion");
				effectNode.addAttribute("loop", ""+e.isLooping());
			}

			Element position = effectNode.addElement("position");
			position.addAttribute("x", Float.toString(effect.getPosition().x));
			position.addAttribute("y", Float.toString(effect.getPosition().y));
			position.addAttribute("z", Float.toString(effect.getPosition().z));

			Element size = effectNode.addElement("particlesize");
			size.setText(""+effect.getParticleSize());

			Element maxNumber = effectNode.addElement("numberparticles");
			maxNumber.setText(""+effect.getMaxParticleCount());

			// explosions don't need an inteval
			if(effect instanceof Explosion == false)
			{
				Element interval = effectNode.addElement("interval");
				interval.setText(""+effect.getCreationInterval());
			}

			Element maxAge = effectNode.addElement("maxage");
			maxAge.setText(""+effect.getMaxPartAge());

			if(effect.getTexturePath() != null)
			{
				Element tex = effectNode.addElement("texture");
				tex.setText(this.getRelativeFilePath(effect.getTexturePath()));
			}
		}
	}

	/**
	 * Helper method to save the entry points of a room.
	 * @param root document root
	 */
	private void saveEntryPoints(Element root)
	{
		Element entryPointsNode = root.addElement("entrypoints");
		for(EntryPoint ep : this.room.getEntryPoints())
		{
			Element epNode = entryPointsNode.addElement("entrypoint");
			epNode.addAttribute("scriptid", ep.getScriptId());

			// position
			Element position = epNode.addElement("position");
			position.addAttribute("x", Float.toString(ep.getPosition().x));
			position.addAttribute("y", Float.toString(ep.getPosition().y));
			position.addAttribute("z", Float.toString(ep.getPosition().z));

			// rotation
			Element rotation = epNode.addElement("rotation");
			rotation.addAttribute("x", Float.toString(ep.getRotation().x));
			rotation.addAttribute("y", Float.toString(ep.getRotation().y));
			rotation.addAttribute("z", Float.toString(ep.getRotation().z));
		}
	}


	// special load methods
	/**
	 * Helper method to load the Milkshape3D Ascii data from file
	 * @param document the dom4j document object
	 * @param type entity type, "item" or "character"
	 */
	private void loadMs3dAscii(Node meshNode, String type)
	{
		if(meshNode.selectSingleNode("@type").getText().equals("ms3dascii"))
		{
			String volume = meshNode.selectSingleNode("@volume").getText();
			String shadow = meshNode.selectSingleNode("@shadow").getText();
			String internalId = meshNode.selectSingleNode("@id").getText();
			String file = meshNode.selectSingleNode("file").getText();
			String tex0 = null;
			String tex1 = null;
			String mipmap = "false";
			String rotate = "false";
			String flip = "false";

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: Milkshape3D Ascii model - "+internalId);

			// find texture nodes, according to their texture unit
			List<Node> texNodes = meshNode.selectNodes("texture");
			for(Node texNode : texNodes)
			{
				if(texNode.selectSingleNode("@unit").getText().equals("0"))
				{
					tex0 = texNode.getText();
					mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
					rotate = texNode.selectSingleNode("@rotate").getText();
					flip = texNode.selectSingleNode("@flip").getText();
				}
				else if(texNode.selectSingleNode("@unit").getText().equals("1"))
				{
					tex1 = texNode.getText();
					mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
					rotate = texNode.selectSingleNode("@rotate").getText();
					flip = texNode.selectSingleNode("@flip").getText();
				}
			}

			// get position, rotation and scale data
			String x = meshNode.selectSingleNode("position/@x").getText();
			String y = meshNode.selectSingleNode("position/@y").getText();
			String z = meshNode.selectSingleNode("position/@z").getText();

			String rotX = meshNode.selectSingleNode("rotation/@x").getText();
			String rotY = meshNode.selectSingleNode("rotation/@y").getText();
			String rotZ = meshNode.selectSingleNode("rotation/@z").getText();

			String scaleX = meshNode.selectSingleNode("scale/@x").getText();
			String scaleY = meshNode.selectSingleNode("scale/@y").getText();
			String scaleZ = meshNode.selectSingleNode("scale/@z").getText();

			// create the model object with the read in data
			//TODO make frame interval accessible in the editor
			ModelMS3DAscii model = null;
			if(volume.equals("sphere"))
				model = new ModelMS3DAscii(BoundingVolume.SPHERE);
			else if(volume.equals("box"))
				model = new ModelMS3DAscii(BoundingVolume.BOX);
			else if(volume.equals("cylinder"))
				model = new ModelMS3DAscii(BoundingVolume.CYLINDER);

			model.load(file, 1f);
			if(tex0 != null)
				model.setTexUnit0(
						tex0,
						Boolean.parseBoolean(mipmap),
						Boolean.parseBoolean(rotate),
						Boolean.parseBoolean(flip));
			if(tex1 != null)
				model.setTexUnit1(
						tex1,
						Boolean.parseBoolean(mipmap),
						Boolean.parseBoolean(rotate),
						Boolean.parseBoolean(flip));
			model.setName(internalId);
			model.setPosition(
					Float.parseFloat(x),
					Float.parseFloat(y),
					Float.parseFloat(z));
			model.setRotation(
					Float.parseFloat(rotX),
					Float.parseFloat(rotY),
					Float.parseFloat(rotZ));
			model.scale(
					Float.parseFloat(scaleX),
					Float.parseFloat(scaleY),
					Float.parseFloat(scaleZ));
			model.setShadowEnabled(Boolean.parseBoolean(shadow));

			// add animations
			List<Node> animNodes = meshNode.selectNodes("animation");
			for(Node animNode : animNodes)
			{
				String animName = animNode.selectSingleNode("@name").getText();
				String start = animNode.selectSingleNode("@start").getText();
				String end = animNode.selectSingleNode("@end").getText();
				String durPerFrame = animNode.selectSingleNode("@timeperframe").getText();
				Node soundNode = animNode.selectSingleNode("@sound");
				Node loopSoundNode = animNode.selectSingleNode("@loopsound");

				// add animation with sound file
				if(soundNode != null)
				{
					String sound = animNode.selectSingleNode("@sound").getText();

					boolean loop = false;
					if(loopSoundNode != null) loop = new Boolean(loopSoundNode.getText());

					model.addAnimation(
							new Animation(animName,
							new Integer(start),
							new Integer(end),
							new Integer(durPerFrame),new Mp3Sound(sound, loop)));
				}
				else // add animation without sound file
				{
					model.addAnimation(
							new Animation(animName,
							new Integer(start),
							new Integer(end),
							new Integer(durPerFrame)));
				}

				// sets the last added animation
				model.setAnimation(animName);
			}


			// create the according item or charcter with the model
			if(type.equals("item"))
			{
				String name = meshNode.selectSingleNode("name").getText();
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();

				String inventImgPath = null;
				if(meshNode.selectSingleNode("image") != null)
					inventImgPath = meshNode.selectSingleNode("image").getText();

				String pickable = meshNode.selectSingleNode("pickable").getText();

				Item item = new Item(model);
				item.setScreenName(name);
				item.setScriptId(scriptId);
				if(inventImgPath != null) item.setInventoryImage(inventImgPath);
				item.setCanBePickedUp(Boolean.parseBoolean(pickable));
				this.room.addItem(item);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (item)");
			}
			if(type.equals("character"))
			{
				String name = meshNode.selectSingleNode("name").getText();
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();

				NonPlayerCharacter npc = new NonPlayerCharacter(model);
				npc.setScreenName(name);
				npc.setScriptId(scriptId);
				this.room.addCharacter(npc);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (npc)");
			}
			if(type.equals("prop"))
			{
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();
				Prop prop = new Prop(model);
				prop.setScriptId(scriptId);
				this.room.addProp(prop);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (prop)");
			}
		}
	}

	/**
	 * Helper method to load the MD2 model data from file
	 * @param document the dom4j document object
	 * @param type entity type, "item" or "character"
	 */
	private void loadModelMd2(Node meshNode, String type)
	{
		if(meshNode.selectSingleNode("@type").getText().equals("md2"))
		{
			String smooth = meshNode.selectSingleNode("@smooth").getText();
			String volume = meshNode.selectSingleNode("@volume").getText();
			String shadow = meshNode.selectSingleNode("@shadow").getText();
			String internalId = meshNode.selectSingleNode("@id").getText();
			String file = meshNode.selectSingleNode("file").getText();
			String tex0 = null;
			String tex1 = null;
			String mipmap = "false";
			String rotate = "false";
			String flip = "false";

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: MD2 model - "+internalId);

			// find texture nodes, according to their texture unit
			List<Node> texNodes = meshNode.selectNodes("texture");
			for(Node texNode : texNodes)
			{
				if(texNode.selectSingleNode("@unit").getText().equals("0"))
				{
					tex0 = texNode.getText();
					mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
					rotate = texNode.selectSingleNode("@rotate").getText();
					flip = texNode.selectSingleNode("@flip").getText();
				}
				else if(texNode.selectSingleNode("@unit").getText().equals("1"))
				{
					tex1 = texNode.getText();
					mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
					rotate = texNode.selectSingleNode("@rotate").getText();
					flip = texNode.selectSingleNode("@flip").getText();
				}
			}

			// get position, rotation and scale data
			String x = meshNode.selectSingleNode("position/@x").getText();
			String y = meshNode.selectSingleNode("position/@y").getText();
			String z = meshNode.selectSingleNode("position/@z").getText();

			String rotX = meshNode.selectSingleNode("rotation/@x").getText();
			String rotY = meshNode.selectSingleNode("rotation/@y").getText();
			String rotZ = meshNode.selectSingleNode("rotation/@z").getText();

			String scaleX = meshNode.selectSingleNode("scale/@x").getText();
			String scaleY = meshNode.selectSingleNode("scale/@y").getText();
			String scaleZ = meshNode.selectSingleNode("scale/@z").getText();

			// create the model object with the read in data
			//TODO make frame interval accessible in the editor
			ModelMD2 model = null;
			if(volume.equals("sphere"))
				model = new ModelMD2(Boolean.parseBoolean(smooth),BoundingVolume.SPHERE);
			else if(volume.equals("box"))
				model = new ModelMD2(Boolean.parseBoolean(smooth),BoundingVolume.BOX);
			else if(volume.equals("cylinder"))
				model = new ModelMD2(Boolean.parseBoolean(smooth),BoundingVolume.CYLINDER);

			model.load(file, 1f);
			if(tex0 != null)
				model.setTexUnit0(
						tex0,
						Boolean.parseBoolean(mipmap),
						Boolean.parseBoolean(rotate),
						Boolean.parseBoolean(flip));
			if(tex1 != null)
				model.setTexUnit1(
						tex1,
						Boolean.parseBoolean(mipmap),
						Boolean.parseBoolean(rotate),
						Boolean.parseBoolean(flip));
			model.setName(internalId);
			model.setPosition(
					Float.parseFloat(x),
					Float.parseFloat(y),
					Float.parseFloat(z));
			model.setRotation(
					Float.parseFloat(rotX),
					Float.parseFloat(rotY),
					Float.parseFloat(rotZ));
			model.scale(
					Float.parseFloat(scaleX),
					Float.parseFloat(scaleY),
					Float.parseFloat(scaleZ));
			model.setShadowEnabled(Boolean.parseBoolean(shadow));


			// add animations
			List<Node> animNodes = meshNode.selectNodes("animation");
			for(Node animNode : animNodes)
			{
				String animName = animNode.selectSingleNode("@name").getText();
				String start = animNode.selectSingleNode("@start").getText();
				String end = animNode.selectSingleNode("@end").getText();
				String durPerFrame = animNode.selectSingleNode("@timeperframe").getText();
				Node soundNode = animNode.selectSingleNode("@sound");
				Node loopSoundNode = animNode.selectSingleNode("@loopsound");

				// add animation with sound file
				if(soundNode != null)
				{
					String sound = animNode.selectSingleNode("@sound").getText();

					boolean loop = false;
					if(loopSoundNode != null) loop = new Boolean(loopSoundNode.getText());

					model.addAnimation(
							new Animation(animName,
							new Integer(start),
							new Integer(end),
							new Integer(durPerFrame),new Mp3Sound(sound, loop)));
				}
				else // add animation without sound file
				{
					model.addAnimation(
							new Animation(animName,
							new Integer(start),
							new Integer(end),
							new Integer(durPerFrame)));
				}

				// sets the last added animation
				model.setAnimation(animName);
			}

			// create the according item or charcter with the model
			if(type.equals("item"))
			{
				String name = meshNode.selectSingleNode("name").getText();
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();

				String inventImgPath = null;
				if(meshNode.selectSingleNode("image") != null)
					inventImgPath = meshNode.selectSingleNode("image").getText();

				String pickable = meshNode.selectSingleNode("pickable").getText();

				Item item = new Item(model);
				item.setScreenName(name);
				item.setScriptId(scriptId);
				if(inventImgPath != null) item.setInventoryImage(inventImgPath);
				item.setCanBePickedUp(Boolean.parseBoolean(pickable));
				this.room.addItem(item);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (item)");
			}
			if(type.equals("character"))
			{
				String name = meshNode.selectSingleNode("name").getText();
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();

				NonPlayerCharacter npc = new NonPlayerCharacter(model);
				npc.setScreenName(name);
				npc.setScriptId(scriptId);
				this.room.addCharacter(npc);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (npc)");
			}
			if(type.equals("prop"))
			{
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();
				Prop prop = new Prop(model);
				prop.setScriptId(scriptId);
				this.room.addProp(prop);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (prop)");
			}
		}
	}

	/**
	 * Helper method to load the water layers from the XML file.
	 * @param propsNode dom4j props node
	 */
	private void loadWaterLayers(Node propsNode)
	{
		List<Node> waterNodes = propsNode.selectNodes("water");

		for(Node waterNode : waterNodes)
		{
			String meshName = waterNode.selectSingleNode("@id").getText();
			String width = waterNode.selectSingleNode("@width").getText();
			String length = waterNode.selectSingleNode("@length").getText();
			String moveX = waterNode.selectSingleNode("@movex").getText();
			String moveZ = waterNode.selectSingleNode("@movez").getText();
			String scriptId = waterNode.selectSingleNode("@scriptid").getText();
			String transparency = waterNode.selectSingleNode("@transparency").getText();

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: water layer - "+scriptId);

			String tex0 = null; // static texture
			String tex1 = null; // moveable texture

			// find texture nodes, according to their texture unit
			List<Node> texNodes = waterNode.selectNodes("texture");
			for(Node texNode : texNodes)
			{
				if(texNode.selectSingleNode("@unit").getText().equals("0"))
					tex0 = texNode.getText();
				else if(texNode.selectSingleNode("@unit").getText().equals("1"))
					tex1 = texNode.getText();
			}

			// get position, rotation and scale data
			String x = waterNode.selectSingleNode("position/@x").getText();
			String y = waterNode.selectSingleNode("position/@y").getText();
			String z = waterNode.selectSingleNode("position/@z").getText();
			Point3D pos = new Point3D(new Float(x),new Float(y),new Float(z));

			String rotX = waterNode.selectSingleNode("rotation/@x").getText();
			String rotY = waterNode.selectSingleNode("rotation/@y").getText();
			String rotZ = waterNode.selectSingleNode("rotation/@z").getText();

			String scaleX = waterNode.selectSingleNode("scale/@x").getText();
			String scaleY = waterNode.selectSingleNode("scale/@y").getText();
			String scaleZ = waterNode.selectSingleNode("scale/@z").getText();

			// create the water layer mesh
			WaterLayer water =
				new WaterLayer(
						pos,tex0,tex1,
						new Float(width),
						new Float(length),
						new Float(moveX),
						new Float(moveZ));

			water.setName(meshName);

			water.setRotation(
					new Float(rotX),
					new Float(rotY),
					new Float(rotZ));

			water.scale(
					new Float(scaleX),
					new Float(scaleY),
					new Float(scaleZ));

			water.setTransparency(new Float(transparency));

			Prop waterProp = new Prop(water);
			waterProp.setScriptId(scriptId);
			this.room.addProp(waterProp);
		}
	}

	/**
	 * Helper method to load the 3DS model data from file
	 * @param document the dom4j document object
	 * @param type entity type, "item" or "character"
	 */
	private void loadModel3ds(Node meshNode, String type)
	{
		if(meshNode.selectSingleNode("@type").getText().equals("3ds"))
		{
			String smooth = meshNode.selectSingleNode("@smooth").getText();
			String volume = meshNode.selectSingleNode("@volume").getText();
			String shadow = meshNode.selectSingleNode("@shadow").getText();
			String internalId = meshNode.selectSingleNode("@id").getText();
			String file = meshNode.selectSingleNode("file").getText();
			String tex0 = null;
			String tex1 = null;
			String mipmap = "false";
			String rotate = "false";
			String flip = "false";

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: 3DS model - "+internalId);

			// find texture nodes, according to their texture unit
			List<Node> texNodes = meshNode.selectNodes("texture");
			for(Node texNode : texNodes)
			{
				if(texNode.selectSingleNode("@unit").getText().equals("0"))
				{
					tex0 = texNode.getText();
					mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
					rotate = texNode.selectSingleNode("@rotate").getText();
					flip = texNode.selectSingleNode("@flip").getText();
				}
				else if(texNode.selectSingleNode("@unit").getText().equals("1"))
				{
					tex1 = texNode.getText();
					mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
					rotate = texNode.selectSingleNode("@rotate").getText();
					flip = texNode.selectSingleNode("@flip").getText();
				}
			}

			// get position, rotation and scale data
			String x = meshNode.selectSingleNode("position/@x").getText();
			String y = meshNode.selectSingleNode("position/@y").getText();
			String z = meshNode.selectSingleNode("position/@z").getText();

			String rotX = meshNode.selectSingleNode("rotation/@x").getText();
			String rotY = meshNode.selectSingleNode("rotation/@y").getText();
			String rotZ = meshNode.selectSingleNode("rotation/@z").getText();

			String scaleX = meshNode.selectSingleNode("scale/@x").getText();
			String scaleY = meshNode.selectSingleNode("scale/@y").getText();
			String scaleZ = meshNode.selectSingleNode("scale/@z").getText();

			// create the model object with the read in data
			Model3DS model = null;
			if(volume.equals("sphere"))
				model = new Model3DS(Boolean.parseBoolean(smooth),BoundingVolume.SPHERE);
			else if(volume.equals("box"))
				model = new Model3DS(Boolean.parseBoolean(smooth),BoundingVolume.BOX);
			else if(volume.equals("cylinder"))
				model = new Model3DS(Boolean.parseBoolean(smooth),BoundingVolume.CYLINDER);

			model.load(file, 1f);
			if(tex0 != null)
				model.setTexUnit0(
						tex0,
						Boolean.parseBoolean(mipmap),
						Boolean.parseBoolean(rotate),
						Boolean.parseBoolean(flip));
			if(tex1 != null)
				model.setTexUnit1(
						tex1,
						Boolean.parseBoolean(mipmap),
						Boolean.parseBoolean(rotate),
						Boolean.parseBoolean(flip));
			model.setName(internalId);
			model.setPosition(
					Float.parseFloat(x),
					Float.parseFloat(y),
					Float.parseFloat(z));
			model.setRotation(
					Float.parseFloat(rotX),
					Float.parseFloat(rotY),
					Float.parseFloat(rotZ));
			model.scale(
					Float.parseFloat(scaleX),
					Float.parseFloat(scaleY),
					Float.parseFloat(scaleZ));
			model.setShadowEnabled(Boolean.parseBoolean(shadow));

			// create the according item or charcter with the model
			if(type.equals("item"))
			{
				String name = meshNode.selectSingleNode("name").getText();
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();

				String inventImgPath = null;
				if(meshNode.selectSingleNode("image") != null)
					inventImgPath = meshNode.selectSingleNode("image").getText();

				String pickable = meshNode.selectSingleNode("pickable").getText();

				Item item = new Item(model);
				item.setScreenName(name);
				item.setScriptId(scriptId);
				if(inventImgPath != null) item.setInventoryImage(inventImgPath);
				item.setCanBePickedUp(Boolean.parseBoolean(pickable));
				this.room.addItem(item);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (item)");
			}
			if(type.equals("character"))
			{
				String name = meshNode.selectSingleNode("name").getText();
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();

				NonPlayerCharacter npc = new NonPlayerCharacter(model);
				npc.setScreenName(name);
				npc.setScriptId(scriptId);
				this.room.addCharacter(npc);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (npc)");
			}
			if(type.equals("prop"))
			{
				String scriptId = meshNode.selectSingleNode("@scriptid").getText();
				Prop prop = new Prop(model);
				prop.setScriptId(scriptId);
				this.room.addProp(prop);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (prop)");
			}
		}
	}

	/**
	 * Helper method to load the ground data from file
	 * @param document the dom4j document object
	 */
	private void loadGround(Document document)
	{
		Node groundNode = document.selectSingleNode("//room/props/ground");
		if(groundNode != null)
		{
			// insert the heightmap terrain ground
			if(groundNode.selectSingleNode("@type").getText().equals("heightmap"))
			{
				ShootOut3DMain.getInstance().setLoadingInfoText("Loading: heightmap terrain");

				boolean smooth = Boolean.parseBoolean(groundNode.selectSingleNode("@smooth").getText());
				String internalId = groundNode.selectSingleNode("@id").getText();
				int resolution = Integer.parseInt(groundNode.selectSingleNode("@resolution").getText());
				String file = groundNode.selectSingleNode("file").getText();
				String tex0 = null;
				String mipmap = "false";
				String rotate = "false";
				String flip = "false";

				// find texture nodes, according to their texture unit
				List<Node> texNodes = groundNode.selectNodes("texture");
				for(Node texNode : texNodes)
				{
					if(texNode.selectSingleNode("@unit").getText().equals("0"))
					{
						tex0 = texNode.getText();
						mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
						rotate = texNode.selectSingleNode("@rotate").getText();
						flip = texNode.selectSingleNode("@flip").getText();
					}
				}

				// get position, rotation and scale data
				String x = groundNode.selectSingleNode("position/@x").getText();
				String y = groundNode.selectSingleNode("position/@y").getText();
				String z = groundNode.selectSingleNode("position/@z").getText();

				String rotX = groundNode.selectSingleNode("rotation/@x").getText();
				String rotY = groundNode.selectSingleNode("rotation/@y").getText();
				String rotZ = groundNode.selectSingleNode("rotation/@z").getText();

				String scaleX = groundNode.selectSingleNode("scale/@x").getText();
				String scaleY = groundNode.selectSingleNode("scale/@y").getText();
				String scaleZ = groundNode.selectSingleNode("scale/@z").getText();

				Node gridNode = groundNode.selectSingleNode("pathgrid");
				int gridNodeSize = Integer.parseInt(gridNode.selectSingleNode("@nodesize").getText());

				List<Node> walkableNodes = gridNode.selectNodes("walkable");

				// create the model object with the read in data
				Terrain model = new Terrain(file,tex0,resolution,smooth);
				if(tex0 != null)
				{
					model.setTexUnit0(
							tex0,
							Boolean.parseBoolean(mipmap),
							Boolean.parseBoolean(rotate),
							Boolean.parseBoolean(flip));
				}

				model.setName(internalId);
				model.setPosition(
						Float.parseFloat(x),
						Float.parseFloat(y),
						Float.parseFloat(z));
				model.setRotation(
						Float.parseFloat(rotX),
						Float.parseFloat(rotY),
						Float.parseFloat(rotZ));
				model.scale(
						Float.parseFloat(scaleX),
						Float.parseFloat(scaleY),
						Float.parseFloat(scaleZ));

				// init search grid and set walkable nodes
				model.initSearchGrid(gridNodeSize);
				for(Node walkableNode : walkableNodes)
				{
					int id = Integer.parseInt(walkableNode.selectSingleNode("@nodeid").getText());
					model.getSearchGrid().getGridNode(id).setWalkable(true);
				}

				String scriptId = groundNode.selectSingleNode("@scriptid").getText();
				Prop prop = new Prop(model);
				prop.setScriptId(scriptId);
				this.room.addProp(prop);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId + " (prop)");
			}

			// insert the ground plane type
			if(groundNode.selectSingleNode("@type").getText().equals("plane"))
			{
				ShootOut3DMain.getInstance().setLoadingInfoText("Loading: ground plane");

				String internalId = groundNode.selectSingleNode("@id").getText();
				int size = Integer.parseInt(groundNode.selectSingleNode("@size").getText());
				float texRep = Float.parseFloat(groundNode.selectSingleNode("@texrepeat").getText());

				String tex0 = null;
				String mipmap = "false";
				String rotate = "false";
				String flip = "false";

				// find texture nodes, according to their texture unit
				List<Node> texNodes = groundNode.selectNodes("texture");
				for(Node texNode : texNodes)
				{
					if(texNode.selectSingleNode("@unit").getText().equals("0"))
					{
						tex0 = texNode.getText();
						mipmap = rotate = texNode.selectSingleNode("@mipmap").getText();
						rotate = texNode.selectSingleNode("@rotate").getText();
						flip = texNode.selectSingleNode("@flip").getText();
					}
				}

				// get position, rotation and scale data
				String x = groundNode.selectSingleNode("position/@x").getText();
				String y = groundNode.selectSingleNode("position/@y").getText();
				String z = groundNode.selectSingleNode("position/@z").getText();

				String rotX = groundNode.selectSingleNode("rotation/@x").getText();
				String rotY = groundNode.selectSingleNode("rotation/@y").getText();
				String rotZ = groundNode.selectSingleNode("rotation/@z").getText();

				String scaleX = groundNode.selectSingleNode("scale/@x").getText();
				String scaleY = groundNode.selectSingleNode("scale/@y").getText();
				String scaleZ = groundNode.selectSingleNode("scale/@z").getText();

				Node gridNode = groundNode.selectSingleNode("pathgrid");
				int gridNodeSize = Integer.parseInt(gridNode.selectSingleNode("@nodesize").getText());

				List<Node> walkableNodes = gridNode.selectNodes("walkable");


				// create the model object with the read in data
				GroundPlane model = new GroundPlane(size,texRep);

				if(tex0 != null)
				{
					model.setTexUnit0(
							tex0,
							Boolean.parseBoolean(mipmap),
							Boolean.parseBoolean(rotate),
							Boolean.parseBoolean(flip));
				}

				model.setName(internalId);
				model.setPosition(
						Float.parseFloat(x),
						Float.parseFloat(y),
						Float.parseFloat(z));
				model.setRotation(
						Float.parseFloat(rotX),
						Float.parseFloat(rotY),
						Float.parseFloat(rotZ));
				model.scale(
						Float.parseFloat(scaleX),
						Float.parseFloat(scaleY),
						Float.parseFloat(scaleZ));

				// init search grid and set walkable nodes
				model.initSearchGrid(gridNodeSize);
				for(Node walkableNode : walkableNodes)
				{
					int id = Integer.parseInt(walkableNode.selectSingleNode("@nodeid").getText());
					model.getSearchGrid().getGridNode(id).setWalkable(true);
				}

				String scriptId = groundNode.selectSingleNode("@scriptid").getText();
				Prop prop = new Prop(model);
				prop.setScriptId(scriptId);
				this.room.addProp(prop);

				if(this.forEditor == true)
					EditorMenu.addEntityToListMenu(internalId +" (prop)");
			}
		}
	}

	/**
	 * Helper method to load the sky box data from file
	 * @param document the dom4j document object
	 */
	private void loadSkyBox(Document document)
	{
		Node sky = document.selectSingleNode("//room/props/skybox");
		if(sky != null)
		{
			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: skybox of room - "+this.room.getScriptId());

			String internalId = sky.selectSingleNode("@id").getText();

			String posX = null;
			String negX = null;
			String posY = null;
			String negY = null;
			String posZ = null;
			String negZ = null;
			if(sky.selectSingleNode("textures/posx") != null)
				posX = sky.selectSingleNode("textures/posx").getText();
			if(sky.selectSingleNode("textures/negx") != null)
				negX = sky.selectSingleNode("textures/negx").getText();
			if(sky.selectSingleNode("textures/posy") != null)
				posY = sky.selectSingleNode("textures/posy").getText();
			if(sky.selectSingleNode("textures/negy") != null)
				negY = sky.selectSingleNode("textures/negy").getText();
			if(sky.selectSingleNode("textures/posz") != null)
				posZ = sky.selectSingleNode("textures/posz").getText();
			if(sky.selectSingleNode("textures/negz") != null)
				negZ = sky.selectSingleNode("textures/negz").getText();

			// create skybox object
			SkyBox skyBox = new SkyBox(5f,5f,5f);
			skyBox.setName(internalId);
			if(posZ != null) skyBox.setTextureSouth(posZ);
			if(negZ != null) skyBox.setTextureNorth(negZ);
			if(posX != null) skyBox.setTextureEast(posX);
			if(negX != null) skyBox.setTextureWest(negX);
			if(posY != null) skyBox.setTextureTop(posY);
			if(negY != null) skyBox.setTextureBottom(negY);
			skyBox.setName("sky box");
			skyBox.init();
			skyBox.setPosY(-2.5f);
			this.room.addProp(new Prop(skyBox));
		}
	}

	/**
	 * Helper method to load the spot lights' data from file
	 * @param lightsNode dom4j lights node
	 */
	private void loadSpotLights(Node lightsNode)
	{
		List<Node> spotLightNodes = lightsNode.selectNodes("spot");
		for(Node spot : spotLightNodes)
		{
			// light number
			String nr = spot.selectSingleNode("@nr").getText();

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: spot light "+nr+" of room - "+this.room.getScriptId());

			// position
			String x = spot.selectSingleNode("position/@x").getText();
			String y = spot.selectSingleNode("position/@y").getText();
			String z = spot.selectSingleNode("position/@z").getText();

			// rotation
			String rotX = spot.selectSingleNode("rotation/@x").getText();
			String rotY = spot.selectSingleNode("rotation/@y").getText();

			// spot cutoff and exponent
			String cutoff = spot.selectSingleNode("cutoff/@value").getText();
			String exp = spot.selectSingleNode("exponent/@value").getText();

			// ambient color
			String ambRed = spot.selectSingleNode("ambient/@r").getText();
			String ambGreen = spot.selectSingleNode("ambient/@g").getText();
			String ambBlue = spot.selectSingleNode("ambient/@b").getText();
			String ambAlpha = spot.selectSingleNode("ambient/@a").getText();

			// diffuse color
			String diffRed = spot.selectSingleNode("diffuse/@r").getText();
			String diffGreen = spot.selectSingleNode("diffuse/@g").getText();
			String diffBlue = spot.selectSingleNode("diffuse/@b").getText();
			String diffAlpha = spot.selectSingleNode("diffuse/@a").getText();

			SpotLight sl = new SpotLight(Integer.parseInt(nr));

			Color4f ambient = new Color4f(
					Float.parseFloat(ambRed),
					Float.parseFloat(ambGreen),
					Float.parseFloat(ambBlue),
					Float.parseFloat(ambAlpha));
			Color4f diffuse = new Color4f(
					Float.parseFloat(diffRed),
					Float.parseFloat(diffGreen),
					Float.parseFloat(diffBlue),
					Float.parseFloat(diffAlpha));

			sl.setDiffuse(diffuse);
			sl.setAmbient(ambient);
			sl.setPosition(
					Float.parseFloat(x),
					Float.parseFloat(y),
					Float.parseFloat(z));
			sl.setRotX(Float.parseFloat(rotX));
			sl.setRotY(Float.parseFloat(rotY));
			sl.setCutOff(Float.parseFloat(cutoff));
			sl.setExponent(Float.parseFloat(exp));

			this.room.addLight(sl);

			if(this.forEditor == true)
			{
				EditorMenu.addLightToListMenu(sl.getLightNr());
				sl.setShowLightPosition(true);
			}
		}
	}

	/**
	 * Helper method to load the positional lights' data from file
	 * @param lightsNode dom4j lights node
	 */
	private void loadPositionalLights(Node lightsNode)
	{
		List<Node> posLightNodes = lightsNode.selectNodes("positional");
		for(Node pos : posLightNodes)
		{
			// light number
			String nr = pos.selectSingleNode("@nr").getText();

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: positional light "+nr+" of room - "+this.room.getScriptId());

			// position
			String x = pos.selectSingleNode("position/@x").getText();
			String y = pos.selectSingleNode("position/@y").getText();
			String z = pos.selectSingleNode("position/@z").getText();

			// ambient color
			String ambRed = pos.selectSingleNode("ambient/@r").getText();
			String ambGreen = pos.selectSingleNode("ambient/@g").getText();
			String ambBlue = pos.selectSingleNode("ambient/@b").getText();
			String ambAlpha = pos.selectSingleNode("ambient/@a").getText();

			// diffuse color
			String diffRed = pos.selectSingleNode("diffuse/@r").getText();
			String diffGreen = pos.selectSingleNode("diffuse/@g").getText();
			String diffBlue = pos.selectSingleNode("diffuse/@b").getText();
			String diffAlpha = pos.selectSingleNode("diffuse/@a").getText();

			PositionalLight pl = new PositionalLight(Integer.parseInt(nr));

			Color4f ambient = new Color4f(
					Float.parseFloat(ambRed),
					Float.parseFloat(ambGreen),
					Float.parseFloat(ambBlue),
					Float.parseFloat(ambAlpha));
			Color4f diffuse = new Color4f(
					Float.parseFloat(diffRed),
					Float.parseFloat(diffGreen),
					Float.parseFloat(diffBlue),
					Float.parseFloat(diffAlpha));

			pl.setDiffuse(diffuse);
			pl.setAmbient(ambient);
			pl.setPosition(
					Float.parseFloat(x),
					Float.parseFloat(y),
					Float.parseFloat(z));

			this.room.addLight(pl);
			if(this.forEditor == true)
			{
				EditorMenu.addLightToListMenu(pl.getLightNr());
				pl.setShowLightPosition(true);
			}
		}
	}

	/**
	 * Helper method to load the directional lights' data from file
	 * @param lightsNode dom4j lights node
	 */
	private void loadDirectionalLights(Node lightsNode)
	{
		List<Node> dirLightNodes = lightsNode.selectNodes("directional");
		for(Node dir : dirLightNodes)
		{
			// light number
			String nr = dir.selectSingleNode("@nr").getText();

			ShootOut3DMain.getInstance().setLoadingInfoText("Loading: directional light "+nr+" of room - "+this.room.getScriptId());

			// direction
			String dirX = dir.selectSingleNode("direction/@x").getText();
			String dirY = dir.selectSingleNode("direction/@y").getText();
			String dirZ = dir.selectSingleNode("direction/@z").getText();

			// ambient color
			String ambRed = dir.selectSingleNode("ambient/@r").getText();
			String ambGreen = dir.selectSingleNode("ambient/@g").getText();
			String ambBlue = dir.selectSingleNode("ambient/@b").getText();
			String ambAlpha = dir.selectSingleNode("ambient/@a").getText();

			// diffuse color
			String diffRed = dir.selectSingleNode("diffuse/@r").getText();
			String diffGreen = dir.selectSingleNode("diffuse/@g").getText();
			String diffBlue = dir.selectSingleNode("diffuse/@b").getText();
			String diffAlpha = dir.selectSingleNode("diffuse/@a").getText();

			DirectionalLight dl = new DirectionalLight(Integer.parseInt(nr));

			Color4f ambient = new Color4f(
					Float.parseFloat(ambRed),
					Float.parseFloat(ambGreen),
					Float.parseFloat(ambBlue),
					Float.parseFloat(ambAlpha));
			Color4f diffuse = new Color4f(
					Float.parseFloat(diffRed),
					Float.parseFloat(diffGreen),
					Float.parseFloat(diffBlue),
					Float.parseFloat(diffAlpha));

			dl.setDiffuse(diffuse);
			dl.setAmbient(ambient);
			dl.setDirection(
					Float.parseFloat(dirX),
					Float.parseFloat(dirY),
					Float.parseFloat(dirZ));

			this.room.addLight(dl);

			if(this.forEditor == true)
				EditorMenu.addLightToListMenu(dl.getLightNr());
		}
	}

	/**
	 * Helper method to load the camera perspectives from file
	 * @param document dom4j document
	 */
	private void loadCameraPerspectives(Document document)
	{
		Node cam = document.selectSingleNode("//room/camera");
		if(cam != null)
		{
			List<Node> perspectives = cam.selectNodes("perspective");
			for(Node p : perspectives)
			{
				int id = Integer.parseInt(p.selectSingleNode("@id").getText());

				ShootOut3DMain.getInstance().setLoadingInfoText("Loading: camera perspective "+id);

				float fov = Float.parseFloat(p.selectSingleNode("@fov").getText());

				String x = p.selectSingleNode("position/@x").getText();
				String y = p.selectSingleNode("position/@y").getText();
				String z = p.selectSingleNode("position/@z").getText();

				Point3D position = new Point3D(
						Float.parseFloat(x),
						Float.parseFloat(y),
						Float.parseFloat(z));

				String rotX = p.selectSingleNode("rotation/@x").getText();
				String rotY = p.selectSingleNode("rotation/@y").getText();
				String near = p.selectSingleNode("clipping/@near").getText();
				String far = p.selectSingleNode("clipping/@far").getText();

				//I can't figure out why I have to invert the roation
				// values for the non-editor-loading, but it is
				// neccessary to differ between the two loading modes
				CameraPerspective persp = null;
				if(this.forEditor == true)
				{
					persp = new CameraPerspective(
							position,
							Float.parseFloat(rotX),
							Float.parseFloat(rotY),
							Float.parseFloat(near),
							Float.parseFloat(far));
					persp.setFieldOfView(fov);
				}
				else
				{
					persp = new CameraPerspective(
							position,
							-Float.parseFloat(rotX),
							-Float.parseFloat(rotY),
							Float.parseFloat(near),
							Float.parseFloat(far));
					persp.setFieldOfView(fov);
				}

				// add the perspective to the room
				this.room.addCameraPerspective(persp, id);

				if(this.forEditor == true)
					EditorMenu.addCamPerspectiveToListMenu(id);
			}
		}
	}

	/**
	 * Helper method to load the hotspots from file
	 * @param document dom4j document
	 */
	private void loadHotspots(Document document)
	{
		Node hotspotsNode = document.selectSingleNode("//room/hotspots");
		if(hotspotsNode != null)
		{
			List<Node> hotspots = hotspotsNode.selectNodes("hotspot");
			for(Node hs : hotspots)
			{
				String scriptId = hs.selectSingleNode("@scriptid").getText();

				ShootOut3DMain.getInstance().setLoadingInfoText("Loading: hotspot "+scriptId);

				String type = hs.selectSingleNode("@type").getText();
				String needsItem = hs.selectSingleNode("@needsitem").getText();

				String name = hs.selectSingleNode("name").getText();
				String x = hs.selectSingleNode("position/@x").getText();
				String y = hs.selectSingleNode("position/@y").getText();
				String z = hs.selectSingleNode("position/@z").getText();
				Point3D pos = new Point3D(new Float(x),new Float(y),new Float(z));

				if(type.equals("sphere"))
				{
					String radius = hs.selectSingleNode("dimension/@radius").getText();

					SphericalHotspot hotspot = new SphericalHotspot(
							pos,new Float(radius),new Boolean(needsItem));
					hotspot.setScriptId(scriptId);
					hotspot.setScreenName(name);

					// add the hotspot to the room
					this.room.addHotspot(hotspot);
				}
				if(type.equals("box"))
				{
					String width = hs.selectSingleNode("dimension/@width").getText();
					String height = hs.selectSingleNode("dimension/@height").getText();
					String length = hs.selectSingleNode("dimension/@length").getText();

					CubicalHotpot hotspot = new CubicalHotpot(
							pos,
							new Float(width),
							new Float(height),
							new Float(length),
							new Boolean(needsItem));
					hotspot.setScriptId(scriptId);
					hotspot.setScreenName(name);

					// add the hotspot to the room
					this.room.addHotspot(hotspot);
				}

				if(this.forEditor == true)
					EditorMenu.addHotspotToListMenu(scriptId);
			}
		}
	}


	/**
	 * Helper method to load the triggers from file
	 * @param document dom4j document
	 */
	private void loadTriggers(Document document)
	{
		Node triggersNode = document.selectSingleNode("//room/triggers");
		if(triggersNode != null)
		{
			List<Node> triggers = triggersNode.selectNodes("trigger");
			for(Node t : triggers)
			{
				String scriptId = t.selectSingleNode("@scriptid").getText();

				ShootOut3DMain.getInstance().setLoadingInfoText("Loading: trigger "+scriptId);

				String type = t.selectSingleNode("@type").getText();
				String enabled = t.selectSingleNode("@enabled").getText();

				String x = t.selectSingleNode("position/@x").getText();
				String z = t.selectSingleNode("position/@z").getText();

				if(type.equals("circle"))
				{
					String radius = t.selectSingleNode("dimension/@radius").getText();

					CircleTriggerArea trigger = new CircleTriggerArea(
							scriptId,
							new Float(x),
							new Float(z),
							new Float(radius));

					trigger.setEnabled(new Boolean(enabled));

					// add the trigger to the room
					this.room.addTrigger(trigger);
				}
				if(type.equals("rect"))
				{
					String width = t.selectSingleNode("dimension/@width").getText();
					String length = t.selectSingleNode("dimension/@length").getText();

					RectTriggerArea trigger = new RectTriggerArea(
							scriptId,
							new Float(x),
							new Float(z),
							new Float(width),
							new Float(length));

					trigger.setEnabled(new Boolean(enabled));

					// add the trigger to the room
					this.room.addTrigger(trigger);
				}

				if(this.forEditor == true)
					EditorMenu.addTriggerToListMenu(scriptId);
			}
		}
	}



	// special save methods

	/**
	 * Saves the camera perspectives which were defined in the
	 * editor.
	 * @param camNode dom4j node to attach the perspectives to
	 */
	private void saveCameraPerspectives(Element camNode)
	{
		for(Entry<Integer, CameraPerspective> p : this.room.getCameraPerspectives().entrySet())
		{
			// create a perspective node for every camera perspective
			Element perspNode = camNode.addElement("perspective");
			perspNode.addAttribute("id", ""+p.getKey());
			perspNode.addAttribute("fov", ""+p.getValue().getFieldOfView());

			// position
			Element posNode = perspNode.addElement("position");
			posNode.addAttribute("x", ""+p.getValue().getPosition().x*-1);
			posNode.addAttribute("y", ""+p.getValue().getPosition().y*-1);
			posNode.addAttribute("z", ""+p.getValue().getPosition().z*-1);

			// rotations
			Element rotNode = perspNode.addElement("rotation");
			rotNode.addAttribute("x", ""+p.getValue().getRotX()*-1);
			rotNode.addAttribute("y", ""+p.getValue().getRotY()*-1);

			// clipping planes
			Element clippingNode = perspNode.addElement("clipping");
			clippingNode.addAttribute("near", ""+p.getValue().getNearPlane());
			clippingNode.addAttribute("far", ""+p.getValue().getFarPlane());
		}
	}

	/**
	 * Saves the interactive hotspots which were defined in
	 * the editor.
	 * @param hotspotsNode dom4j node to attach the hotspots to
	 */
	private void saveHotspots(Element hotspotsNode)
	{
		for(AbstractInteractiveHotspot hotspot : this.room.getHotspots())
		{
			// create a hotspot node for every hotspot
			Element hotspotNode = hotspotsNode.addElement("hotspot");

			// position
			Element posNode = hotspotNode.addElement("position");
			posNode.addAttribute("x", ""+hotspot.getPosition().x);
			posNode.addAttribute("y", ""+hotspot.getPosition().y);
			posNode.addAttribute("z", ""+hotspot.getPosition().z);

			if(hotspot instanceof SphericalHotspot)
			{
				SphericalHotspot sphere = (SphericalHotspot)hotspot;

				hotspotNode.addAttribute("scriptid", ""+sphere.getScriptId());
				hotspotNode.addAttribute("type", "sphere");
				hotspotNode.addAttribute("needsitem", ""+hotspot.needsItem());
				Element nameNode = hotspotNode.addElement("name");
				nameNode.setText(sphere.getScreenName());

				// dimension
				Element dimNode = hotspotNode.addElement("dimension");
				dimNode.addAttribute("radius", ""+sphere.getRadius());

			}
			if(hotspot instanceof CubicalHotpot)
			{
				CubicalHotpot box = (CubicalHotpot)hotspot;

				hotspotNode.addAttribute("scriptid", ""+box.getScriptId());
				hotspotNode.addAttribute("type", "box");
				hotspotNode.addAttribute("needsitem", ""+hotspot.needsItem());

				Element nameNode = hotspotNode.addElement("name");
				nameNode.setText(box.getScreenName());
				// dimension
				Element dimNode = hotspotNode.addElement("dimension");
				dimNode.addAttribute("width", ""+box.getWidth());
				dimNode.addAttribute("height", ""+box.getHeight());
				dimNode.addAttribute("length", ""+box.getLength());

			}
		}
	}

	/**
	 * Saves the triggers which were defined in the
	 * editor.
	 * @param triggersNode dom4j node to attach the triggers to
	 */
	private void saveTriggers(Element triggersNode)
	{
		for(AbstractTrigger trigger : this.room.getTriggers())
		{
			// create a trigger node for every trigger
			Element triggerNode = triggersNode.addElement("trigger");
			triggerNode.addAttribute("enabled", ""+trigger.isEnabled());

			// position
			Element posNode = triggerNode.addElement("position");
			posNode.addAttribute("x", ""+trigger.getX());
			posNode.addAttribute("z", ""+trigger.getZ());

			if(trigger instanceof CircleTriggerArea)
			{
				CircleTriggerArea circle = (CircleTriggerArea)trigger;

				triggerNode.addAttribute("scriptid", ""+circle.getScriptId());
				triggerNode.addAttribute("type", "circle");

				// dimension
				Element dimNode = triggerNode.addElement("dimension");
				dimNode.addAttribute("radius", ""+circle.getRadius());

			}
			if(trigger instanceof RectTriggerArea)
			{
				RectTriggerArea rect = (RectTriggerArea)trigger;

				triggerNode.addAttribute("scriptid", ""+rect.getScriptId());
				triggerNode.addAttribute("type", "rect");

				// dimension
				Element dimNode = triggerNode.addElement("dimension");
				dimNode.addAttribute("width", ""+rect.getWidth());
				dimNode.addAttribute("length", ""+rect.getLength());

			}
		}
	}

	/**
	 * Helper method that saves water layer data to file.
	 * @param propsNode dom4j mesh node
	 * @param prop mesh object that actually is a WaterLayer object
	 */
	private void saveWaterLayer(Element propsNode, Prop prop)
	{
		WaterLayer model = (WaterLayer)prop.getMesh();

		Element propNode = propsNode.addElement("water");
		propNode.addAttribute("id", model.getName());
		propNode.addAttribute("width", ""+model.getWidth());
		propNode.addAttribute("length", ""+model.getLength());
		propNode.addAttribute("movex", ""+model.getText1XMovement());
		propNode.addAttribute("movez", ""+model.getText1YMovement()*-1);
		propNode.addAttribute("transparency", ""+model.getMaterial().getDiffuse().alpha);

		propNode.addAttribute("scriptid",prop.getScriptId());

		Element texUnit0 = propNode.addElement("texture");
		texUnit0.addAttribute("unit", "0");
		texUnit0.setText(this.getRelativeFilePath(model.getTex0Path()));

		Element texUnit1 = propNode.addElement("texture");
		texUnit1.addAttribute("unit", "1");
		texUnit1.setText(this.getRelativeFilePath(model.getTex1Path()));

		// position
		Element position = propNode.addElement("position");
		position.addAttribute("x", Float.toString(model.getPosX()));
		position.addAttribute("y", Float.toString(model.getPosY()));
		position.addAttribute("z", Float.toString(model.getPosZ()));

		// rotation
		Element rotation = propNode.addElement("rotation");
		rotation.addAttribute("x", Float.toString(model.getRotX()));
		rotation.addAttribute("y", Float.toString(model.getRotY()));
		rotation.addAttribute("z", Float.toString(model.getRotZ()));

		// scale
		Element scale = propNode.addElement("scale");
		scale.addAttribute("x", Float.toString(model.getScaleX()));
		scale.addAttribute("y", Float.toString(model.getScaleY()));
		scale.addAttribute("z", Float.toString(model.getScaleZ()));
	}

	/**
	 * Helper method that saves ground plane data to file.
	 * @param propsNode dom4j mesh node
	 * @param prop mesh object that actually is a GroundPlane object
	 */
	private void saveGroundPlane(Element propsNode, Prop prop)
	{
		GroundPlane model = (GroundPlane)prop.getMesh();

		Element propNode = propsNode.addElement("ground");
		propNode.addAttribute("id", model.getName());
		propNode.addAttribute("type", "plane");
		propNode.addAttribute("size", ""+model.getSize());
		propNode.addAttribute("texrepeat", ""+model.getTexRepeat());

		propNode.addAttribute("scriptid",prop.getScriptId());

		if(model.getTex0Path() != null)
		{
			Element texUnit0 = propNode.addElement("texture");
			texUnit0.addAttribute("unit", "0");
			texUnit0.addAttribute("mipmap", ""+model.isTex0MipMapped());
			texUnit0.addAttribute("rotate", ""+model.isTex0Rotated());
			texUnit0.addAttribute("flip", ""+model.isTex0Flipped());
			texUnit0.setText(this.getRelativeFilePath(model.getTex0Path()));
			//TODO add behaviour for texture unit 2 later on
		}

		// position
		Element position = propNode.addElement("position");
		position.addAttribute("x", Float.toString(model.getPosX()));
		position.addAttribute("y", Float.toString(model.getPosY()));
		position.addAttribute("z", Float.toString(model.getPosZ()));

		// rotation
		Element rotation = propNode.addElement("rotation");
		rotation.addAttribute("x", Float.toString(model.getRotX()));
		rotation.addAttribute("y", Float.toString(model.getRotY()));
		rotation.addAttribute("z", Float.toString(model.getRotZ()));

		// scale
		Element scale = propNode.addElement("scale");
		scale.addAttribute("x", Float.toString(model.getScaleX()));
		scale.addAttribute("y", Float.toString(model.getScaleY()));
		scale.addAttribute("z", Float.toString(model.getScaleZ()));


		// save walkable grid nodes, if grid available
		if(model.getSearchGrid() != null)
		{
			Element grid = propNode.addElement("pathgrid");
			grid.addAttribute("nodesize", ""+model.getSearchGrid().getGridNodeSize());
			for(GridNode node : model.getSearchGrid().getGridNodesList())
			{
				if(node.isWalkable() == true)
				{
					Element walkable = grid.addElement("walkable");
					walkable.addAttribute("nodeid", ""+node.getId());
				}
			}
		}
	}

	/**
	 * Helper method that saves heightmap terrain data to file.
	 * @param propsNode dom4j mesh node
	 * @param prop prop object that actually is an Terrain object
	 */
	private void saveHMTerrain(Element propsNode, Prop prop)
	{
		Terrain model = (Terrain)prop.getMesh();

		Element propNode = propsNode.addElement("ground");
		propNode.addAttribute("id", model.getName());
		propNode.addAttribute("type", "heightmap");
		propNode.addAttribute("smooth", ""+model.isSmoothShaded());
		propNode.addAttribute("resolution", ""+model.getResolution());

		propNode.addAttribute("scriptid",prop.getScriptId());

		Element file = propNode.addElement("file");
		file.setText(this.getRelativeFilePath(model.getFilePath()));

		if(model.getTex0Path() != null)
		{
			Element texUnit0 = propNode.addElement("texture");
			texUnit0.addAttribute("unit", "0");
			texUnit0.addAttribute("mipmap", ""+model.isTex0MipMapped());
			texUnit0.addAttribute("rotate", ""+model.isTex0Rotated());
			texUnit0.addAttribute("flip", ""+model.isTex0Flipped());
			texUnit0.setText(this.getRelativeFilePath(model.getTex0Path()));
			//TODO add behaviour for texture unit 2 later on
		}

		// position
		Element position = propNode.addElement("position");
		position.addAttribute("x", Float.toString(model.getPosX()));
		position.addAttribute("y", Float.toString(model.getPosY()));
		position.addAttribute("z", Float.toString(model.getPosZ()));

		// rotation
		Element rotation = propNode.addElement("rotation");
		rotation.addAttribute("x", Float.toString(model.getRotX()));
		rotation.addAttribute("y", Float.toString(model.getRotY()));
		rotation.addAttribute("z", Float.toString(model.getRotZ()));

		// scale
		Element scale = propNode.addElement("scale");
		scale.addAttribute("x", Float.toString(model.getScaleX()));
		scale.addAttribute("y", Float.toString(model.getScaleY()));
		scale.addAttribute("z", Float.toString(model.getScaleZ()));

		// save walkable grid nodes, if grid available
		if(model.getSearchGrid() != null)
		{
			Element grid = propNode.addElement("pathgrid");
			grid.addAttribute("nodesize", ""+model.getSearchGrid().getGridNodeSize());
			for(GridNode node : model.getSearchGrid().getGridNodesList())
			{
				if(node.isWalkable() == true)
				{
					Element walkable = grid.addElement("walkable");
					walkable.addAttribute("nodeid", ""+node.getId());
				}
			}
		}
	}

	/**
	 * Helper method that saves ms3d ascii model data to file.
	 * @param meshNode dom4j mesh node
	 * @param entity entity object that actually is an ModelMS3DAscii object
	 */
	private void saveModelMs3dAscii(Element meshNode, AbstractEntity entity)
	{
		ModelMS3DAscii model = (ModelMS3DAscii)entity.getMesh();

		Element entityNode = null;

		if(entity instanceof Prop)
		{
			entityNode = meshNode.addElement("prop");
			entityNode.addAttribute("scriptid",entity.getScriptId());
		}
		if(entity instanceof Item)
		{
			Item item = (Item)entity;
			entityNode = meshNode.addElement("item");
			Element name = entityNode.addElement("name");
			name.setText(item.getScreenName());
			entityNode.addAttribute("scriptid",entity.getScriptId());

			if(item.getInventoryImage() != null)
			{
				Element img = entityNode.addElement("image");
				img.setText(this.getRelativeFilePath(item.getInventoryImage().getFilePath()));
			}

			Element pick = entityNode.addElement("pickable");
			pick.setText(""+item.canBePickedUp());
		}
		if(entity instanceof NonPlayerCharacter)
		{
			entityNode = meshNode.addElement("character");
			Element name = entityNode.addElement("name");
			name.setText(entity.getScreenName());
			entityNode.addAttribute("scriptid",entity.getScriptId());
		}

		entityNode.addAttribute("id", model.getName());
		entityNode.addAttribute("type", "ms3dascii");
		entityNode.addAttribute("smooth", ""+model.isSmoothShaded());

		if(model.getBoundingVolume() instanceof BoundingSphere)
			entityNode.addAttribute("volume", "sphere");
		else if(model.getBoundingVolume() instanceof BoundingBox)
			entityNode.addAttribute("volume", "box");
		else if(model.getBoundingVolume() instanceof BoundingCylinder)
			entityNode.addAttribute("volume", "cylinder");

		entityNode.addAttribute("shadow", ""+entity.getMesh().isShadowEnabled());

		Element file = entityNode.addElement("file");
		file.setText(this.getRelativeFilePath(model.getFilePath()));

		if(model.getTex0Path() != null)
		{
			Element texUnit0 = entityNode.addElement("texture");
			texUnit0.addAttribute("unit", "0");
			texUnit0.addAttribute("mipmap", ""+model.isTex0MipMapped());
			texUnit0.addAttribute("rotate", ""+model.isTex0Rotated());
			texUnit0.addAttribute("flip", ""+model.isTex0Flipped());
			texUnit0.setText(this.getRelativeFilePath(model.getTex0Path()));
		}

		if(model.getTex1Path() != null)
		{
			Element texUnit1 = entityNode.addElement("texture");
			texUnit1.addAttribute("unit", "1");
			texUnit1.addAttribute("mipmap", ""+model.isTex1MipMapped());
			texUnit1.addAttribute("rotate", ""+model.isTex1Rotated());
			texUnit1.addAttribute("flip", ""+model.isTex1Flipped());
			texUnit1.setText(this.getRelativeFilePath(model.getTex1Path()));
		}

		// position
		Element position = entityNode.addElement("position");
		position.addAttribute("x", Float.toString(model.getPosX()));
		position.addAttribute("y", Float.toString(model.getPosY()));
		position.addAttribute("z", Float.toString(model.getPosZ()));

		// rotation
		Element rotation = entityNode.addElement("rotation");
		rotation.addAttribute("x", Float.toString(model.getRotX()));
		rotation.addAttribute("y", Float.toString(model.getRotY()));
		rotation.addAttribute("z", Float.toString(model.getRotZ()));

		// scale
		Element scale = entityNode.addElement("scale");
		scale.addAttribute("x", Float.toString(model.getScaleX()));
		scale.addAttribute("y", Float.toString(model.getScaleY()));
		scale.addAttribute("z", Float.toString(model.getScaleZ()));

		// save animations		
		for(Animation anim : model.getAnimations())
		{
			Element animNode = entityNode.addElement("animation");
			animNode.addAttribute("name", anim.getName());
			animNode.addAttribute("start", ""+anim.getStartFrame());
			animNode.addAttribute("end", ""+anim.getEndFrame());
			animNode.addAttribute("timeperframe", ""+anim.getDurPerFrame());

			if(anim.getSound() != null)
			{
				animNode.addAttribute("sound", anim.getSound().getSoundPath());
				animNode.addAttribute("loopsound", ""+anim.getSound().isLooped());
			}
		}
		
		// if the model has no animation, create a default
		// idle animation that has one frame
		if(model.getAnimations().size() == 0)
		{
			Element animNode = entityNode.addElement("animation");
			animNode.addAttribute("name", "idle");
			animNode.addAttribute("start", "1");
			animNode.addAttribute("end", "1");
			animNode.addAttribute("timeperframe", "100");
		}
	}

	/**
	 * Helper method that saves 3ds model data to file.
	 * @param meshNode dom4j mesh node
	 * @param entity entity object that actually is an ModelMD2 object
	 */
	private void saveModelMd2(Element meshNode, AbstractEntity entity)
	{
		ModelMD2 model = (ModelMD2)entity.getMesh();

		Element entityNode = null;

		if(entity instanceof Prop)
		{
			entityNode = meshNode.addElement("prop");
			entityNode.addAttribute("scriptid",entity.getScriptId());
		}
		if(entity instanceof Item)
		{
			Item item = (Item)entity;
			entityNode = meshNode.addElement("item");
			Element name = entityNode.addElement("name");
			name.setText(item.getScreenName());
			entityNode.addAttribute("scriptid",entity.getScriptId());

			if(item.getInventoryImage() != null)
			{
				Element img = entityNode.addElement("image");
				img.setText(this.getRelativeFilePath(item.getInventoryImage().getFilePath()));
			}

			Element pick = entityNode.addElement("pickable");
			pick.setText(""+item.canBePickedUp());
		}
		if(entity instanceof NonPlayerCharacter)
		{
			entityNode = meshNode.addElement("character");
			Element name = entityNode.addElement("name");
			name.setText(entity.getScreenName());
			entityNode.addAttribute("scriptid",entity.getScriptId());
		}

		entityNode.addAttribute("id", model.getName());
		entityNode.addAttribute("type", "md2");
		entityNode.addAttribute("smooth", ""+model.isSmoothShaded());

		if(model.getBoundingVolume() instanceof BoundingSphere)
			entityNode.addAttribute("volume", "sphere");
		else if(model.getBoundingVolume() instanceof BoundingBox)
			entityNode.addAttribute("volume", "box");
		else if(model.getBoundingVolume() instanceof BoundingCylinder)
			entityNode.addAttribute("volume", "cylinder");

		entityNode.addAttribute("shadow", ""+entity.getMesh().isShadowEnabled());

		Element file = entityNode.addElement("file");
		file.setText(this.getRelativeFilePath(model.getFilePath()));

		if(model.getTex0Path() != null)
		{
			Element texUnit0 = entityNode.addElement("texture");
			texUnit0.addAttribute("unit", "0");
			texUnit0.addAttribute("mipmap", ""+model.isTex0MipMapped());
			texUnit0.addAttribute("rotate", ""+model.isTex0Rotated());
			texUnit0.addAttribute("flip", ""+model.isTex0Flipped());
			texUnit0.setText(this.getRelativeFilePath(model.getTex0Path()));
		}

		if(model.getTex1Path() != null)
		{
			Element texUnit1 = entityNode.addElement("texture");
			texUnit1.addAttribute("unit", "1");
			texUnit1.addAttribute("mipmap", ""+model.isTex1MipMapped());
			texUnit1.addAttribute("rotate", ""+model.isTex1Rotated());
			texUnit1.addAttribute("flip", ""+model.isTex1Flipped());
			texUnit1.setText(this.getRelativeFilePath(model.getTex1Path()));
		}

		// position
		Element position = entityNode.addElement("position");
		position.addAttribute("x", Float.toString(model.getPosX()));
		position.addAttribute("y", Float.toString(model.getPosY()));
		position.addAttribute("z", Float.toString(model.getPosZ()));

		// rotation
		Element rotation = entityNode.addElement("rotation");
		rotation.addAttribute("x", Float.toString(model.getRotX()));
		rotation.addAttribute("y", Float.toString(model.getRotY()));
		rotation.addAttribute("z", Float.toString(model.getRotZ()));

		// scale
		Element scale = entityNode.addElement("scale");
		scale.addAttribute("x", Float.toString(model.getScaleX()));
		scale.addAttribute("y", Float.toString(model.getScaleY()));
		scale.addAttribute("z", Float.toString(model.getScaleZ()));

		// save animations
		for(Animation anim : model.getAnimations())
		{
			Element animNode = entityNode.addElement("animation");
			animNode.addAttribute("name", anim.getName());
			animNode.addAttribute("start", ""+anim.getStartFrame());
			animNode.addAttribute("end", ""+anim.getEndFrame());
			animNode.addAttribute("timeperframe", ""+anim.getDurPerFrame());

			if(anim.getSound() != null)
			{
				animNode.addAttribute("sound", anim.getSound().getSoundPath());
				animNode.addAttribute("loopsound", ""+anim.getSound().isLooped());
			}
		}
		
//		 if the model has no animation, create a default
		// idle animation that has one frame
		if(model.getAnimations().size() == 0)
		{
			Element animNode = entityNode.addElement("animation");
			animNode.addAttribute("name", "idle");
			animNode.addAttribute("start", "1");
			animNode.addAttribute("end", "1");
			animNode.addAttribute("timeperframe", "100");
		}
	}

	/**
	 * Helper method that saves 3ds model data to file.
	 * @param meshNode dom4j mesh node
	 * @param entity entity that actually is an Model3DS object
	 */
	private void saveModel3ds(Element meshNode, AbstractEntity entity)
	{
		Model3DS model = (Model3DS)entity.getMesh();
		Element entityNode = null;

		if(entity instanceof Prop)
		{
			entityNode = meshNode.addElement("prop");
			entityNode.addAttribute("scriptid",entity.getScriptId());
		}
		if(entity instanceof Item)
		{
			Item item = (Item)entity;
			entityNode = meshNode.addElement("item");
			Element name = entityNode.addElement("name");
			name.setText(item.getScreenName());
			entityNode.addAttribute("scriptid",entity.getScriptId());

			if(item.getInventoryImage() != null)
			{
				Element img = entityNode.addElement("image");
				img.setText(this.getRelativeFilePath(item.getInventoryImage().getFilePath()));
			}

			Element pick = entityNode.addElement("pickable");
			pick.setText(""+item.canBePickedUp());
		}
		if(entity instanceof NonPlayerCharacter)
		{
			entityNode = meshNode.addElement("character");
			Element name = entityNode.addElement("name");
			name.setText(entity.getScreenName());
			entityNode.addAttribute("scriptid",entity.getScriptId());
		}

		entityNode.addAttribute("id", model.getName());
		entityNode.addAttribute("type", "3ds");
		entityNode.addAttribute("smooth", ""+model.isSmoothShaded());

		if(model.getBoundingVolume() instanceof BoundingSphere)
			entityNode.addAttribute("volume", "sphere");
		else if(model.getBoundingVolume() instanceof BoundingBox)
			entityNode.addAttribute("volume", "box");
		else if(model.getBoundingVolume() instanceof BoundingCylinder)
			entityNode.addAttribute("volume", "cylinder");

		entityNode.addAttribute("shadow", ""+entity.getMesh().isShadowEnabled());

		Element file = entityNode.addElement("file");
		file.setText(this.getRelativeFilePath(model.getFilePath()));

		if(model.getTex0Path() != null)
		{
			Element texUnit0 = entityNode.addElement("texture");
			texUnit0.addAttribute("unit", "0");
			texUnit0.addAttribute("mipmap", ""+model.isTex0MipMapped());
			texUnit0.addAttribute("rotate", ""+model.isTex0Rotated());
			texUnit0.addAttribute("flip", ""+model.isTex0Flipped());
			texUnit0.setText(this.getRelativeFilePath(model.getTex0Path()));
		}

		if(model.getTex1Path() != null)
		{
			Element texUnit1 = entityNode.addElement("texture");
			texUnit1.addAttribute("unit", "1");
			texUnit1.addAttribute("mipmap", ""+model.isTex1MipMapped());
			texUnit1.addAttribute("rotate", ""+model.isTex1Rotated());
			texUnit1.addAttribute("flip", ""+model.isTex1Flipped());
			texUnit1.setText(this.getRelativeFilePath(model.getTex1Path()));
		}

		// position
		Element position = entityNode.addElement("position");
		position.addAttribute("x", Float.toString(model.getPosX()));
		position.addAttribute("y", Float.toString(model.getPosY()));
		position.addAttribute("z", Float.toString(model.getPosZ()));

		// rotation
		Element rotation = entityNode.addElement("rotation");
		rotation.addAttribute("x", Float.toString(model.getRotX()));
		rotation.addAttribute("y", Float.toString(model.getRotY()));
		rotation.addAttribute("z", Float.toString(model.getRotZ()));

		// scale
		Element scale = entityNode.addElement("scale");
		scale.addAttribute("x", Float.toString(model.getScaleX()));
		scale.addAttribute("y", Float.toString(model.getScaleY()));
		scale.addAttribute("z", Float.toString(model.getScaleZ()));
	}

	/**
	 * Saves the sky box data to file.
	 * @param meshNode dom4j node of the hierarchy
	 */
	private void saveSkyBox(Element meshNode)
	{
		// save sky box, if available
		SkyBox box = (SkyBox)this.room.getMeshes().getMeshByName("sky box");
		if(box != null)
		{
			Element skybox = meshNode.addElement("skybox");
			skybox.addAttribute("id", box.getName());
			{
				Element textures = skybox.addElement("textures");
				{
					if(box.getPathEast() != null)
					{
						Element posX = textures.addElement("posx");
						posX.addText(this.getRelativeFilePath(box.getPathEast()));
					}

					if(box.getPathWest() != null)
					{
						Element negX = textures.addElement("negx");
						negX.addText(this.getRelativeFilePath(box.getPathWest()));
					}

					if(box.getPathTop() != null)
					{
						Element posY = textures.addElement("posy");
						posY.addText(this.getRelativeFilePath(box.getPathTop()));
					}

					if(box.getPathBottom() != null)
					{
						Element negY = textures.addElement("negy");
						negY.addText(this.getRelativeFilePath(box.getPathBottom()));
					}

					if(box.getPathSouth() != null)
					{
						Element posZ = textures.addElement("posz");
						posZ.addText(this.getRelativeFilePath(box.getPathSouth()));
					}

					if(box.getPathNorth() != null)
					{
						Element negZ = textures.addElement("negz");
						negZ.addText(this.getRelativeFilePath(box.getPathNorth()));
					}
				}
			}
		}
	}

	/**
	 * Helper method to save positional light data to file
	 * @param lightNode element of a dom4j node.
	 * @param light LightIface object that actually is a PositionalLight
	 * object.
	 */
	private void savePositionalLight(Element lightNode, LightIface light)
	{
		PositionalLight positional = (PositionalLight)light;

		Element pos = lightNode.addElement("positional");
		pos.addAttribute("nr", Integer.toString(positional.getLightNr()));
		{
			// position
			Element position = pos.addElement("position");
			position.addAttribute("x", Float.toString(positional.getPosition().x));
			position.addAttribute("y", Float.toString(positional.getPosition().y));
			position.addAttribute("z", Float.toString(positional.getPosition().z));

			// ambient color
			Element ambient = pos.addElement("ambient");
			ambient.addAttribute("r", Float.toString(positional.getAmbient().red));
			ambient.addAttribute("g", Float.toString(positional.getAmbient().green));
			ambient.addAttribute("b", Float.toString(positional.getAmbient().blue));
			ambient.addAttribute("a", Float.toString(positional.getAmbient().alpha));

			// diffuse color
			Element diffuse = pos.addElement("diffuse");
			diffuse.addAttribute("r", Float.toString(positional.getDiffuse().red));
			diffuse.addAttribute("g", Float.toString(positional.getDiffuse().green));
			diffuse.addAttribute("b", Float.toString(positional.getDiffuse().blue));
			diffuse.addAttribute("a", Float.toString(positional.getDiffuse().alpha));
		}
	}

	/**
	 * Helper method to save spot light data to file
	 * @param lightNode element of a dom4j node.
	 * @param light LightIface object that actually is a SpotLight
	 * object.
	 */
	private void saveSpotLight(Element lightNode, LightIface light)
	{
		SpotLight spotLight = (SpotLight)light;

		Element spot = lightNode.addElement("spot");
		spot.addAttribute("nr", Integer.toString(spotLight.getLightNr()));
		{
			// position
			Element position = spot.addElement("position");
			position.addAttribute("x", Float.toString(spotLight.getPosition().x));
			position.addAttribute("y", Float.toString(spotLight.getPosition().y));
			position.addAttribute("z", Float.toString(spotLight.getPosition().z));

			// ambient color
			Element ambient = spot.addElement("ambient");
			ambient.addAttribute("r", Float.toString(spotLight.getAmbient().red));
			ambient.addAttribute("g", Float.toString(spotLight.getAmbient().green));
			ambient.addAttribute("b", Float.toString(spotLight.getAmbient().blue));
			ambient.addAttribute("a", Float.toString(spotLight.getAmbient().alpha));

			// diffuse color
			Element diffuse = spot.addElement("diffuse");
			diffuse.addAttribute("r", Float.toString(spotLight.getDiffuse().red));
			diffuse.addAttribute("g", Float.toString(spotLight.getDiffuse().green));
			diffuse.addAttribute("b", Float.toString(spotLight.getDiffuse().blue));
			diffuse.addAttribute("a", Float.toString(spotLight.getDiffuse().alpha));

			// rotation
			Element rotation = spot.addElement("rotation");
			rotation.addAttribute("x", Float.toString(spotLight.getRotX()));
			rotation.addAttribute("y", Float.toString(spotLight.getRotY()));

			// spot cutoff and exponent
			Element cutOff = spot.addElement("cutoff");
			cutOff.addAttribute("value", Float.toString(spotLight.getCutoff()));
			Element exp = spot.addElement("exponent");
			exp.addAttribute("value",Float.toString(spotLight.getExponent()));
		}
	}

	/**
	 * Helper method to save directional light data to file
	 * @param lightNode element of a dom4j node.
	 * @param light LightIface object that actually is a DirectionalLight
	 * object.
	 */
	private void saveDirectionalLight(Element lightNode, LightIface light)
	{
		DirectionalLight directional = (DirectionalLight)light;

		Element dir = lightNode.addElement("directional");
		dir.addAttribute("nr", Integer.toString(directional.getLightNr()));
		{
			// direction
			Element direction = dir.addElement("direction");
			direction.addAttribute("x", Float.toString(directional.getDirection().x));
			direction.addAttribute("y", Float.toString(directional.getDirection().y));
			direction.addAttribute("z", Float.toString(directional.getDirection().z));

			// ambient color
			Element ambient = dir.addElement("ambient");
			ambient.addAttribute("r", Float.toString(directional.getAmbient().red));
			ambient.addAttribute("g", Float.toString(directional.getAmbient().green));
			ambient.addAttribute("b", Float.toString(directional.getAmbient().blue));
			ambient.addAttribute("a", Float.toString(directional.getAmbient().alpha));

			// diffuse color
			Element diffuse = dir.addElement("diffuse");
			diffuse.addAttribute("r", Float.toString(directional.getDiffuse().red));
			diffuse.addAttribute("g", Float.toString(directional.getDiffuse().green));
			diffuse.addAttribute("b", Float.toString(directional.getDiffuse().blue));
			diffuse.addAttribute("a", Float.toString(directional.getDiffuse().alpha));
		}
	}

}
