"use strict";

require("encoding-indexes");
require("encoding");

var auth = require("auth");
var app = require("app");

var gProcessIndex = 0;
var gProcesses = {};
var gSessionIndex = 0;

var gGlobalSettings = {
	index: "/~cory/index",
};

var kGlobalSettingsFile = "data/global/settings.json";

var kPingInterval = 60 * 1000;

function getCookies(headers) {
	var cookies = {};

	if (headers.cookie) {
		var parts = headers.cookie.split(/,|;/);
		for (var i in parts) {
			var equals = parts[i].indexOf("=");
			var name = parts[i].substring(0, equals).trim();
			var value = parts[i].substring(equals + 1).trim();
			cookies[name] = value;
		}
	}

	return cookies;
}

function makeSessionId() {
	return (gSessionIndex++).toString();
}

function printError(out, error) {
	if (error.stackTrace) {
		out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
		out.print(error.stackTrace);
	} else {
		for (var i in error) {
			out.print(i);
		}
		out.print(error.toString());
	}
}

function invoke(handlers, argv) {
	var promises = [];
	if (handlers) {
		for (var i = 0; i < handlers.length; ++i) {
			try {
				promises.push(handlers[i](...argv));
			} catch (error) {
				handlers.splice(i, 1);
				i--;
				promises.push(new Promise(function(resolve, reject) { reject(error); }));
			}
		}
	}
	return Promise.all(promises);
}

function broadcastEvent(eventName, argv) {
	var promises = [];
	for (var i in gProcesses) {
		var process = gProcesses[i];
		if (process.eventHandlers[eventName]) {
			promises.push(invoke(process.eventHandlers[eventName], argv));
		}
	}
	return Promise.all(promises);
}

function broadcast(message) {
	var sender = this;
	var promises = [];
	for (var i in gProcesses) {
		var process = gProcesses[i];
		if (process != sender
			&& process.packageOwner == sender.packageOwner
			&& process.packageName == sender.packageName) {
			var from = getUser(process, sender);
			promises.push(postMessageInternal(from, process, message));
		}
	}
	return Promise.all(promises);
}

function getUser(caller, process) {
	return {
		name: process.userName,
		key: process.key,
		index: process.index,
		packageOwner: process.packageOwner,
		packageName: process.packageName,
		credentials: process.credentials,
		postMessage: postMessageInternal.bind(caller, caller, process),
	};
}

function getUsers(packageOwner, packageName) {
	var result = [];
	for (var key in gProcesses) {
		var process = gProcesses[key];
		if ((!packageOwner || process.packageOwner == packageOwner)
			&& (!packageName || process.packageName == packageName)) {
			result.push(getUser(this, process));
		}
	}
	return result;
}

function postMessageInternal(from, to, message) {
	if (to.eventHandlers['message']) {
		return invoke(to.eventHandlers['message'], [getUser(from, from), message]);
	}
}

async function getSessionProcessBlob(blobId, session, options) {
	var actualOptions = {timeout: kPingInterval};
	if (options) {
		for (var i in options) {
			actualOptions[i] = options[i];
		}
	}
	return getProcessBlob(blobId, 'session_' + session, actualOptions);
}

async function readFileUtf8(fileName) {
	let data = await File.readFile(fileName);
	data = utf8Decode(data);
	return data;
}

let gManifestCache = {};

