var neighborData = [
	{
		"position": [-1, 0, 0],
		"corners": [
			[0, 0, 0, 0, 0],
			[0, 0, 1, 1, 0],
			[0, 1, 0, 0, 1],
			[0, 1, 1, 1, 1]
		]
	},
	{
		"position": [1, 0, 0],
		"corners": [
			[1, 0, 1, 0, 0],
			[1, 0, 0, 1, 0],
			[1, 1, 1, 0, 1],
			[1, 1, 0, 1, 1]
		]
	},
	{
		"position": [0, -1, 0],
		"corners": [
			[0, 0, 0, 0, 1],
			[1, 0, 0, 1, 1],
			[0, 0, 1, 0, 0],
			[1, 0, 1, 1, 0]
		]
	},
	{
		"position": [0, 1, 0],
		"corners": [
			[0, 1, 1, 0, 0],
			[1, 1, 1, 1, 0],
			[0, 1, 0, 0, 1],
			[1, 1, 0, 1, 1]
		]
	},
	{
		"position": [0, 0, -1],
		"corners": [
			[1, 0, 0, 0, 0],
			[0, 0, 0, 1, 0],
			[1, 1, 0, 0, 1],
			[0, 1, 0, 1, 1]
		]
	},
	{
		"position": [0, 0, 1],
		"corners": [
			[0, 0, 1, 0, 0],
			[1, 0, 1, 1, 0],
			[0, 1, 1, 0, 1],
			[1, 1, 1, 1, 1]
		]
	}
];

var timePalette = {
	"21600": 0x081632, // Marine Blue (#081632)
	"25200": 0xFF7656,
	"28800": 0xBDE4F5,
	"61200": 0xBDE4F5,
	"64800": 0xFA88AF,
	"68400": 0x081632
};

var textureData = {
	"resolution": [16, 16],
	"size": [256, 256],
	"textures": {

	}
};

