const loaderData = {
	"utils": {
		"type": "global",
		"source": "./lib/utils.js",
		"loadAs": "utils",
		"dependsOn": []
	},
	"three": {
		"type": "global",
		"source": "./lib/three.js",
		"loadAs": "three",
		"dependsOn": []
	},
	/*"cannon": {
		"type": "global",
		"source": "./lib/cannon-es.js",
		"loadAs": "cannon",
		"dependsOn": []
	},*/
	"fflate": {
		"type": "global",
		"source": "./lib/fflate.js",
		"loadAs": "fflate",
		"dependsOn": []
	},
	"olm": {
		"type": "global",
		"source": "./lib/olm.js",
		"loadAs": "olm",
		"dependsOn": []
	},
	"matrix": {
		"type": "global",
		"source": "./lib/browser-matrix.min.js",
		"loadAs": "matrix",
		"dependsOn": ["olm"]
	},
	"peer": {
		"type": "global",
		"source": "./lib/peer.js",
		"loadAs": "peer",
		"dependsOn": []
	},
	"gamecore": {
		"type": "global",
		"source": "./core.js",
		"loadAs": "gamecore",
		"dependsOn": ["utils"]
	},
	"locales": {
		"type": "global",
		"source": "./lib/locales.js",
		"loadAs": "locales",
		"dependsOn": []
	},
	"menu": {
		"type": "global",
		"source": "./menu.js",
		"loadAs": "menu",
		"dependsOn": ["locales"]
	}
};

const throwError = window.throwError = function (err) {
	alert(err + "\n" + err.stack);
	throw err;
};

var onLoadingFinished = function (modules) {
	var initPromises = {};
	var promiseReturners = {};

	initPromises["render"] = new Promise(function (resolve, reject) {
		promiseReturners["render"] = {
			resolve,
			reject
		};
	});

	initPromises["input"] = new Promise(function (resolve, reject) {
		promiseReturners["input"] = {
			resolve,
			reject
		};
	});

	core.on("render-init", function () {
		promiseReturners["render"].resolve();

		fetch("./data/resources/XVoxel.zip").then(function (response) {
    		if (response.status != 200) return throwError(new Error("ENOENT"));

    		response.arrayBuffer().then(function (arrayBuffer) {
        		core.modules["resources"].ResourcePack.loadFromZIP(arrayBuffer).then(function (pack) {
            		window.resourcePacks.push(pack);


        		}).catch(throwError);
    		}).catch(throwError);
		}).catch(throwError);
	});

	core.on("input-init", function () {
		promiseReturners["input"].resolve();
	});

	Promise.all(Object.values(initPromises)).then(function () {
		addPlayer(0);
	});
};

window.onload = function (event) {
	renderersContainer = document.querySelector("#renderers");
	var librariesToLoad = Object.keys(loaderData);

	var libraryLoaders = [];

	for (var l = 0; l < librariesToLoad.length; l++) {
		libraryLoaders.push(
			loadFromMeta(librariesToLoad[l], loaderData[librariesToLoad[l]])
		);
	}

	Promise.all(libraryLoaders).then(function (libraries) {
		core = window.core = new window.gamecore.GameCore();

		fetch("./data/strings/" + window.currentLanguage + ".json").then(function (response) {
    		if (response.status != 200) return throwError(new Error("ENOENT"));

    		response.text().then(function (text) {
        		window.locales.loadFromJSON(window.currentLanguage, JSON.parse(text));

				menu.makeMenu();
    		}).catch(throwError);
		}).catch(throwError);

		onLoadingFinished(core.modules);

		core.init(gamecore.loadData.modules).then(function (gameCore) {
			for (var moduleName in core.modules) {
				if (core.modules.hasOwnProperty(moduleName)) {
					var thisModule = core.modules[moduleName];

					if ((typeof thisModule) != "object") continue;

					if ("menus" in thisModule) {
						for (var menuName in thisModule.menus) {
							if (thisModule.menus.hasOwnProperty(menuName)) {
								if (!(menuName in menu.menuData)) {
									menu.menuData[menuName] = thisModule.menus[menuName];
								} else {
									var thisMenu = thisModule.menus[menuName];

									if (!("elements" in thisMenu)) continue;

									for (var e = 0; e < thisMenu.elements.length; e++) {
										menu.menuData[menuName].elements.push(thisMenu.elements[e]);
									}
								}

								menu.menus[menuName] = menu.constructMenu(menuName, menu.menuData[menuName]);
							}
						}
					}
				}
			}

			window.menu.showMenu("main", 0);
		}).catch(throwError);
	}).catch(throwError);
};

