importScripts("../lib/improved-noise.js");
importScripts("../lib/cannon-es.js");
importScripts("../lib/ammo.js");

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]
		]
	}
];

let worldConfig = {
	"chunkSize": [16, 16, 16]
};

let textureData = {
	"resolution": [16, 16],
	"size": [256, 256],
	"blocks": {

	}
};

var tasks = {
	"physics": {
		"running": false,
		"interval": setInterval(async function () {
			if (!tasks["physics"].running && (tasks["physics"].queue.length > 0)) {
				tasks["physics"].running = true;
				while (tasks["physics"].queue.length > 0) {
					tasks["physics"].queue[0].run(...tasks["physics"].queue[0].arguments);
					tasks["physics"].queue.splice(0, 1);
				}
				tasks["physics"].running = false;
			}
		}, 256),
		"queue": []
	}
};

var ammoIsReady = false;

let world;

var worlds = {};

var bodies = {};

var onmessage = function (event) {
	var command = event.data["command"];
	var arguments = event.data["arguments"];

	var callbackID = arguments["callbackID"];

	switch (command) {
		case "config":
			if (arguments["world"]) worldConfig = arguments["world"];
			textureData = arguments["textureData"];
			break;
		case "generate":
			var generator = self.generators[arguments["generator"]];
			var data = arguments["data"];

			if (!generator) return postMessage({
				"event": "generated",
				"data": {
					"callbackID": callbackID,
					"blocks": []
				}
			});

			var position = arguments["position"];

			var blocks = [];

			var chunkWidth = worldConfig.chunkSize[0];
			var chunkHeight = worldConfig.chunkSize[1];
			var chunkDepth = worldConfig.chunkSize[2];

			switch (generator.type) {
				case "stepped":
					for (var x = 0; x < chunkWidth; x++) {
						for (var z = 0; z < chunkDepth; z++) {
							for (var y = 0; y < chunkHeight; y++) {
								var block = generator.run([
									(position[0] * chunkWidth) + x,
									(position[1] * chunkHeight) + y,
									(position[2] * chunkDepth) + z
								], data, worldConfig.chunkSize);
								var blockIndex = computeBlockIndex(
									worldConfig.chunkSize,
									[
										x,
										y,
										z
									]
								);

								if (block) blocks[blockIndex] = block;
							}
						}
					}
					break;
				case "full":
					var blocks = generator.run(position, data, worldConfig.chunkSize);
					break;
				default:

			}

			postMessage({
				"event": "generated",
				"data": {
					"callbackID": callbackID,
					"blocks": blocks
				}
			});
			break;
		case "construct":
			var chunk = arguments["chunk"];
			var neighbors = arguments["neighbors"];

			postMessage({
				"event": "constructed",
				"data": {
					"callbackID": callbackID,
					"properties": construct(chunk, neighbors)
				}
			});
			break;
		case "create-world":
			// worlds[arguments["uuid"]] = new CANNON.World({
			// 	"allowSleep": arguments["allowSleep"] || true,
			// 	"gravity": ("gravity" in arguments) ? new CANNON.Vec3(
			// 		arguments["gravity"][0],
			// 		arguments["gravity"][1],
			// 		arguments["gravity"][2]
			// 	) : new CANNON.Vec3(0, -9.82, 0),
			// 	"solver": new CANNON.GSSolver()
			// });

			var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
			var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
			var overlappingPairCache = new Ammo.btDbvtBroadphase();
			var solver = new Ammo.btSequentialImpulseConstraintSolver();

			world = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
			world.setGravity(new Ammo.btVector3(0, -9.82, 0));

			postMessage({
				"event": "world-created",
				"data": {
					"callbackID": callbackID
				}
			});
			break;
		case "create-body":
			let bodyType;

			switch (arguments["type"]) {
				case "dynamic":
					bodyType = CANNON.Body.DYNAMIC;
					break;
				case "static":
					bodyType = CANNON.Body.STATIC;
					break;
				case "kinematic":
					bodyType = CANNON.Body.KINEMATIC;
					break;
				default:
					bodyType = CANNON.Body.DYNAMIC;
			}

			var bodyOptions = {
				"allowSleep": arguments["allowSleep"] || true,
				"fixedRotation": arguments["fixedRotation"] || false,
				"mass": arguments["mass"] || 0,
				"position": ("position" in arguments) ? new CANNON.Vec3(
					arguments["position"][0],
					arguments["position"][1],
					arguments["position"][2]
				) : new CANNON.Vec3(0, 0, 0),
				"quaternion": ("quaternion" in arguments) ? new CANNON.Quaternion(
					arguments["quaternion"][0],
					arguments["quaternion"][1],
					arguments["quaternion"][2],
					arguments["quaternion"][3]
				) : new CANNON.Quaternion(0, 0, 0, 1),
				"type": bodyType,
				"material": new CANNON.Material()
				/*"shape": ("shape" in arguments) ? new CANNON.ConvexPolyhedron({
					"vertices": (function (positions) {
						var vertices = [];

						for (var v = 0; v < (positions.length / 3); v++) {
							var vertexOffset = v * 3;

							vertices[v] = new CANNON.Vec3(
								positions[vertexOffset + 0],
								positions[vertexOffset + 1],
								positions[vertexOffset + 2]
							);
						}

						return vertices;
					})(arguments["shape"].vertices),
					"faces": (function (indices) {
						var faces = [];

						for (var f = 0; f < (indices.length / 3); f++) {
							var faceOffset = f * 3;

							faces[f] = indices.slice(faceOffset, faceOffset + 3);
						}

						return faces;
					})(arguments["shape"].indices)
				}) : new CANNON.Box(new CANNON.Vec3(1, 1, 1))*/
			};

			var shape = ("shape" in arguments) ? new CANNON.Trimesh(
				arguments["shape"].vertices,
				arguments["shape"].indices
			) : new CANNON.Box(new CANNON.Vec3(1, 1, 1));

			postMessage({
				"event": "body-created",
				"data": {
					"callbackID": callbackID
				}
			});

			bodies[arguments["uuid"]] = (shape instanceof CANNON.Trimesh) ? trimeshToPolyhedron(bodyOptions, shape) : new CANNON.Body(bodyOptions);

			if (shape instanceof CANNON.Box) bodies[arguments["uuid"]].addShape(shape);

			bodies[arguments["uuid"]].uuid = arguments["uuid"];

			break;
		case "set-body":
			if ("shape" in arguments) {
				var convexPolyhedronBody = trimeshToPolyhedron({}, new CANNON.Trimesh(
					arguments["shape"].vertices,
					arguments["shape"].indices
				));

				for (var s = bodies[arguments["uuid"]].shapes.length - 1; s >= 0; s--) {
					bodies[arguments["uuid"]].removeShape(bodies[arguments["uuid"]].shapes[s]);
				}

				for (var s = 0; s < convexPolyhedronBody.shapes.length; s++) {
					bodies[arguments["uuid"]].addShape(convexPolyhedronBody.shapes[s]);
				}

				bodies[arguments["uuid"]].updateBoundingRadius();
				bodies[arguments["uuid"]].updateAABB();
				/*bodies[arguments["uuid"]].shapes[0] = new CANNON.ConvexPolyhedron({
					"vertices": (function (positions) {
						var vertices = [];

						for (var v = 0; v < (positions.length / 3); v++) {
							var vertexOffset = v * 3;

							vertices[v] = new CANNON.Vec3(
								positions[vertexOffset + 0],
								positions[vertexOffset + 1],
								positions[vertexOffset + 2]
							);
						}

						return vertices;
					})(arguments["shape"].vertices),
					"faces": (function (indices) {
						var faces = [];

						for (var f = 0; f < (indices.length / 3); f++) {
							var faceOffset = f * 3;

							faces[f] = indices.slice(faceOffset, faceOffset + 3);
						}

						return faces;
					})(arguments["shape"].indices)
				});*/
			}

			if ("position" in arguments) {
				bodies[arguments["uuid"]].position.set(
					arguments["position"][0],
					arguments["position"][1],
					arguments["position"][2]
				);
				bodies[arguments["uuid"]].aabbNeedsUpdate = true;
			}

			if ("quaternion" in arguments) {
				bodies[arguments["uuid"]].quaternion.set(
					arguments["quaternion"][0],
					arguments["quaternion"][1],
					arguments["quaternion"][2],
					arguments["quaternion"][3]
				);
			}
			break;
		case "add-body":
			worlds[arguments["worldUUID"]].addBody(bodies[arguments["bodyUUID"]]);

			postMessage({
				"event": "body-added",
				"data": {
					"callbackID": callbackID
				}
			});
			break;
		case "step-simulation":
			worlds[arguments["uuid"]].step(arguments["delta"]);

			var updates = {};

			for (var b = 0; b < worlds[arguments["uuid"]].bodies.length; b++) {
				var body = worlds[arguments["uuid"]].bodies[b];

				updates[body.uuid] = {
					"position": body.position.toArray(),
					"quaternion": body.quaternion.toArray()
				}
			}

			postMessage({
				"event": "simulation-stepped",
				"data": {
					"callbackID": callbackID,
					"updates": updates
				}
			});
			break;
		case "remove-body":
			worlds[arguments["worldUUID"]].removeBody(bodies[arguments["bodyUUID"]]);

			postMessage({
				"event": "body-removed",
				"data": {
					"callbackID": callbackID
				}
			});
			break;
		case "destroy-body":
			delete bodies[arguments["uuid"]];

			postMessage({
				"event": "body-destroyed",
				"data": {
					"callbackID": callbackID
				}
			});
			break;
		case "put-voxels":
			tasks["physics"].queue.push({
				"run": function (args) {
					var worldUUID = args["worldUUID"];
					var chunkUUID = args["chunkUUID"];
					var chunkPosition = args["chunkPosition"];
					var voxelIndices = args["indices"];

					var bodyOptions = {
						"mass": 0,
						"type": CANNON.Body.STATIC,
						"material": new CANNON.Material(),
						"position": new CANNON.Vec3(
							chunkPosition[0] * worldConfig.chunkSize[0],
							chunkPosition[1] * worldConfig.chunkSize[1],
							chunkPosition[2] * worldConfig.chunkSize[2]
						)
					};

					var chunk = bodies[chunkUUID] = new CANNON.Body(bodyOptions);

					var shapes = [];

					for (var i = 0; i < voxelIndices.length; i++) {
						var shape = shapes[i] = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
						var shapePosition = computeBlockPosition(worldConfig.chunkSize, voxelIndices[i]);

						chunk.addShape(shape, new CANNON.Vec3(
							shapePosition[0],
							shapePosition[1],
							shapePosition[2]
						));
					}

					worlds[worldUUID].addBody(chunk);
				},
				"arguments": [arguments]
			});
			break;
		case "remove-voxels":
			tasks["physics"].queue.push({
				"run": function (args) {
					var worldUUID = args["worldUUID"];
					var chunkUUID = args["chunkUUID"];

					worlds[worldUUID].removeBody(bodies[chunkUUID]);

					delete bodies[chunkUUID];
				},
				"arguments": [arguments]
			});
			break;
		case "destroy-world":
			delete worlds[arguments["uuid"]];

			postMessage({
				"event": "world-destroyed",
				"data": {
					"callbackID": callbackID
				}
			});
			break;
		default:

	}
};