class World extends global.three.Scene {
	constructor (parameters) {
		super();

		this.generator = parameters.generator || "flat";

		this.chunks = new global.Map();
		this.chunkSize = parameters.chunkSize || [16, 16, 16];

		this.background = new global.three.Color(0xBDE4F5);

		this.ambientLight = new global.three.AmbientLight(0xFFFFFF, 1);
		this.directionalLight = new global.three.DirectionalLight(0xFFFFFF, 1);

		this.directionalLight.position.set(1, 1, 1);

		this.add(this.ambientLight);
		this.add(this.directionalLight);

		this.worker = new global.Worker("./modules/world_worker.js");

		this.callbackObjects = {};

		global.core.on("resourcepack-load", (function (pack) {
			var textureData = {
				"resolution": pack.meta.textures["block-atlas"].size,
				"size": pack.meta.textures["block-atlas"].totalSize,
				"textures": pack.meta.textures
			};

			this.worker.postMessage({
				"command": "config",
				"arguments": {
					"textureData": textureData
				}
			});

			this.chunks.forEach((chunk, chunkID) => {
				chunk.material.map = pack.textures.get("block-atlas").map;
			});
		}).bind(this));

		var resourcePackNames = Object.keys(global.resourcePacks);
		if (resourcePackNames.length > 0) {
			var pack = global.resourcePacks[resourcePackNames[0]];

			var textureData = {
				"resolution": pack.meta.textures["block-atlas"].size,
				"size": pack.meta.textures["block-atlas"].totalSize,
				"textures": pack.meta.textures
			};

			this.worker.postMessage({
				"command": "config",
				"arguments": {
					"world": {
						"chunkSize": this.chunkSize
					},
					"textureData": textureData
				}
			});
		}

		this.worker.postMessage({
			"command": "create-world",
			"arguments": {
				"uuid": this.uuid
			}
		});

		this.worker.onmessage = (function (message) {
			var event = message.data["event"];
			var data = message.data["data"];

			var callbackID = data["callbackID"];

			var callbackObject = this.callbackObjects[callbackID];

			switch (event) {
				case "generated":
					var newChunk = new Chunk(this.chunkSize, this);

					for (var b = 0; b < data.blocks.length; b++) {
						var block = data.blocks[b];

						if (!block) continue;
						newChunk.blocks[b] = block;
					}

					this.setChunk(newChunk, callbackObject.position);
					callbackObject.resolve(newChunk);
					break;
				case "constructed":
					var properties = data.properties;
					var chunk = callbackObject.chunk;

					chunk.geometry.setAttribute("position", new global.three.Float32BufferAttribute(properties["positions"], 3));
					chunk.geometry.setAttribute("normal", new global.three.Float32BufferAttribute(properties["normals"], 3));
					chunk.geometry.setAttribute("uv", new global.three.Float32BufferAttribute(properties["uvs"], 2));
					chunk.geometry.setIndex(properties["indexes"]);
					chunk.geometry.computeBoundingSphere();

					var resourcePackNames = Object.keys(global.resourcePacks);
					if (resourcePackNames.length > 0) {
						var pack = global.resourcePacks[resourcePackNames[0]];

						chunk.material.map = pack.textures.get("block-atlas").map;
					}

					// this.worker.postMessage({
					// 	"command": "set-body",
					// 	"arguments": {
					// 		"uuid": chunk.uuid,
					// 		"position": [
					// 			chunk.position.x,
					// 			chunk.position.y,
					// 			chunk.position.z
					// 		],
					// 		"shape": {
					// 			"vertices": chunk.geometry.attributes["position"].array,
					// 			"indices": chunk.geometry.index.array
					// 		},
					// 		"type": "static"
					// 	}
					// });

					callbackObject.resolve(chunk);
					break;
				case "simulation-stepped":
					for (var uuid in data.updates) {
						if (data.updates.hasOwnProperty(uuid)) {
							var meshUpdate = data.updates[uuid];

							var mesh = this.children.find(function (m) {
								return m.uuid === uuid;
							});

							if (!mesh) continue;

							mesh.position.set(
								meshUpdate.position[0],
								meshUpdate.position[1],
								meshUpdate.position[2]
							);

							mesh.quaternion.set(
								meshUpdate.quaternion[0],
								meshUpdate.quaternion[1],
								meshUpdate.quaternion[2],
								meshUpdate.quaternion[3]
							);
						}
					}

					callbackObject.resolve(data.updates);
					break;
				default:

			}
		}).bind(this);
	}
	addChunk (chunk) {
		if (!chunk) return new Error("First argument is not a chunk. ");
		if (!(chunk instanceof Chunk)) return new Error("First argument is not an instance of Chunk. ");

		this.add(chunk);

		var blockIndices = [];

		for (var b = 0; b < chunk.blocks.length; b++) {
			if (chunk.blocks[b]) blockIndices.push(b);
		}

		// this.worker.postMessage({
		// 	"command": "put-voxels",
		// 	"arguments": {
		// 		"worldUUID": this.uuid,
		// 		"chunkUUID": chunk.uuid,
		// 		"chunkPosition": chunk.position,
		// 		"indices": blockIndices
		// 	}
		// });


		return chunk;
	}
	setChunk (chunk, position) {
		if (!chunk) return new Error("First argument is not a chunk. ");
		if (!(chunk instanceof Chunk)) return new Error("First argument is not an instance of Chunk. ");
		if (!position) return new Error("Second argument is not a position. ");

		var worldPosition = [
			this.chunkSize[0] * position[0],
			this.chunkSize[1] * position[1],
			this.chunkSize[2] * position[2]
		];

		if (this.chunks.has(position.join(":"))) {
			var oldChunk = this.chunks.get(position.join(":"));

			var voxelIndices = [];

			for (var b = 0; b < oldChunk.blocks.length; b++) {
				if (oldChunk.blocks[b]) voxelIndices.push(b);
			}

			// this.worker.postMessage({
			// 	"command": "remove-voxels",
			// 	"arguments": {
			// 		"worldUUID": this.uuid,
			// 		"chunkUUID": oldChunk.uuid
			// 	}
			// });
		}

		var blockIndices = [];

		for (var b = 0; b < chunk.blocks.length; b++) {
			if (chunk.blocks[b]) blockIndices.push(b);
		}

		// this.worker.postMessage({
		// 	"command": "put-voxels",
		// 	"arguments": {
		// 		"worldUUID": this.uuid,
		// 		"chunkUUID": chunk.uuid,
		// 		"chunkPosition": position,
		// 		"indices": blockIndices
		// 	}
		// });

		chunk.position.set(
			worldPosition[0],
			worldPosition[1],
			worldPosition[2]
		);

		// this.worker.postMessage({
		// 	"command": "set-body",
		// 	"arguments": {
		// 		"uuid": chunk.uuid,
		// 		"position": worldPosition
		// 	}
		// });

		return this.chunks.set(position.join(":"), chunk);
	}
	addEntity (entity) {
		if (!entity) return new Error("First argument is not an entity. ");
		if (!(entity instanceof Entity)) return new Error("First argument is not an instance of Entity. ");

		this.add(entity);

		this.worker.postMessage({
			"command": "add-body",
			"arguments": {
				"worldUUID": this.uuid,
				"bodyUUID": entity.uuid,
				"type": "dynamic"
			}
		});

		return entity;
	}
	removeChunk (position) {
		if (!position) return new Error("First argument is not a position. ");

		if (this.chunks.has(position.join(":"))) return this.chunks.delete(position.join(":"));
		return false;
	}
	generateChunk (position, data) {
		return new Promise((function (resolve, reject) {
			var callbackID = "G" + Math.round(Math.random() * 65536);

			this.callbackObjects[callbackID] = {
				"position": position,
				"resolve": resolve,
				"reject": reject
			};

			this.worker.postMessage({
				"command": "generate",
				"arguments": {
					"callbackID": callbackID,
					"position": position,
					"generator": this.generator
				}
			});
		}).bind(this));
	}
	constructChunk (position) {
		return new Promise((function (resolve, reject) {
			var callbackID = "C" + Math.round(Math.random() * 65536);

			this.callbackObjects[callbackID] = {
				"chunk": this.chunks.get(position.join(":")),
				"position": position,
				"resolve": resolve,
				"reject": reject
			};

			var neighbors = [];

			for (var n = 0; n < neighborData.length; n++) {
				var neighborPosition = addPositions(position, neighborData[n].position);
				var neighborKey = neighborPosition.join(":");

				if (this.chunks.has(neighborKey)) {
					neighbors.push(this.chunks.get(neighborKey).toJSON());
				} else {
					neighbors.push(null);
				}
			}

			this.worker.postMessage({
				"command": "construct",
				"arguments": {
					"callbackID": callbackID,
					"position": position,
					"chunk": this.chunks.get(position.join(":")).toJSON(),
					"neighbors": neighbors
				}
			});
		}).bind(this));
	}
	stepSimulation (delta) {
		return new Promise((function (resolve, reject) {
			var callbackID = "C" + Math.round(Math.random() * 65536);

			this.callbackObjects[callbackID] = {
				"resolve": resolve,
				"reject": reject
			};

			this.worker.postMessage({
				"command": "step-simulation",
				"arguments": {
					"callbackID": callbackID,
					"uuid": this.uuid,
					"delta": delta
				}
			});
		}).bind(this));
	}
	toJSON () {
		var chunksObject = {};

		this.chunks.forEach(function (value, key) {
			chunksObject[key] = value;
		});

		return {
			"generator": this.generator,
			"chunks": chunksObject
		};
	}
}