var loadFromMeta = function (name, metadata) {
	return new Promise((function (resolve, reject) {
		var dependencies = [];

		if (metadata.dependsOn) {
			for (var d = 0; d < metadata.dependsOn.length; d++) {
				var dependencyName = metadata.dependsOn[d];
				var dependency = loaderData[dependencyName];

				if (!loadings[dependencyName]) {
					loadings[dependencyName] = loadFromMeta(dependencyName, dependency);
				}
				dependencies.push(loadings[dependencyName]);
			}
		}

		Promise.all(dependencies).then((function (deps) {
			loadings[name] = load(metadata.source).then(function (mod) {
				window[metadata.loadAs || name] = mod;

				resolve(mod);
			}).catch(throwError);
		}).bind(name)).catch(throwError);
	}).bind(name));
};

var render = window.render = function (instanceIndex) {
	if (instanceWorlds[instanceIndex]) {
		renderers[instanceIndex].render(worlds[instanceWorlds[instanceIndex]], cameras[instanceIndex]);
	}
};

var createWorld = window.createWorld = function (name, generator = "flat", options, generateInitialChunks = true) {
	return new Promise(function (resolve, reject) {
		window.worlds[name] = new core.modules["world"].World({
			"generator": generator
		});

		if (!generateInitialChunks) return resolve(window.worlds[name]);

		var min = [-4, 0, -4];
		var max = [4, 8, 4];

		var positions = [];

		for (var x = min[0]; x < max[0]; x++) {
			for (var z = min[2]; z < max[2]; z++) {
				for (var y = min[1]; y < max[1]; y++) {
					positions.push([x, y, z]);
				}
			}
		}

		var chunkGenerators = [];

		genNext = function (pos, p) {
			chunkGenerators.push(
				worlds[name].generateChunk(pos[p])
					.then(function (chunk) {
						if (pos[p + 1]) return genNext(pos, p + 1);

						Promise.all(chunkGenerators).then(function (chunks) {
							resolve(worlds[name]);
						}).catch(throwError);
					})
					.catch(throwError)
			);
		};

		genNext(positions, 0);
	});
};

var startGame = window.startGame = function (instance, worldName) {
	instanceWorlds[instance] = worldName;
	renderers[instance].setAnimationLoop(render.bind(window, instance));

	inputs[instance].grab();

	var chunkSize = window.worlds[worldName].chunkSize;

	var positions = [];

	worlds[worldName].chunks.forEach(function (chunk, c) {
		positions.push(
			c.split(":")
				.map(coord => parseInt(coord))
		);
	});

	if (positions.length > 0) {
		constructNext = function (pos, p) {
			worlds[worldName].constructChunk(pos[p]).then(function (chunk) {
				worlds[worldName].addChunk(chunk);

				if (pos[p + 1]) return constructNext(pos, p + 1);
			}).catch(throwError);
		};

		constructNext(positions, 0);
	}
};

window.addEventListener("resize", function (event) {
	reorganizeRenderers(false);

	for (var r = 0; r < renderers.length; r++) {
		var rdr = renderers[r];
		var rdrDOM = rdr.domElement;

		var cam = cameras[r];

		rdr.setSize(rdrDOM.clientWidth, rdrDOM.clientHeight);

		cam.aspect = rdrDOM.clientWidth / rdrDOM.clientHeight;
		cam.updateProjectionMatrix();
	}
});

var reorganizeRenderers = function (remakeLayout = true) {
	var rows = [];
	var rowAmount = Math.round(Math.sqrt(renderers.length));

	if (remakeLayout) {
		for (var r = renderersContainer.childElementCount - 1; r >= 0; r--) {
			renderersContainer.children.item(r).remove();
		}

		for (var r = 0; r < rowAmount; r++) {
			rows[r] = document.createElement("div");

			rows[r].style.width = "100%";
			rows[r].style.height = "calc(100% / " + rowAmount + ")";

			renderersContainer.append(rows[r]);
		}

		for (var r = 0; r < renderers.length; r++) {
			rows[Math.floor((r / renderers.length) * rowAmount)].append(renderers[r].domElement);
		}
	} else {
		for (var r = renderersContainer.childElementCount - 1; r >= 0; r--) {
			rows[r] = renderersContainer.children.item(r);
		}
	}

	for (var r = 0; r < rowAmount; r++) {
		var row = rows[r];

		for (var rd = 0; rd < row.childElementCount; rd++) {
			row.children[rd].style.width = "calc(100% / " + row.childElementCount + ")";
			row.children[rd].style.height = "100%";
		}
	}
};