self.generators = {
	"flat": {
		"type": "stepped",
		"run": function (position, data = {}) {
			if (position[1] >= 0 && position[1] <= 5) return ["glitch", {}];
			if (position[1] >= 6 && position[1] <= 57) return ["stone", {}];
			if (position[1] >= 58 && position[1] <= 62) return ["dirt", {}];
			if (position[1] == 63) return ["grass", {}];
		}
	},
	"sphere": {
		"type": "stepped",
		"run": function (position, data = {}) {
			var mustGenerate = ((position[0] ** 2) + (position[1] ** 2) + (position[2] ** 2)) <= 16 * 16;

			if (mustGenerate) return ["stone", {}];
		}
	},
	"xr-flat": {
		"type": "stepped",
		"run": function (position, data = {}) {
			if (position[1] >= 0 && position[1] <= 1) return ["glitch", {}];
			if (position[1] >= 2 && position[1] <= 4) return ["stone", {}];
			if (position[1] >= 5 && position[1] <= 6) return ["dirt", {}];
			if (position[1] == 7) return ["grass", {}];
		}
	},
	"terrain": {
		"type": "full",
		"run": function (position, data = {}, chunkSize) {
			var chunkWidth = chunkSize[0];
			var chunkHeight = chunkSize[1];
			var chunkDepth = chunkSize[2];

			var blocks = new Array(chunkSize[0] * chunkSize[1] * chunkSize[2]),
				perlin = new ImprovedNoise(),
				averageY = data["averageY"] || 64;

			let quality = 25;

			for (var x = 0; x < chunkWidth; x++) {
				for (var z = 0; z < chunkDepth; z++) {
					var absoluteCoords = [
						(position[0] * chunkWidth) + x,
						(position[2] * chunkDepth) + z
					];

					var maxY = Math.round(
						perlin.noise(
							absoluteCoords[0] / quality,
							averageY / quality,
							absoluteCoords[1] / quality
						) * quality
					) + averageY;

					for (var y = 0; y < chunkHeight; y++) {
						var absoluteY = (position[1] * chunkHeight) + y;

						if (absoluteY >= (position[1] * chunkHeight)
						&& absoluteY <= ((position[1] * chunkHeight) + chunkHeight)) {
							if (absoluteY >= 0 && absoluteY <= 3) {
								blocks[computeBlockIndex(worldConfig.chunkSize, [
									x, y, z
								])] = ["glitch", {}];
							} else if (absoluteY >= 4 && absoluteY <= (maxY - 4)) {
								blocks[computeBlockIndex(worldConfig.chunkSize, [
									x, y, z
								])] = ["stone", {}];
							} else if (absoluteY >= (maxY - 3) && absoluteY <= (maxY - 1)) {
								blocks[computeBlockIndex(worldConfig.chunkSize, [
									x, y, z
								])] = ["dirt", {}];
							} else if (absoluteY == maxY) {
								blocks[computeBlockIndex(worldConfig.chunkSize, [
									x, y, z
								])] = ["grass", {}];
							}
						}
					}
				}
			}

			return blocks;
		}
	}
};