class Chunk extends global.three.Mesh {
	constructor (size = [16, 16, 16], world) {
		super(
			new global.three.BufferGeometry(),
			new global.three.MeshStandardMaterial({
				"side": global.three.DoubleSide,
				"transparent": true,
				"alphaTest": 0.5
			})
		);

		this._size = size;
		this.blocks = new global.Array(size[0] * size[1] * size[2]);

		if (world instanceof World) {
			this.world = world;
			// world.worker.postMessage({
			// 	"command": "create-body",
			// 	"arguments": {
			// 		"uuid": this.uuid,
			// 		"type": "static"
			// 	}
			// });
		}
	}
	get size () {
		return this._size;
	}
	setBlock (position, block) {
		if (!position) return new Error("First argument is not a position. ");
		if (!block) return new Error("Second argument is not a block. ");
		if (!(block instanceof Block)) return new Error("Second argument is not an instance of Block. ");

		var blockIndex = computeBlockIndex(this.size, position);

		if (this.world) {
			// this.world.worker.postMessage({
			// 	"command": "remove-body",
			// 	"arguments": {
			// 		"uuid": this.uuid + ";" + position.join(":")
			// 	}
			// });
			if (!this.blocks[blockIndex]) {
				this.world.worker.postMessage({
					"command": "create-body",
					"arguments": {
						"uuid": this.uuid + ";" + blockIndex,
						"type": "static",
						"position": [
							this.position.x + position[0],
							this.position.y + position[1],
							this.position.z + position[2],
						]
					}
				});
			}
		}

		this.blocks[blockIndex] = block;
	}
	removeBlock (position) {
		if (!position) return new Error("First argument is not a position. ");

		var blockIndex = computeBlockIndex(this.size, position);

		delete this.blocks[blockIndex];

		if (this.world) {
			this.world.worker.postMessage({
				"command": "remove-body",
				"arguments": {
					"worldUUID": this.world.uuid,
					"bodyUUID": this.uuid + ";" + blockIndex
				}
			});
			this.world.worker.postMessage({
				"command": "destroy-body",
				"arguments": {
					"uuid": this.uuid + ";" + blockIndex
				}
			});
		}
	}
	toJSON () {
		return {
			"size": this.size,
			"blocks": this.blocks
		};
	}
}