async function getProcessBlob(blobId, key, options) {
	var process = gProcesses[key];
	if (!process
		&& !(options && "create" in options && !options.create)) {
		try {
			print("Creating task for " + blobId + " " + key);
			process = {};
			process.key = key;
			process.index = gProcessIndex++;
			process.userName = 'user' + process.index;
			process.credentials = options.credentials || {};
			process.task = new Task();
			process.eventHandlers = {};
			process.app = new App();
			process.lastActive = Date.now();
			process.lastPing = null;
			process.timeout = options.timeout;
			var resolveReady;
			var rejectReady;
			process.ready = new Promise(function(resolve, reject) {
				resolveReady = resolve;
				rejectReady = reject;
			});
			gProcesses[key] = process;
			process.task.onExit = function(exitCode, terminationSignal) {
				broadcastEvent('onSessionEnd', [getUser(process, process)]);
				process.task = null;
				delete gProcesses[key];
			};
			var imports = {
				'core': {
					'broadcast': broadcast.bind(process),
					'getUsers': getUsers.bind(process),
					'register': function(eventName, handler) {
						if (!process.eventHandlers[eventName]) {
							process.eventHandlers[eventName] = [];
						}
						process.eventHandlers[eventName].push(handler);
					},
					'unregister': function(eventHandle, handler) {
						if (process.eventHandlers[eventName]) {
							let index = process.eventHandlers[eventName].indexOf(handler);
							if (index != -1) {
								process.eventHandlers[eventName].splice(index, 1);
							}
							if (process.eventHandlers[eventName].length == 0) {
								delete process.eventHandlers[eventName];
							}
						}
					},
					'getUser': getUser.bind(null, process, process),
					'user': getUser(process, process),
					'apps': function() {
						var apps = process.credentials &&
							process.credentials.session &&
							process.credentials.session.name ?
								new Database(process.credentials.session.name).getLike('path:%') :
								{};
						return Object.fromEntries(Object.keys(apps).map(key => [key.substring(5), apps[key]]));
					},
				}
			};
			if (options.api) {
				imports.app = {};
				for (let i in options.api) {
					let api = options.api[i];
					imports.app[api[0]] = process.app.makeFunction(api);
				}
			}
			process.task.onError = function(error) {
				try {
					process.app.send({action: 'error', error: error});
				} catch(e) {
					print(e);
				}
			};
			imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
			if (process.credentials &&
				process.credentials.session &&
				process.credentials.session.name) {
				imports.database = function(key) {
					var db = new Database(process.credentials.session.name + ':' + key);
					return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)]));
				};
			}
			process.task.setImports(imports);
			process.task.activate();
			let source = await getBlobOrContent(blobId);
			var appSource = utf8Decode(source);
			try {
				var app = JSON.parse(appSource);
				if (app.type == "tildefriends-app") {
					var id = app.files["app.js"];
					var blob = await getBlobOrContent(id);
					appSource = utf8Decode(blob);
					await Promise.all(Object.keys(app.files).map(async function(f) {
						await process.task.loadFile([f, await getBlobOrContent(app.files[f])]);
					}));
				}
			} catch (e) {
				printError({print: print}, e);
			}
			broadcastEvent('onSessionBegin', [getUser(process, process)]);
			resolveReady(process);
			if (process.app) {
				process.app.send({action: "ready"});
			}
			await process.task.execute({name: blobId, source: appSource});
		} catch (error) {
			printError({print: print}, error);
			rejectReady();
		}
	}
	return process;
}

function makeDirectoryForFile(fileName) {
	var parts = fileName.split("/");
	var path = "";
	for (var i = 0; i < parts.length - 1; i++) {
		path += parts[i];
		File.makeDirectory(path);
		path += "/";
	}
}

function getGlobalSettings() {
	return gGlobalSettings;
}

function setGlobalSettings(settings) {
	makeDirectoryForFile(kGlobalSettingsFile);
	gGlobalSettings = settings;
	return File.writeFile(kGlobalSettingsFile, JSON.stringify(settings)).catch(function(error) {
		throw new Error("Unable to save settings: " + error);
	});
}

