/**
 * @author alteredq / http://alteredqualia.com/
 */

THREE.SceneLoader = function (manager) {

	this.onLoadStart = function () { };
	this.onLoadProgress = function () { };
	this.onLoadComplete = function () { };

	this.callbackSync = function () { };
	this.callbackProgress = function () { };

	this.geometryHandlers = {};
	this.hierarchyHandlers = {};

	this.addGeometryHandler("ascii", THREE.JSONLoader);

	this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;

};

THREE.SceneLoader.prototype = {

	constructor: THREE.SceneLoader,

	load: function (url, onLoad, onProgress, onError) {

		var scope = this;

		var loader = new THREE.FileLoader(scope.manager);
		loader.load(url, function (text) {

			scope.parse(JSON.parse(text), onLoad, url);

		}, onProgress, onError);

	},

	addGeometryHandler: function (typeID, loaderClass) {

		this.geometryHandlers[typeID] = { "loaderClass": loaderClass };

	},

	addHierarchyHandler: function (typeID, loaderClass) {

		this.hierarchyHandlers[typeID] = { "loaderClass": loaderClass };

	},

	parse: function (json, callbackFinished, url) {

		var scope = this;

		var urlBase = THREE.Loader.prototype.extractUrlBase(url);

		var geometry, material, camera, fog,
			texture, images, color,
			light, hex, intensity,
			counter_models, counter_textures,
			total_models, total_textures,
			result;

		var target_array = [];

		var data = json;

		// async geometry loaders

		for (var typeID in this.geometryHandlers) {

			var loaderClass = this.geometryHandlers[typeID]["loaderClass"];
			this.geometryHandlers[typeID]["loaderObject"] = new loaderClass();

		}

		// async hierachy loaders

		for (var typeID in this.hierarchyHandlers) {

			var loaderClass = this.hierarchyHandlers[typeID]["loaderClass"];
			this.hierarchyHandlers[typeID]["loaderObject"] = new loaderClass();

		}

		counter_models = 0;
		counter_textures = 0;

		result = {

			scene: new THREE.Scene(),
			geometries: {},
			face_materials: {},
			materials: {},
			textures: {},
			objects: {},
			cameras: {},
			lights: {},
			fogs: {},
			empties: {},
			groups: {}

		};

		if (data.transform) {

			var position = data.transform.position,
				rotation = data.transform.rotation,
				scale = data.transform.scale;

			if (position) {

				result.scene.position.fromArray(position);

			}

			if (rotation) {

				result.scene.rotation.fromArray(rotation);

			}

			if (scale) {

				result.scene.scale.fromArray(scale);

			}

			if (position || rotation || scale) {

				result.scene.updateMatrix();
				result.scene.updateMatrixWorld();

			}

		}

		function get_url(source_url, url_type) {

			if (url_type == "relativeToHTML") {

				return source_url;

			} else {

				return urlBase + source_url;

			}

		}

		// toplevel loader function, delegates to handle_children

		function handle_objects() {

			handle_children(result.scene, data.objects);

		}

		// handle all the children from the loaded json and attach them to given parent

		function handle_children(parent, children) {

			var mat, dst, pos, rot, scl, quat;

			for (var objID in children) {

				// check by id if child has already been handled,
				// if not, create new object

				var object = result.objects[objID];
				var objJSON = children[objID];

				if (object === undefined) {

					// meshes

					if (objJSON.type && (objJSON.type in scope.hierarchyHandlers)) {

						if (objJSON.loading === undefined) {

							material = result.materials[objJSON.material];

							objJSON.loading = true;

							var loader = scope.hierarchyHandlers[objJSON.type]["loaderObject"];

							// ColladaLoader

							if (loader.options) {

								loader.load(get_url(objJSON.url, data.urlBaseType), create_callback_hierachy(objID, parent, material, objJSON));

								// UTF8Loader
								// OBJLoader

							} else {

								loader.load(get_url(objJSON.url, data.urlBaseType), create_callback_hierachy(objID, parent, material, objJSON));

							}

						}

					} else if (objJSON.geometry !== undefined) {

						geometry = result.geometries[objJSON.geometry];

						// geometry already loaded

						if (geometry) {

							material = result.materials[objJSON.material];

							pos = objJSON.position;
							rot = objJSON.rotation;
							scl = objJSON.scale;
							mat = objJSON.matrix;
							quat = objJSON.quaternion;

							// use materials from the model file
							// if there is no material specified in the object

							if (!objJSON.material) {

								material = new THREE.MultiMaterial(result.face_materials[objJSON.geometry]);

							}

							// use materials from the model file
							// if there is just empty face material
							// (must create new material as each model has its own face material)

							if ((material instanceof THREE.MultiMaterial) && material.materials.length === 0) {

								material = new THREE.MultiMaterial(result.face_materials[objJSON.geometry]);

							}

							if (objJSON.skin) {

								object = new THREE.SkinnedMesh(geometry, material);

							} else if (objJSON.morph) {

								object = new THREE.MorphAnimMesh(geometry, material);

								if (objJSON.duration !== undefined) {

									object.duration = objJSON.duration;

								}

								if (objJSON.time !== undefined) {

									object.time = objJSON.time;

								}

								if (objJSON.mirroredLoop !== undefined) {

									object.mirroredLoop = objJSON.mirroredLoop;

								}

								if (material.morphNormals) {

									geometry.computeMorphNormals();

								}

							} else {

								object = new THREE.Mesh(geometry, material);

							}

							object.name = objID;

							if (mat) {

								object.matrixAutoUpdate = false;
								object.matrix.set(
									mat[0], mat[1], mat[2], mat[3],
									mat[4], mat[5], mat[6], mat[7],
									mat[8], mat[9], mat[10], mat[11],
									mat[12], mat[13], mat[14], mat[15]
								);

							} else {

								object.position.fromArray(pos);

								if (quat) {

									object.quaternion.fromArray(quat);

								} else {

									object.rotation.fromArray(rot);

								}

								object.scale.fromArray(scl);

							}

							object.visible = objJSON.visible;
							object.castShadow = objJSON.castShadow;
							object.receiveShadow = objJSON.receiveShadow;

							parent.add(object);

							result.objects[objID] = object;

						}

						// lights

					} else if (objJSON.type === "AmbientLight" || objJSON.type === "PointLight" ||
						objJSON.type === "DirectionalLight" || objJSON.type === "SpotLight" ||
						objJSON.type === "HemisphereLight") {

						var color = objJSON.color;
						var intensity = objJSON.intensity;
						var distance = objJSON.distance;
						var position = objJSON.position;
						var rotation = objJSON.rotation;

						switch (objJSON.type) {

							case 'AmbientLight':
								light = new THREE.AmbientLight(color);
								break;

							case 'PointLight':
								light = new THREE.PointLight(color, intensity, distance);
								light.position.fromArray(position);
								break;

							case 'DirectionalLight':
								light = new THREE.DirectionalLight(color, intensity);
								light.position.fromArray(objJSON.direction);
								break;

							case 'SpotLight':
								light = new THREE.SpotLight(color, intensity, distance);
								light.angle = objJSON.angle;
								light.position.fromArray(position);
								light.target.set(position[0], position[1] - distance, position[2]);
								light.target.applyEuler(new THREE.Euler(rotation[0], rotation[1], rotation[2], 'XYZ'));
								break;

							case 'HemisphereLight':
								light = new THREE.DirectionalLight(color, intensity, distance);
								light.target.set(position[0], position[1] - distance, position[2]);
								light.target.applyEuler(new THREE.Euler(rotation[0], rotation[1], rotation[2], 'XYZ'));
								break;

						}

						parent.add(light);

						light.name = objID;
						result.lights[objID] = light;
						result.objects[objID] = light;

						// cameras

					} else if (objJSON.type === "PerspectiveCamera" || objJSON.type === "OrthographicCamera") {

						pos = objJSON.position;
						rot = objJSON.rotation;
						quat = objJSON.quaternion;

						if (objJSON.type === "PerspectiveCamera") {

							camera = new THREE.PerspectiveCamera(objJSON.fov, objJSON.aspect, objJSON.near, objJSON.far);

						} else if (objJSON.type === "OrthographicCamera") {

							camera = new THREE.OrthographicCamera(objJSON.left, objJSON.right, objJSON.top, objJSON.bottom, objJSON.near, objJSON.far);

						}

						camera.name = objID;
						camera.position.fromArray(pos);

						if (quat !== undefined) {

							camera.quaternion.fromArray(quat);

						} else if (rot !== undefined) {

							camera.rotation.fromArray(rot);

						} else if (objJSON.target) {

							camera.lookAt(new THREE.Vector3().fromArray(objJSON.target));

						}

						parent.add(camera);

						result.cameras[objID] = camera;
						result.objects[objID] = camera;

						// pure Object3D

					} else {

						pos = objJSON.position;
						rot = objJSON.rotation;
						scl = objJSON.scale;
						quat = objJSON.quaternion;

						object = new THREE.Object3D();
						object.name = objID;
						object.position.fromArray(pos);

						if (quat) {

							object.quaternion.fromArray(quat);

						} else {

							object.rotation.fromArray(rot);

						}

						object.scale.fromArray(scl);
						object.visible = (objJSON.visible !== undefined) ? objJSON.visible : false;

						parent.add(object);

						result.objects[objID] = object;
						result.empties[objID] = object;

					}

					if (object) {

						if (objJSON.userData !== undefined) {

							for (var key in objJSON.userData) {

								var value = objJSON.userData[key];
								object.userData[key] = value;

							}

						}

						if (objJSON.groups !== undefined) {

							for (var i = 0; i < objJSON.groups.length; i++) {

								var groupID = objJSON.groups[i];

								if (result.groups[groupID] === undefined) {

									result.groups[groupID] = [];

								}

								result.groups[groupID].push(objID);

							}

						}

					}

				}

				if (object !== undefined && objJSON.children !== undefined) {

					handle_children(object, objJSON.children);

				}

			}

		}

		function handle_mesh(geo, mat, id) {

			result.geometries[id] = geo;
			result.face_materials[id] = mat;
			handle_objects();

		}

		function handle_hierarchy(node, id, parent, material, obj) {

			var p = obj.position;
			var r = obj.rotation;
			var q = obj.quaternion;
			var s = obj.scale;

			node.position.fromArray(p);

			if (q) {

				node.quaternion.fromArray(q);

			} else {

				node.rotation.fromArray(r);

			}

			node.scale.fromArray(s);

			// override children materials
			// if object material was specified in JSON explicitly

			if (material) {

				node.traverse(function (child) {

					child.material = material;

				});

			}

			// override children visibility
			// with root node visibility as specified in JSON

			var visible = (obj.visible !== undefined) ? obj.visible : true;

			node.traverse(function (child) {

				child.visible = visible;

			});

			parent.add(node);

			node.name = id;

			result.objects[id] = node;
			handle_objects();

		}

		function create_callback_geometry(id) {

			return function (geo, mat) {

				geo.name = id;

				handle_mesh(geo, mat, id);

				counter_models -= 1;

				scope.onLoadComplete();

				async_callback_gate();

			}

		}

		function create_callback_hierachy(id, parent, material, obj) {

			return function (event) {

				var result;

				// loaders which use EventDispatcher

				if (event.content) {

					result = event.content;

					// ColladaLoader

				} else if (event.dae) {

					result = event.scene;


					// UTF8Loader

				} else {

					result = event;

				}

				handle_hierarchy(result, id, parent, material, obj);

				counter_models -= 1;

				scope.onLoadComplete();

				async_callback_gate();

			}

		}

		function create_callback_embed(id) {

			return function (geo, mat) {

				geo.name = id;

				result.geometries[id] = geo;
				result.face_materials[id] = mat;

			}

		}

		function async_callback_gate() {

			var progress = {

				totalModels: total_models,
				totalTextures: total_textures,
				loadedModels: total_models - counter_models,
				loadedTextures: total_textures - counter_textures

			};

			scope.callbackProgress(progress, result);

			scope.onLoadProgress();

			if (counter_models === 0 && counter_textures === 0) {

				finalize();
				callbackFinished(result);

			}

		}

		function finalize() {

			// take care of targets which could be asynchronously loaded objects

			for (var i = 0; i < target_array.length; i++) {

				var ta = target_array[i];

				var target = result.objects[ta.targetName];

				if (target) {

					ta.object.target = target;

				} else {

					// if there was error and target of specified name doesn't exist in the scene file
					// create instead dummy target
					// (target must be added to scene explicitly as parent is already added)

					ta.object.target = new THREE.Object3D();
					result.scene.add(ta.object.target);

				}

				ta.object.target.userData.targetInverse = ta.object;

			}

		}

		var callbackTexture = function (count) {

			counter_textures -= count;
			async_callback_gate();

			scope.onLoadComplete();

		};

		// must use this instead of just directly calling callbackTexture
		// because of closure in the calling context loop

		var generateTextureCallback = function (count) {

			return function () {

				callbackTexture(count);

			};

		};

		function traverse_json_hierarchy(objJSON, callback) {

			callback(objJSON);

			if (objJSON.children !== undefined) {

				for (var objChildID in objJSON.children) {

					traverse_json_hierarchy(objJSON.children[objChildID], callback);

				}

			}

		}

		// first go synchronous elements

		// fogs

		var fogID, fogJSON;

		for (fogID in data.fogs) {

			fogJSON = data.fogs[fogID];

			if (fogJSON.type === "linear") {

				fog = new THREE.Fog(0x000000, fogJSON.near, fogJSON.far);

			} else if (fogJSON.type === "exp2") {

				fog = new THREE.FogExp2(0x000000, fogJSON.density);

			}

			color = fogJSON.color;
			fog.color.setRGB(color[0], color[1], color[2]);

			result.fogs[fogID] = fog;

		}

		// now come potentially asynchronous elements

		// geometries

		// count how many geometries will be loaded asynchronously

		var geoID, geoJSON;

		for (geoID in data.geometries) {

			geoJSON = data.geometries[geoID];

			if (geoJSON.type in this.geometryHandlers) {

				counter_models += 1;

				scope.onLoadStart();

			}

		}

		// count how many hierarchies will be loaded asynchronously

		for (var objID in data.objects) {

			traverse_json_hierarchy(data.objects[objID], function (objJSON) {

				if (objJSON.type && (objJSON.type in scope.hierarchyHandlers)) {

					counter_models += 1;

					scope.onLoadStart();

				}

			});

		}

		total_models = counter_models;

		for (geoID in data.geometries) {

			geoJSON = data.geometries[geoID];

			if (geoJSON.type === "cube") {

				geometry = new THREE.BoxGeometry(geoJSON.width, geoJSON.height, geoJSON.depth, geoJSON.widthSegments, geoJSON.heightSegments, geoJSON.depthSegments);
				geometry.name = geoID;
				result.geometries[geoID] = geometry;

			} else if (geoJSON.type === "plane") {

				geometry = new THREE.PlaneGeometry(geoJSON.width, geoJSON.height, geoJSON.widthSegments, geoJSON.heightSegments);
				geometry.name = geoID;
				result.geometries[geoID] = geometry;

			} else if (geoJSON.type === "sphere") {

				geometry = new THREE.SphereGeometry(geoJSON.radius, geoJSON.widthSegments, geoJSON.heightSegments);
				geometry.name = geoID;
				result.geometries[geoID] = geometry;

			} else if (geoJSON.type === "cylinder") {

				geometry = new THREE.CylinderGeometry(geoJSON.topRad, geoJSON.botRad, geoJSON.height, geoJSON.radSegs, geoJSON.heightSegs);
				geometry.name = geoID;
				result.geometries[geoID] = geometry;

			} else if (geoJSON.type === "torus") {

				geometry = new THREE.TorusGeometry(geoJSON.radius, geoJSON.tube, geoJSON.segmentsR, geoJSON.segmentsT);
				geometry.name = geoID;
				result.geometries[geoID] = geometry;

			} else if (geoJSON.type === "icosahedron") {

				geometry = new THREE.IcosahedronGeometry(geoJSON.radius, geoJSON.subdivisions);
				geometry.name = geoID;
				result.geometries[geoID] = geometry;

			} else if (geoJSON.type in this.geometryHandlers) {

				var loader = this.geometryHandlers[geoJSON.type]["loaderObject"];
				loader.load(get_url(geoJSON.url, data.urlBaseType), create_callback_geometry(geoID));

			} else if (geoJSON.type === "embedded") {

				var modelJson = data.embeds[geoJSON.id],
					texture_path = "";

				// pass metadata along to jsonLoader so it knows the format version

				modelJson.metadata = data.metadata;

				if (modelJson) {

					var jsonLoader = this.geometryHandlers["ascii"]["loaderObject"];
					var model = jsonLoader.parse(modelJson, texture_path);
					create_callback_embed(geoID)(model.geometry, model.materials);

				}

			}

		}

		// textures

		// count how many textures will be loaded asynchronously

		var textureID, textureJSON;

		for (textureID in data.textures) {

			textureJSON = data.textures[textureID];

			if (Array.isArray(textureJSON.url)) {

				counter_textures += textureJSON.url.length;

				for (var n = 0; n < textureJSON.url.length; n++) {

					scope.onLoadStart();

				}

			} else {

				counter_textures += 1;

				scope.onLoadStart();

			}

		}

		total_textures = counter_textures;

		for (textureID in data.textures) {

			textureJSON = data.textures[textureID];

			if (textureJSON.mapping !== undefined && THREE[textureJSON.mapping] !== undefined) {

				textureJSON.mapping = THREE[textureJSON.mapping];

			}

			var texture;

			if (Array.isArray(textureJSON.url)) {

				var count = textureJSON.url.length;
				var urls = [];

				for (var i = 0; i < count; i++) {

					urls[i] = get_url(textureJSON.url[i], data.urlBaseType);

				}

				var loader = THREE.Loader.Handlers.get(urls[0]);

				if (loader !== null) {

					texture = loader.load(urls, generateTextureCallback(count));

					if (textureJSON.mapping !== undefined)
						texture.mapping = textureJSON.mapping;

				} else {

					texture = new THREE.CubeTextureLoader().load(urls, generateTextureCallback(count));
					texture.mapping = textureJSON.mapping;

				}

			} else {

				var fullUrl = get_url(textureJSON.url, data.urlBaseType);
				var textureCallback = generateTextureCallback(1);

				var loader = THREE.Loader.Handlers.get(fullUrl);

				if (loader !== null) {

					texture = loader.load(fullUrl, textureCallback);

				} else {

					texture = new THREE.Texture();
					loader = new THREE.ImageLoader();

					(function (texture) {

						loader.load(fullUrl, function (image) {

							texture.image = image;
							texture.needsUpdate = true;

							textureCallback();

						});

					})(texture)


				}

				if (textureJSON.mapping !== undefined)
					texture.mapping = textureJSON.mapping;

				if (THREE[textureJSON.minFilter] !== undefined)
					texture.minFilter = THREE[textureJSON.minFilter];

				if (THREE[textureJSON.magFilter] !== undefined)
					texture.magFilter = THREE[textureJSON.magFilter];

				if (textureJSON.anisotropy) texture.anisotropy = textureJSON.anisotropy;

				if (textureJSON.repeat) {

					texture.repeat.set(textureJSON.repeat[0], textureJSON.repeat[1]);

					if (textureJSON.repeat[0] !== 1) texture.wrapS = THREE.RepeatWrapping;
					if (textureJSON.repeat[1] !== 1) texture.wrapT = THREE.RepeatWrapping;

				}

				if (textureJSON.offset) {

					texture.offset.set(textureJSON.offset[0], textureJSON.offset[1]);

				}

				// handle wrap after repeat so that default repeat can be overriden

				if (textureJSON.wrap) {

					var wrapMap = {
						"repeat": THREE.RepeatWrapping,
						"mirror": THREE.MirroredRepeatWrapping
					};

					if (wrapMap[textureJSON.wrap[0]] !== undefined) texture.wrapS = wrapMap[textureJSON.wrap[0]];
					if (wrapMap[textureJSON.wrap[1]] !== undefined) texture.wrapT = wrapMap[textureJSON.wrap[1]];

				}

			}

			result.textures[textureID] = texture;

		}

		// materials

		var matID, matJSON;
		var parID;

		for (matID in data.materials) {

			matJSON = data.materials[matID];

			for (parID in matJSON.parameters) {

				if (parID === "envMap" || parID === "map" || parID === "lightMap" || parID === "bumpMap" || parID === "normalMap" || parID === "alphaMap") {

					matJSON.parameters[parID] = result.textures[matJSON.parameters[parID]];

				} else if (parID === "shading") {

					matJSON.parameters[parID] = (matJSON.parameters[parID] === "flat") ? THREE.FlatShading : THREE.SmoothShading;

				} else if (parID === "side") {

					if (matJSON.parameters[parID] == "double") {

						matJSON.parameters[parID] = THREE.DoubleSide;

					} else if (matJSON.parameters[parID] == "back") {

						matJSON.parameters[parID] = THREE.BackSide;

					} else {

						matJSON.parameters[parID] = THREE.FrontSide;

					}

				} else if (parID === "blending") {

					matJSON.parameters[parID] = matJSON.parameters[parID] in THREE ? THREE[matJSON.parameters[parID]] : THREE.NormalBlending;

				} else if (parID === "combine") {

					matJSON.parameters[parID] = matJSON.parameters[parID] in THREE ? THREE[matJSON.parameters[parID]] : THREE.MultiplyOperation;

				} else if (parID === "vertexColors") {

					if (matJSON.parameters[parID] == "face") {

						matJSON.parameters[parID] = THREE.FaceColors;

						// default to vertex colors if "vertexColors" is anything else face colors or 0 / null / false

					} else if (matJSON.parameters[parID]) {

						matJSON.parameters[parID] = THREE.VertexColors;

					}

				} else if (parID === "wrapRGB") {

					var v3 = matJSON.parameters[parID];
					matJSON.parameters[parID] = new THREE.Vector3(v3[0], v3[1], v3[2]);

				} else if (parID === "normalScale") {

					var v2 = matJSON.parameters[parID];
					matJSON.parameters[parID] = new THREE.Vector2(v2[0], v2[1]);

				}

			}

			if (matJSON.parameters.opacity !== undefined && matJSON.parameters.opacity < 1.0) {

				matJSON.parameters.transparent = true;

			}

			material = new THREE[matJSON.type](matJSON.parameters);
			material.name = matID;

			result.materials[matID] = material;

		}

		// second pass through all materials to initialize MultiMaterials
		// that could be referring to other materials out of order

		for (matID in data.materials) {

			matJSON = data.materials[matID];

			if (matJSON.parameters.materials) {

				var materialArray = [];

				for (var i = 0; i < matJSON.parameters.materials.length; i++) {

					var label = matJSON.parameters.materials[i];
					materialArray.push(result.materials[label]);

				}

				result.materials[matID].materials = materialArray;

			}

		}

		// objects ( synchronous init of procedural primitives )

		handle_objects();

		// defaults

		if (result.cameras && data.defaults.camera) {

			result.currentCamera = result.cameras[data.defaults.camera];

		}

		if (result.fogs && data.defaults.fog) {

			result.scene.fog = result.fogs[data.defaults.fog];

		}

		// synchronous callback

		scope.callbackSync(result);

		// just in case there are no async elements

		async_callback_gate();

	}

};