class Block extends global.Array {
	constructor (type, data = {}) {
		super();

		if (!type) return new Error("Block type must be non-null. ");
		this[0] = type;
		this[1] = new global.Map();

		for (var key in data) {
			if (data.hasOwnProperty(key)) {
				var value = data[key];

				this[1].set(key, value);
			}
		}
	}
	get type () {
		return this[0];
	}
	get tags () {
		return this[1];
	}
	setTagValue (tag, value) {
		return this[1].set(tag, value);
	}
	removeTagValue (tag) {
		if (this[1].has(tag)) return this[1].delete(tag);
		return false;
	}
	toJSON () {
		var tagsObject = {};

		this[1].forEach(function (value, key) {
			tagsObject[key] = value;
		});

		return {
			"type": this[0],
			"tags": tagsObject
		};
	}
}

class Entity extends global.three.Mesh {
	constructor (type, world) {
		super(
			new global.three.BoxBufferGeometry(0.8, 0.8, 0.8),
			new global.three.MeshStandardMaterial({
				"side": global.three.DoubleSide,
				"transparent": true,
				"alphaTest": 0.5,
				"opacity": 0.0,
				"wireframe": true
			})
		);

		this.type = type;
		this.parts = {};

		this._texture = null;

		for (var p = 0; p < global.resourcePacks.length; p++) {
			var pack = global.resourcePacks[p];

			if (pack.models.has(this.type) && Object.keys(this.parts).length < 1) {
				this.model = pack.models.get(this.type);
			}
			if (pack.textures.has(this.type) && !this.material.map) {
				this.texture = pack.textures.get(this.type).map;
			}
		}

		if (world instanceof World) {
			world.worker.postMessage({
				"command": "create-body",
				"arguments": {
					"uuid": this.uuid,
					"mass": 16,
					"shape": {
						"vertices": this.geometry.attributes["position"].array,
						"indices": this.geometry.index.array
					}
				}
			});
		}
	}
	get texture () {
		if (this._texture) return this._texture;

		for (var partName in this.parts) {
			if (this.parts.hasOwnProperty(partName)) {
				var part = this.parts[partName];

				if (!part.material || !part.material.map) continue;
				return part.material.map;
			}
		}

		return null;
	}
	set texture (texture) {
		this._texture = texture;

		for (var partName in this.parts) {
			if (this.parts.hasOwnProperty(partName)) {
				var part = this.parts[partName];

				if (part.isGroup) continue;

				part.material.map = texture;
			}
		}
	}
	set model (model) {
		this.geometry = new global.three.BoxBufferGeometry(
			model.hitbox[0],
			model.hitbox[1],
			model.hitbox[2]
		);

		for (var partName in model.parts) {
			if (model.parts.hasOwnProperty(partName)) {
				var part = model.parts[partName];

				let partObject;

				switch (part.type) {
					case "group":
						partObject = new global.three.Group();
						break;
					case "geometry":
						partObject = new global.three.Mesh(
							new global.three.BoxBufferGeometry(
								part.geometry[0],
								part.geometry[1],
								part.geometry[2]
							),
							new global.three.MeshStandardMaterial({
								"side": global.three.FrontSide,
								"transparent": true,
								"alphaTest": 0.5,
								"map": this.texture
							})
						);

						if ("uvMap" in part) {
							var uvArray = partObject.geometry.attributes["uv"].array;

							for (var f = 0; f < part.uvMap.length; f++) {
								var faceMap = part.uvMap[f];

								var firstUVIndex = f * 8;

								uvArray[firstUVIndex + 0] = faceMap[0][0];
								uvArray[firstUVIndex + 1] = faceMap[1][1];
								uvArray[firstUVIndex + 2] = faceMap[1][0];
								uvArray[firstUVIndex + 3] = faceMap[1][1];
								uvArray[firstUVIndex + 4] = faceMap[0][0];
								uvArray[firstUVIndex + 5] = faceMap[0][1];
								uvArray[firstUVIndex + 6] = faceMap[1][0];
								uvArray[firstUVIndex + 7] = faceMap[0][1];
							}
						}
						break;
					default:

				}

				if (part.position) partObject.position.set(
					part.position[0],
					part.position[1],
					part.position[2]
				);

				var modelPart = this.parts[partName] = partObject;
			}
		}

		for (var partName in model.parts) {
			if (model.parts.hasOwnProperty(partName)) {
				var part = model.parts[partName];

				if (part.children) {
					for (var c = 0; c < part.children.length; c++) {
						this.parts[partName].add(this.parts[part.children[c]]);
					}
				}
			}
		}

		if ("root" in this.parts) this.add(this.parts["root"]);
	}
	setPosition (position) {
		this.position.set(
			position[0],
			position[1],
			position[2]
		);

		if (this.parent instanceof World) {
			this.parent.worker.postMessage({
				"command": "set-body",
				"arguments": {
					"uuid": this.uuid,
					"position": position
				}
			});
		}
	}
}