var kStaticFiles = [
	{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
	{uri: '/style.css', type: 'text/css; charset=UTF-8'},
	{uri: '/favicon.png', type: 'image/png'},
	{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
	{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},

	{uri: '/split.min.js'},
	{uri: '/split.min.js.map'},
	{uri: '/codemirror/codemirror.min.js'},
	{uri: '/codemirror/base16-dark.min.css'},
	{uri: '/codemirror/matchesonscrollbar.min.css'},
	{uri: '/codemirror/dialog.min.css'},
	{uri: '/codemirror/codemirror.min.css'},
	{uri: '/codemirror/trailingspace.min.js'},
	{uri: '/codemirror/dialog.min.js'},
	{uri: '/codemirror/search.min.js'},
	{uri: '/codemirror/searchcursor.min.js'},
	{uri: '/codemirror/jump-to-line.min.js'},
	{uri: '/codemirror/matchesonscrollbar.min.js'},
	{uri: '/codemirror/annotatescrollbar.min.js'},
	{uri: '/codemirror/javascript.min.js'},
	{uri: '/codemirror/css.min.js'},
	{uri: '/codemirror/xml.min.js'},
	{uri: '/codemirror/htmlmixed.min.js'},
];

function startsWithBytes(data, bytes) {
	if (data.byteLength >= bytes.length) {
		var dataBytes = new Uint8Array(data.slice(0, bytes.length));
		for (var i = 0; i < bytes.length; i++) {
			if (dataBytes[i] != bytes[i] || bytes[i] === null) {
				return;
			}
		}
		return true;
	}
}

async function staticFileHandler(request, response, blobId, uri) {
	for (var i in kStaticFiles) {
		if (uri === kStaticFiles[i].uri) {
			var path = kStaticFiles[i].path || uri.substring(1);
			var type = kStaticFiles[i].type || guessType(path);
			var data = await File.readFile("core/" + path);
			response.writeHead(200, {"Content-Type": type, "Content-Length": data.byteLength});
			response.end(data);
			return;
		}
	}

	response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
	response.end("File not found");
}

async function speedScopeHandler(request, response, uri) {
	var filename = uri || 'index.html';
	if (filename.indexOf('..') != -1) {
		response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
		response.end("File not found");
		return;
	}

	const types = {
		'json': 'text/json',
		'js': 'text/javascript',
		'html': 'text/html',
		'css': 'text/css',
		'map': 'application/json',
	};
	try {
		var data = await File.readFile("deps/speedscope/" + filename);
		response.writeHead(200, {"Content-Type": types[filename.split('.').pop()] || 'text/plain', "Content-Length": data.byteLength});
		response.end(data);
	} catch {
		response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
		response.end("File not found");
	}
}

function sendData(response, data, type) {
	if (data) {
		if (startsWithBytes(data, [0xff, 0xd8, 0xff, 0xdb]) ||
			startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
			startsWithBytes(data, [0xff, 0xd8, 0xff, 0xee]) ||
			startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
			response.writeHead(200, {"Content-Type": "image/jpeg", "Content-Length": data.byteLength});
			response.end(data);
		} else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
			response.writeHead(200, {"Content-Type": "image/png", "Content-Length": data.byteLength});
			response.end(data);
		} else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
			startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
			response.writeHead(200, {"Content-Type": "image/gif", "Content-Length": data.byteLength});
			response.end(data);
		} else {
			response.writeHead(200, {"Content-Type": type || "text/javascript; charset=utf-8", "Content-Length": data.byteLength});
			response.end(data);
		}
	} else {
		response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
		response.end("File not found");
	}
}

async function getBlobOrContent(id) {
	if (!id) {
		return;
	} else if (id.startsWith('&')) {
		return ssb.blobGet(id);
	} else if (id.startsWith('%')) {
		return ssb.messageContentGet(id);
	}
}

function guessType(path) {
	const k_extension_to_type = {
		'css': 'text/css',
		'html': 'text/html',
		'js': 'text/javascript',
	};
	var extension = path.split('.').pop();
	return k_extension_to_type[extension];
}