var construct = function (chunk, neighbors, chunkSize) {
	var positions = [];
	var normals = [];
	var uvs = [];
	var indexes = [];
	let texUV = [];

	for (var b = 0; b < chunk.blocks.length; b++) {
		var blockPosition = computeBlockPosition(worldConfig.chunkSize, b);

		if (chunk.blocks[b]) {
			var type = chunk.blocks[b][0];

			var blockTexData = textureData.textures[type];

			for (var n = 0; n < neighborData.length; n++) {
				var neighbor = neighborData[n];

				var adjacentPosition = addPositions(blockPosition, neighbor.position);

				let adjacentInNeighbor = false;
				if (
					adjacentPosition[0] < 0 || adjacentPosition[0] >= worldConfig.chunkSize[0] ||
					adjacentPosition[1] < 0 || adjacentPosition[1] >= worldConfig.chunkSize[1] ||
					adjacentPosition[2] < 0 || adjacentPosition[2] >= worldConfig.chunkSize[2]
				) adjacentInNeighbor = true;

				let hasAdjacent = true;
				if (adjacentInNeighbor) {
					if (!neighbors[n]) hasAdjacent = false;

					var positionInNeighbor = neighborPosition(adjacentPosition, worldConfig.chunkSize);
					var indexInNeighbor = computeBlockIndex(worldConfig.chunkSize, positionInNeighbor);

					if (!neighbors[n] || !neighbors[n].blocks[indexInNeighbor]) hasAdjacent = false;
				} else {
					var blockIndex = computeBlockIndex(worldConfig.chunkSize, adjacentPosition);
					if (!chunk.blocks[blockIndex]) hasAdjacent = false;
				}

				if (!hasAdjacent) {
					var index = positions.length / 3;

					if (blockTexData.sided) {
						let sideMap;

						for (var sm = 0; sm < blockTexData.sideMaps.length; sm++) {
							if (blockTexData.sideMaps[sm].includes(n)) sideMap = sm;
						}

						texUV[0] = blockTexData.positions[sideMap][0] / blockTexData.sizes[sideMap][0];
						texUV[1] = blockTexData.positions[sideMap][1] / blockTexData.sizes[sideMap][1];
					} else {
						texUV[0] = blockTexData.position[0] / blockTexData.size[0];
						texUV[1] = blockTexData.position[1] / blockTexData.size[1];
					}
					var sideTexData = blockTexData[n];

					// texUV[0] = sideTexData.end[0] - sideTexData.start[0];
					// texUV[1] = sideTexData.end[1] - sideTexData.start[1];

					for (var c = 0; c < neighbor.corners.length; c++) {
						var corner = neighbor.corners[c];

						var position = addPositions(blockPosition, corner);

						positions.push(
							position[0],
							position[1],
							position[2]
						);
						normals.push(
							neighbor.position[0],
							neighbor.position[1],
							neighbor.position[2]
						);

						var uv = corner.slice(3, 5);

						uvs.push(
							(texUV[0] + uv[0]) *
							textureData.resolution[0] /
							textureData.size[0],
							1 - (texUV[1] + 1 - uv[1]) *
							textureData.resolution[1] /
							textureData.size[1]
						);
					}

					indexes.push(
						index + 0, index + 1, index + 2,
						index + 2, index + 1, index + 3
					);
				}
			}
		}
	}

	return {positions, normals, uvs, indexes};
};

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 neighborPosition = function (position, chunkSize) {
	var chunkPosition = position.slice(0, 3);

	if (position[0] < 0) chunkPosition[0] = chunkSize[0] + position[0];
	if (position[0] >= chunkSize[0]) chunkPosition[0] = position[0] - chunkSize[0];
	if (position[1] < 0) chunkPosition[1] = chunkSize[1] + position[1];
	if (position[1] >= chunkSize[1]) chunkPosition[1] = position[1] - chunkSize[1];
	if (position[2] < 0) chunkPosition[2] = chunkSize[2] + position[2];
	if (position[2] >= chunkSize[2]) chunkPosition[2] = position[2] - chunkSize[2];

	return chunkPosition;
};

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;
};