var addPlayer = function (instance, controller) {
	renderers[instance] = new core.modules["render"].Renderer({
		"preserveDrawingBuffer": true,
		"autoResize": true
	});

	var instanceDOMElement = renderers[instance].domElement;

	reorganizeRenderers();

	for (var r = 0; r < renderers.length; r++) {
		var rdr = renderers[r];
		var rdrDOM = rdr.domElement;

		rdr.setSize(rdrDOM.clientWidth, rdrDOM.clientHeight, false);
	}

	cameras[instance] = new core.modules["render"].PerspectiveCamera(130, instanceDOMElement.width / instanceDOMElement.height, 0.0005, 8192);

	if (controller instanceof core.modules["input"].Controller) {
		inputs[instance] = controller;
		inputs[instance].domElement = instanceDOMElement;
	} else {
		inputs[instance] = new core.modules["input"].Controller("desktop", {
			"domElement": instanceDOMElement
		});
	}

	renderers[instance].setAnimationLoop(render.bind(window, instance));
};

var quitGame = window.quitGame = function (instance) {

};

var closeGame = window.closeGame = function () {
	window.close();
};

var openSave = window.openSave = function (archive) {
	return new Promise(function (resolve, reject) {
		window.fflate.unzip(new Uint8Array(archive), {}, function (error, data) {
			if (error) return reject(error);

			var loadedWorlds = {};

			for (var filePath in data) {
				if (data.hasOwnProperty(filePath)) {
					var matches = filePath.match(/([^\/]+)\/meta\.json/);

					if (!matches) continue;
					if (matches[0] !== filePath) continue;
					if (!matches[1]) continue;

					var worldFolderName = matches[1];

					let worldMeta;
					try {
						worldMeta = JSON.parse(window.fflate.strFromU8(data["" + worldFolderName + "/meta.json"]));
					} catch (err) {
						console.error(err);
						continue;
					}

					var worldName = worldMeta.name;

					loadedWorlds[worldName] = new core.modules["world"].World({
						"generator": worldMeta.generator
					});

					var chunkFileNameMatcher = new RegExp(worldFolderName + "\/chunks\/(-?[0-9]+:-?[0-9]+:-?[0-9]+)\.json");

					for (var fileName in data) {
						if (data.hasOwnProperty(fileName)) {
							var nameMatches = fileName.match(chunkFileNameMatcher);

							if (!nameMatches) continue;
							if (nameMatches[0] !== fileName) continue;
							if (!nameMatches[1]) continue;

							var chunkFileName = nameMatches[1];

							let chunkData;
							try {
								chunkData = JSON.parse(window.fflate.strFromU8(data[worldFolderName + "/chunks/" + chunkFileName + ".json"]));
							} catch (err) {
								console.error(err);
								continue;
							}

							var thisChunk = new core.modules["world"].Chunk(chunkData.size, loadedWorlds[worldName]);

							for (var b = 0; b < chunkData.blocks.length; b++) {
								thisChunk.blocks[b] = chunkData.blocks[b];
							}

							var chunkPosition = chunkFileName.split(":").map(function (c) {
								return parseInt(c);
							});

							loadedWorlds[worldName].setChunk(thisChunk, chunkPosition);
							loadedWorlds[worldName].add(thisChunk);
						}
					}
				}
			}

			resolve(loadedWorlds);
		});
	});
};

var makeSave = window.makeSave = function () {
	return new Promise(function (resolve, reject) {
		var files = {};

		for (var world in worlds) {
			if (worlds.hasOwnProperty(world)) {
				var thisWorld = worlds[world].toJSON();

				var worldMeta = {
					"name": world,
					"generator": thisWorld.generator
				};

				files[world + "/meta.json"] = fflate.strToU8(JSON.stringify(worldMeta, null, "\t"));

				for (var chunk in thisWorld.chunks) {
					if (thisWorld.chunks.hasOwnProperty(chunk)) {
						var thisChunk = thisWorld.chunks[chunk];

						files[world + "/chunks/" + chunk + ".json"] = fflate.strToU8(JSON.stringify(thisChunk, null, "\t"));
					}
				}
			}
		}

		window.fflate.zip(files, function (error, archive) {
			if (error) return reject(error);
			resolve(archive);
		});
	});
};

let core;

var currentLanguage = window.currentLanguage = "en-US";

var instanceWorlds = window.instanceWorlds = [];

var renderers = window.renderers = [];
var cameras = window.cameras = [];
var inputs = window.inputs = [];

var loadings = {};

let resourcePacks = window.resourcePacks = [];
let worlds = window.worlds = {};

let renderersContainer;