var computeBlockIndex = function (chunkSize, blockPosition) {
	var x = blockPosition[0] % chunkSize[0];
	var z = blockPosition[2] % chunkSize[2];
	var y = blockPosition[1] % chunkSize[1];

	return (x * (chunkSize[1] * chunkSize[2])) + y + (z * chunkSize[1]);
};

var computeBlockPosition = function (chunkSize, blockIndex) {
	var blockPosition = [0, 0, 0];
	var subtractedIndex = 0 + blockIndex;

	blockPosition[1] = subtractedIndex % chunkSize[1];
	subtractedIndex -= blockPosition[1];

	blockPosition[2] = (subtractedIndex / chunkSize[1]) % chunkSize[2];
	subtractedIndex -= blockPosition[2] * chunkSize[1];

	blockPosition[0] = (subtractedIndex / (chunkSize[1] * chunkSize[2])) % chunkSize[0];
	subtractedIndex -= blockPosition[0] * chunkSize[0];

	return blockPosition;
};

var splitRGBComponents = function (hex) {
	var subtracted = hex;

	var blue = hex % 0x100;
	subtracted -= blue;

	var green = (subtracted / 0x100) % 0x100;
	subtracted -= subtracted * 0x100;

	var red = (subtracted / 0x10000) % 0x100;
	subtracted -= subtracted * 0x10000;

	return [red, green, blue];
};

var computeTimeColor = function (palette, time) {
	var times = Object.keys(palette);
	if (times.length == 1) return palette[times[0]];

	var seconds = ((time[0] * 3600) + (time[1] * 60) + time[2]);
	var timesBefore = times.filter(function (timeFloat) {
		return parseFloat(timeFloat) <= seconds;
	});
	var timesAfter = times.filter(function (timeFloat) {
		return parseFloat(timeFloat) > seconds;
	});
	let startTime, startTimeFloat;
	let endTime, endTimeFloat;
	if (timesBefore.length < 1) {
		startTime = times[times.length - 1];
		startTimeFloat = parseFloat(times[times.length - 1]) - (24 * 3600);
	}
	if (timesAfter.length < 1) {
		endTime = times[0];
		endTimeFloat = (24 * 3600) + parseFloat(times[0]);
	}

	var startComponents = splitRGBComponents(palette[startTime]);
	var endComponents = splitRGBComponents(palette[endTime]);

	var redDiff = endComponents[0] - startComponents[0];
	var greenDiff = endComponents[1] - startComponents[1];
	var blueDiff = endComponents[2] - startComponents[2];

	progress = (seconds - startTimeFloat) / (endTimeFloat - startTimeFloat);

	return (Math.round(startComponents[0] + (progress * redDiff)) * 65536)
		+ (Math.round((startComponents[1] + (progress * greenDiff))) * 256)
		+ Math.round(startComponents[2] + (progress * blueDiff));
};

var addPositions = function (positionA, positionB) {
	var position = new Array(positionA.length);

	for (var p = 0; p < position.length; p++) {
		position[p] = positionA[p] + positionB[p];
	}

	return position;
};

module.exports = {
	"World": World,
	"Chunk": Chunk,
	"Block": Block,
	"Entity": Entity,
	"utils": {
		"computeBlockIndex": computeBlockIndex,
		"computeBlockPosition": computeBlockPosition,
		"splitRGBComponents": splitRGBComponents,
		"computeTimeColor": computeTimeColor,
		"addPositions": addPositions
	}
};