async function blobHandler(request, response, blobId, uri) {
	var found = false;
	if (!found) {
		for (var i in kStaticFiles) {
			if (uri === kStaticFiles[i].uri) {
				found = true;
				var data = await File.readFile("core/" + kStaticFiles[i].path);
				response.writeHead(200, {"Content-Type": kStaticFiles[i].type, "Content-Length": data.byteLength});
				response.end(data);
				break;
			}
		}
	}

	if (!uri) {
		response.writeHead(303, {"Location": 'http://' + request.headers.host + blobId + '/', "Content-Length": "0"});
		response.end(data);
		return;
	}

	if (!found) {
		var process;
		if (uri == "/view") {
			var data;
			if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
				var id = await new Database(match[1]).get('path:' + match[2]);
				if (id) {
					data = await getBlobOrContent(id);
					if (match[3]) {
						var app = JSON.parse(data);
						data = app.files[match[3]];
					}
				}
			} else {
				data = await getBlobOrContent(blobId);
			}
			sendData(response, data);
		} else if (uri == "/save") {
			let newBlobId = await ssb.blobStore(request.body);

			var match;
			if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
				var user = match[1];
				var app = match[2];
				var credentials = auth.query(request.headers);
				if (!credentials || !credentials.session || credentials.session.name != user) {
					response.writeHead(401, {"Content-Type": "text/plain; charset=utf-8"});
					response.end("401 Unauthorized");
					return;
				}
				var database = new Database(user);
				await database.set('path:' + app, newBlobId);
			}

			response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
			response.end("/" + newBlobId);
		} else {
			var data;
			var type;
			if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
				var db = new Database(match[1]);
				var id = await db.get('path:' + match[2]);
				if (id) {
					data = utf8Decode(await getBlobOrContent(id));
					var app = JSON.parse(data);
					data = app.files[uri.substring(1)];
					data = await getBlobOrContent(data);
					type = guessType(uri);
				}
			} else {
				data = utf8Decode(await getBlobOrContent(blobId));
				var app = JSON.parse(data);
				data = app.files[uri.substring(1)];
				data = await getBlobOrContent(data);
			}
			sendData(response, data, type);
		}
	}
}

ssb.addEventListener('broadcasts', function() {
	broadcastEvent('onBroadcastsChanged', []);
});

ssb.addEventListener('connections', function() {
	broadcastEvent('onConnectionsChanged', []);
});

async function loadSettings() {
	try {
		var data = await readFileUtf8(kGlobalSettingsFile);
		if (data) {
			gGlobalSettings = JSON.parse(data);
		}
	} catch (error) {
		print("Error loading settings from " + kGlobalSettingsFile + ": " + error);
	}
}

loadSettings().then(function() {
	var auth = require("auth");
	var httpd = require("httpd");
	httpd.all("/login", auth.handler);
	httpd.all("", function(request, response) {
		var match;
		if (request.uri === "/" || request.uri === "") {
			response.writeHead(303, {"Location": 'http://' + request.headers.host + gGlobalSettings.index, "Content-Length": "0"});
			return response.end();
		} else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) {
			return blobHandler(request, response, match[1], match[2]);
		} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
			return blobHandler(request, response, match[1], match[2]);
		} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
			return staticFileHandler(request, response, null, match[1]);
		} else if (match = /^\/scope$/.exec(request.uri)) {
			response.writeHead(303, {"Location": 'http://' + request.headers.host + '/speedscope/#profileURL=http://' + request.headers.host + '/trace', "Content-Length": "0"});
			return response.end(data);
		} else if (match = /^\/speedscope\/([\.\w-]*)$/.exec(request.uri)) {
			return speedScopeHandler(request, response, match[1]);
		} else if (match = /^(.*)(\/save)$/.exec(request.uri)) {
			return blobHandler(request, response, match[1], match[2]);
		} else if (match = /^\/trace$/.exec(request.uri)) {
			var data = trace();
			response.writeHead(404, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
			return response.end(data);
		} else if (request.uri == "/robots.txt") {
			return blobHandler(request, response, null, request.uri);
		} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
			var data = File.readFile("data/global/.well-known/" + match[1]);
			if (data) {
				response.writeHead(200, {"Content-Type": "text/plain", "Content-Length": data.length});
				response.end(data);
			} else {
				response.writeHead(404, {"Content-Type": "text/plain", "Content-Length": "File not found".length});
				response.end("File not found");
			}
		} else {
			var data = "File not found.";
			response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length.toString()});
			return response.end(data);
		}
	});
	httpd.registerSocketHandler("/app/socket", app.socket);
}).catch(function(error) {
	print('Failed to load settings.');
});