// Thanks to https://github.com/pmndrs/cannon-es/issues/21 (I slightly modified it)
var trimeshToPolyhedron = function (bodyOptions, trimesh, upvector) {
	let p1 = new CANNON.Vec3(),
		p2 = new CANNON.Vec3(),
		p3 = new CANNON.Vec3(),
		p4 = new CANNON.Vec3(),
		mp = new CANNON.Vec3(),
		tmp = new CANNON.Vec3(),
		e1 = new CANNON.Vec3(),
		e2 = new CANNON.Vec3();

	const body = new CANNON.Body(bodyOptions);

	for (let i = 0; i < trimesh.indices.length / 3; i++) {
		mp.set(0, 0, 0);
		trimesh.getTriangleVertices(i, p1, p2, p3);
		trimesh.getNormal(i, p4);
		// if (upvector && p4.dot(upvector) < 0) p4.scale(-1, p4);
		p4.normalize();
		mp = mp.vadd(p1).vadd(p2).vadd(p3).scale(1/3);
		const vertices = [
			new CANNON.Vec3().copy(p1),
			new CANNON.Vec3().copy(p2),
			new CANNON.Vec3().copy(p3),
			mp.vadd(p4.scale(-1 / 2))
		];
		const faces = [
			[0, 1, 2],
			[0, 3, 1],
			[1, 3, 2],
			[2, 3, 0]
		];
		const normals = [
			new CANNON.Vec3().copy(p4)
		];
		for (let j = 0; j < 3; j++) {
			vertices[j].vsub(vertices[(j + 1) % 3], e1);
			vertices[(j + 1) % 3].vsub(p4, e2);
			tmp.set(0, 0, 0);
			const points = faces[j + 1];
			for (let p = 0; p < points.length; p++) {
				tmp.vadd(vertices[points[p]], tmp);
			}
			tmp.scale(1 / points.length, tmp);
			const normal = e1.cross(e2);
			normal.normalize();
			normal.scale(-1, normal);
			const angle = normal.dot(tmp);

			if(angle < 0) normal.scale(-1, normal);
            normals.push(normal);
		}
		const polyhedron = new CANNON.ConvexPolyhedron({vertices, faces, normals});
		body.addShape(polyhedron);
	}

	return body;
};

Ammo().then(function () {
	ammoIsReady = true;
}).catch(function (error) {

});
