"use strict";

var gSocket;
var gCredentials;

var gCurrentFile;
var gFiles = {};
var gApp = {files: {}};
var gEditor;
var gSplit;
var gGraphs = {};
var gParentApp;

var kErrorColor = "#dc322f";
var kStatusColor = "#fff";

window.addEventListener("keydown", function(event) {
	if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
		if (editing()) {
			save();
			event.preventDefault();
		}
	} else if (event.keyCode == 66 && event.altKey) {
		if (editing()) {
			closeEditor();
			event.preventDefault();
		}
	}
});

function ensureLoaded(nodes, callback) {
	if (!nodes.length) {
		callback();
		return;
	}

	var search = nodes.shift();
	var head = document.head;
	var found = false;
	for (var i = 0; i < head.childNodes.length; i++) {
		if (head.childNodes[i].tagName == search.tagName) {
			var match = true;
			for (var attribute in search.attributes) {
				if (head.childNodes[i].attributes[attribute].value != search.attributes[attribute]) {
					match = false;
				}
			}
			if (match) {
				found = true;
				break;
			}
		}
	}
	if (found) {
		ensureLoaded(nodes, callback);
	} else {
		var node = document.createElement(search.tagName);
		node.onreadystatechange = node.onload = function() {
			ensureLoaded(nodes, callback);
		};
		for (var attribute in search.attributes) {
			node.setAttribute(attribute, search.attributes[attribute]);
		}
		head.insertBefore(node, head.firstChild);
	}
}

function editing() {
	return document.getElementById("editPane").style.display != 'none';
}

function toggleEdit() {
	if (editing()) {
		closeEditor();
	} else {
		edit();
	}
}

function edit() {
	if (editing()) {
		return;
	}

	window.localStorage.setItem('editing', '1');
	if (gSplit) {
		gSplit.destroy();
		gSplit = undefined;
	}
	gSplit = Split(['#editPane', '#viewPane'], {minSize: 0});

	ensureLoaded([
		{tagName: "script", attributes: {src: "/static/codemirror/codemirror.min.js"}},
		{tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/base16-dark.min.css"}},
		{tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/matchesonscrollbar.min.css"}},
		{tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/dialog.min.css"}},
		{tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/codemirror.min.css"}},
		{tagName: "script", attributes: {src: "/static/codemirror/trailingspace.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/dialog.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/search.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/searchcursor.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/jump-to-line.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/matchesonscrollbar.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/annotatescrollbar.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/javascript.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/css.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/xml.min.js"}},
		{tagName: "script", attributes: {src: "/static/codemirror/htmlmixed.min.js"}},
	], function() {
		load().catch(function(error) {
			alert(error);
			closeEditor();
		});
	});
}

function hideFiles() {
	window.localStorage.setItem('files', '0');
	document.getElementById('filesPane').classList.add('collapsed');
}

function showFiles() {
	window.localStorage.setItem('files', '1');
	document.getElementById('filesPane').classList.remove('collapsed');
}

function trace() {
	fetch('/trace')
		.then(function(response) {
			if (!response.ok) {
				throw new Error('Request failed: ' + response.status + ' ' + response.statusText);
			}
			return response.arrayBuffer();
		}).then(function(data) {
			var perfetto = window.open('/perfetto/');
			var done = false;
			if (perfetto) {
				function message_handler(message) {
					if (message.data == 'PONG') {
						perfetto.postMessage({
							perfetto: {
								buffer: data,
								title: 'Tilde Friends Trace',
								url: window.location.href,
							}
						}, '*');
						done = true;
					}
				}
				window.addEventListener('message', message_handler);
				function ping_perfetto() {
					perfetto.postMessage('PING', window.location.origin);
					if (!done && !perfetto.closed) {
						setTimeout(ping_perfetto, 50);
					} else {
						window.removeEventListener('message', message_handler);
					}
				}
				setTimeout(ping_perfetto, 50);
			} else {
				alert("Unable to open perfetto.");
			}
		}).catch(function(error) {
			alert('Failed to load trace: ' + error);
		});
}

function stats() {
	window.localStorage.setItem('stats', '1');
	document.getElementById("statsPane").style.display = 'flex';
	send({action: 'enableStats', enabled: true});
}

function closeStats() {
	window.localStorage.setItem('stats', '0');
	document.getElementById("statsPane").style.display = 'none';
	send({action: 'enableStats', enabled: false});
}

function toggleStats() {
	if (document.getElementById("statsPane").style.display == 'none') {
		stats();
	} else {
		closeStats();
	}
}

function guessMode(name) {
	return name.endsWith(".js") ? "javascript" :
		name.endsWith(".html") ? "htmlmixed" :
		null;
}

function loadFile(name, id) {
	return fetch('/' + id + '/view').then(function(response) {
		if (!response.ok) {
			throw new Error('Request failed: ' + response.status + ' ' + response.statusText);
		}
		return response.text();
	}).then(function(text) {
		gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
		if (!Object.values(gFiles).some(x => !x.doc)) {
			document.getElementById("editPane").style.display = 'flex';
			openFile(Object.keys(gFiles).sort()[0]);
		}
	});
}

function load(path) {
	return fetch((path || url()) + 'view').then(function(response) {
		if (!response.ok) {
			if (response.status == 404) {
				return null;
			} else {
				throw new Error(response.status + ' ' + response.statusText);
			}
		}
		return response.json();
	}).then(function(json) {
		if (!gEditor) {
			gEditor = CodeMirror.fromTextArea(document.getElementById("editor"), {
				'theme': 'base16-dark',
				'lineNumbers': true,
				'tabSize': 4,
				'indentUnit': 4,
				'indentWithTabs': true,
				'showTrailingSpace': true,
			});
			gEditor.on('changes', function() {
				updateFiles();
			});
		}
		gFiles = {};
		var isApp = false;
		var promises = [];

		if (json && json['type'] == 'tildefriends-app') {
			isApp = true;
			Object.keys(json['files']).forEach(function(name) {
				gFiles[name] = {};
				promises.push(loadFile(name, json['files'][name]));
			});
			if (Object.keys(json['files']).length == 0) {
				document.getElementById("editPane").style.display = 'flex';
			}
			gApp = json;
		}
		if (!isApp) {
			document.getElementById("editPane").style.display = 'flex';
			var text = '// New script.\n';
			gCurrentFile = 'app.js';
			gFiles[gCurrentFile] = {
				doc: new CodeMirror.Doc(text, guessMode(gCurrentFile)),
			};
			openFile(gCurrentFile);
		}
		return Promise.all(promises);
	});
}

function closeEditor() {
	window.localStorage.setItem('editing', '0');
	document.getElementById("editPane").style.display = 'none';
	if (gSplit) {
		gSplit.destroy();
		gSplit = undefined;
	}
}

function explodePath() {
	return /^\/~([^\/]+)\/([^\/]+)(.*)/.exec(window.location.pathname);
}

function save(save_to) {
	document.getElementById("save").disabled = true;
	document.getElementById("push_to_parent").disabled = true;
	document.getElementById("pull_from_parent").disabled = true;
	if (gCurrentFile) {
		gFiles[gCurrentFile].doc = gEditor.getDoc();
	}

	var save_path = save_to;
	if (!save_path) {
		var name = document.getElementById("name");
		if (name && name.value) {
			save_path = name.value;
		} else {
			save_path = url();
		}
	}

	var promises = [];
	for (let name of Object.keys(gFiles)) {
		let file = gFiles[name];
		if (file.doc.isClean(file.generation)) {
			continue;
		}

		delete file.id;
		promises.push(fetch('/save', {
			method: 'POST',
			headers: {
				'Content-Type': 'text/plain',
			},
			body: file.doc.getValue(),
		}).then(function(response) {
			if (!response.ok) {
				throw new Error('Saving "' + name + '": ' + response.status + ' ' + response.statusText);
			}
			return response.text();
		}).then(function(text) {
			file.id = text;
			if (file.id.charAt(0) == '/') {
				file.id = file.id.substr(1);
			}
		}));
	}

	return Promise.all(promises).then(function() {
		var app = {
			type: "tildefriends-app",
			files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])),
		};
		Object.values(gFiles).forEach(function(file) { delete file.id; });
		gApp = JSON.parse(JSON.stringify(app));

		return fetch(save_path + 'save', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(app),
		}).then(function(response) {
			if (!response.ok) {
				throw new Error(response.status + ' ' + response.statusText);
			}

			if (save_path != window.location.pathname) {
				alert('Saved to ' + save_path + '.');
			} else {
				reconnect(save_path);
			}
		});
	}).catch(function(error) {
		alert(error);
	}).finally(function() {
		document.getElementById("save").disabled = false;
		document.getElementById("push_to_parent").disabled = false;
		document.getElementById("pull_from_parent").disabled = false;
		Object.values(gFiles).forEach(function(file) {
			file.generation = file.doc.changeGeneration();
		});
		updateFiles();
	});
}

function pullFromParent() {
	load(gParentApp ? gParentApp.path : null).then(x => save()).catch(function(error) {
		alert(error)
	});
}

function pushToParent() {
	save(gParentApp ? gParentApp.path : null);
}

function url() {
	var hash = window.location.href.indexOf('#');
	var question = window.location.href.indexOf('?');
	var end = -1;
	if (hash != -1 && (hash < end || end == -1))
	{
		end = hash;
	}
	if (question != -1 && (question < end || end == -1))
	{
		end = question;
	}
	return end != -1 ? window.location.href.substring(0, end) : window.location.href;
}

function hash() {
	return window.location.hash != "#" ? window.location.hash : "";
}

function receive(message) {
	if (message && message.action == "session") {
		setStatusMessage("...Executing...", kStatusColor, true);
		gCredentials = message.credentials;
		gParentApp = message.parentApp;
		updateLogin();
		var parent_enabled = message.parentApp;
		document.getElementById('push_to_parent').style.display = parent_enabled ? 'inline-block' : 'none';
		document.getElementById('pull_from_parent').style.display = parent_enabled ? 'inline-block' : 'none';
	} else if (message && message.action == "ready") {
		setStatusMessage(null);
		if (window.location.hash) {
			send({event: "hashChange", hash: window.location.hash});
		}
		if (window.localStorage.getItem('stats') == '1') {
			/* Stats were opened before we connected. */
			send({action: 'enableStats', enabled: true});
		}
	} else if (message && message.action == "setDocument") {
		var iframe = document.getElementById("document");
		iframe.srcdoc = message.content;
	} else if (message && message.action == "postMessage") {
		var iframe = document.getElementById("document");
		iframe.contentWindow.postMessage(message.message, "*");
	} else if (message && message.action == "ping") {
		send({action: "pong"});
	} else if (message && message.action == "error") {
		if (message.error) {
			if (typeof(message.error) == 'string') {
				setStatusMessage(message.error, '#f00', false);
			} else {
				setStatusMessage(message.error.message + '\n' + message.error.stack, '#f00', false);
			}
		}
		console.log('error', message);
	} else if (message && message.action == "localStorageSet") {
		window.localStorage.setItem('app:' + message.key, message.value);
	} else if (message && message.action == "localStorageGet") {
		send({message: 'localStorage', key: message.key, value: window.localStorage.getItem('app:' + message.key)});
	} else if (message && message.action == "print") {
		console.log('app>', ...message.args);
	} else if (message && message.action == "stats") {
		var now = new Date().getTime();
		for (var key of Object.keys(message.stats)) {
			if (!gGraphs[key]) {
				var graph = {
					chart: new SmoothieChart({
							millisPerPixel: 100,
							minValue: 0,
							grid: {
								millisPerLine: 1000,
								verticalSections: 10,
							},
						}),
					canvas: document.createElement('canvas'),
					timeseries: new TimeSeries(),
				};
				gGraphs[key] = graph;
				graph.canvas.width = 240;
				graph.canvas.height = 64;
				var div = document.createElement('div');
				div.innerText = key;
				div.style.flex = '0';
				document.getElementById('graphs').appendChild(div);
				document.getElementById('graphs').appendChild(graph.canvas);
				graph.chart.streamTo(graph.canvas, 1000);
				graph.chart.addTimeSeries(graph.timeseries, {lineWidth: 2});
			}
			gGraphs[key].timeseries.append(now, message.stats[key]);
		}
	}
}

function keyEvent(event) {
	send({
		event: "key",
		type: event.type,
		which: event.which,
		keyCode: event.keyCode,
		charCode: event.charCode,
		character: String.fromCharCode(event.keyCode || event.which),
		altKey: event.altKey,
	});
}

function setStatusMessage(message, color, keep) {
	var node = document.getElementById("status");
	if (!keep) {
		while (node.firstChild) {
			node.removeChild(node.firstChild);
		}
	}
	if (message) {
		node.appendChild(document.createTextNode(message));
		node.setAttribute("style", "display: inline-block; vertical-align: top; white-space: pre; color: " + (color || kErrorColor));
	}
}

function send(value) {
	try {
		gSocket.send(JSON.stringify(value));
	} catch (error) {
		setStatusMessage("Send failed: " + error.toString(), kErrorColor);
	}
}

function updateLogin() {
	var login = document.getElementById("login");
	while (login.firstChild) {
		login.removeChild(login.firstChild);
	}

	var a = document.createElement("a");
	if (gCredentials && gCredentials.session) {
		a.appendChild(document.createTextNode("logout " + gCredentials.session.name));
		a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash()));
	} else {
		a.appendChild(document.createTextNode("login"));
		a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash()));
	}
	login.appendChild(a);
}

var gOriginalInput;
function dragHover(event) {
	event.stopPropagation();
	event.preventDefault();
	var input = document.getElementById("input");
	if (event.type == "dragover") {
		if (!input.classList.contains("drop")) {
			input.classList.add("drop");
			gOriginalInput = input.value;
			input.value = "drop file to upload";
		}
	} else {
		input.classList.remove("drop");
		input.value = gOriginalInput;
	}
}

function fixImage(sourceData, maxWidth, maxHeight, callback) {
	var result = sourceData;
	var image = new Image();
	image.crossOrigin = "anonymous";
	image.referrerPolicy = "no-referrer";
	image.onload = function() {
		if (image.width > maxWidth || image.height > maxHeight) {
			var downScale = Math.min(maxWidth / image.width, maxHeight / image.height);
			var canvas = document.createElement("canvas");
			canvas.width = image.width * downScale;
			canvas.height = image.height * downScale;
			var context = canvas.getContext("2d");
			context.clearRect(0, 0, canvas.width, canvas.height);
			image.width = canvas.width;
			image.height = canvas.height;
			context.drawImage(image, 0, 0, image.width, image.height);
			result = canvas.toDataURL();
		}
		callback(result);
	};
	image.src = sourceData;
}

function sendImage(image) {
	fixImage(image, 320, 240, function(result) {
		send({image: result});
	});
}

function fileDropRead(event) {
	sendImage(event.target.result);
}

function fileDrop(event) {
	dragHover(event);

	var done = false;
	if (!done) {
		var files = event.target.files || event.dataTransfer.files;
		for (var i = 0; i < files.length; i++) {
			var file = files[i];
			if (file.type.substring(0, "image/".length) == "image/") {
				var reader = new FileReader();
				reader.onloadend = fileDropRead;
				reader.readAsDataURL(file);
				done = true;
			}
		}
	}

	if (!done) {
		var html = event.dataTransfer.getData("text/html");
		var match = /<img.*src="([^"]+)"/.exec(html);
		if (match) {
			sendImage(match[1]);
			done = true;
		}
	}

	if (!done) {
		var text = event.dataTransfer.getData("text/plain");
		if (text) {
			send(text);
			done = true;
		}
	}
}

function enableDragDrop() {
	var body = document.body;
	body.addEventListener("dragover", dragHover);
	body.addEventListener("dragleave", dragHover);
	body.addEventListener("drop", fileDrop);
}

function hashChange() {
	send({event: 'hashChange', hash: window.location.hash});
}

function focus() {
	if (gSocket && gSocket.readyState == gSocket.CLOSED) {
		connectSocket();
	} else {
		send({event: "focus"});
	}
}

function blur() {
	if (gSocket && gSocket.readyState == gSocket.OPEN) {
		send({event: "blur"});
	}
}

function message(event) {
	if (event.data && event.data.event == "resizeMe" && event.data.width && event.data.height) {
		var iframe = document.getElementById("iframe_" + event.data.name);
		iframe.setAttribute("width", event.data.width);
		iframe.setAttribute("height", event.data.height);
	} else if (event.data && event.data.action == "setHash") {
		window.location.hash = event.data.hash;
	} else if (event.data && event.data.action == 'storeBlob') {
		fetch('/save', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/binary',
			},
			body: event.data.blob.buffer,
		}).then(function(response) {
			if (!response.ok) {
				throw new Error(response.status + ' ' + response.statusText);
			}
			return response.text();
		}).then(function(text) {
			var iframe = document.getElementById("document");
			iframe.contentWindow.postMessage({'storeBlobComplete': {name: event.data.blob.name, path: text, type: event.data.blob.type, context: event.data.context}}, '*');
		});
	} else {
		send({event: "message", message: event.data});
	}
}

function reconnect(path) {
	let oldSocket = gSocket;
	gSocket = null
	oldSocket.onopen = null;
	oldSocket.onclose = null;
	oldSocket.onmessage = null;
	oldSocket.close();
	connectSocket(path);
}

function connectSocket(path) {
	if (!gSocket || gSocket.readyState != gSocket.OPEN) {
		if (gSocket) {
			gSocket.onopen = null;
			gSocket.onclose = null;
			gSocket.onmessage = null;
			gSocket.close();
		}
		setStatusMessage("Connecting...", kStatusColor, false);
		gSocket = new WebSocket(
			(window.location.protocol == "https:" ? "wss://" : "ws://")
			+ window.location.hostname
			+ (window.location.port.length ? ":" + window.location.port : "")
			+ "/app/socket");
		gSocket.onopen = function() {
			setStatusMessage("...Authenticating...", kStatusColor, true);
			gSocket.send(JSON.stringify({
				action: "hello",
				path: path,
				api: [
					['setDocument', 'content'],
					['postMessage', 'message'],
					['error', 'error'],
					['localStorageSet', 'key', 'value'],
					['localStorageGet', 'key'],
				],
			}));
		}
		gSocket.onmessage = function(event) {
			receive(JSON.parse(event.data));
		}
		gSocket.onclose = function(event) {
			const k_codes = {
				1000: 'Normal closure',
				1001: 'Going away',
				1002: 'Protocol error',
				1003: 'Unsupported data',
				1005: 'No status received',
				1006: 'Abnormal closure',
				1007: 'Invalid frame payload data',
				1008: 'Policy violation',
				1009: 'Message too big',
				1010: 'Missing extension',
				1011: 'Internal error',
				1012: 'Service restart',
				1013: 'Try again later',
				1014: 'Bad gateway',
				1015: 'TLS handshake',
			};
			setStatusMessage("Connection closed: " + (k_codes[event.code] || event.code), kErrorColor);
		}
	}
}

function openFile(name) {
	var newDoc = (name && gFiles[name]) ? gFiles[name].doc : new CodeMirror.Doc("", guessMode(name));
	var oldDoc = gEditor.swapDoc(newDoc);
	if (gFiles[gCurrentFile]) {
		gFiles[gCurrentFile].doc = oldDoc;
	}
	gCurrentFile = name;
	updateFiles();
	gEditor.focus();
}

function onFileClicked(event) {
	openFile(event.target.textContent);
}

function updateFiles() {
	var node = document.getElementById("files");
	while (node.firstChild) {
		node.removeChild(node.firstChild);
	}

	for (var file of Object.keys(gFiles).sort()) {
		var li = document.createElement("li");
		li.onclick = onFileClicked;
		li.appendChild(document.createTextNode(file));
		if (file == gCurrentFile) {
			li.classList.add("current");
		}
		if (!gFiles[file].doc.isClean(gFiles[file].generation)) {
			li.classList.add("dirty");
		}
		node.appendChild(li);
	}

	gEditor.focus();
}

function makeNewFile(name) {
	gFiles[name] = {
		doc: new CodeMirror.Doc("", guessMode(name)),
		generation: -1,
	};
	openFile(name);
}

function newFile() {
	var name = prompt("Name of new file:", "file.js");
	if (name && !gFiles[name]) {
		makeNewFile(name);
	}
}

function removeFile() {
	if (confirm("Remove " + gCurrentFile + "?")) {
		delete gFiles[gCurrentFile];
		openFile(Object.keys(gFiles)[0]);
	}
}

window.addEventListener("load", function() {
	window.addEventListener("hashchange", hashChange);
	window.addEventListener("focus", focus);
	window.addEventListener("blur", blur);
	window.addEventListener("message", message, false);
	window.addEventListener("online", connectSocket);
	document.getElementById("name").value = window.location.pathname;
	for (let tag of document.getElementsByTagName('a')) {
		if (tag.accessKey) {
			tag.classList.add('tooltip_parent');
			var tooltip = document.createElement('div');
			tooltip.classList.add('tooltip');
			if (tag.dataset.tip) {
				var description = document.createElement('div');
				description.innerText = tag.dataset.tip;
				tooltip.appendChild(description);
			}
			var parts = tag.accessKeyLabel ? tag.accessKeyLabel.split('+') : [];
			for (var i = 0; i < parts.length; i++)
			{
				var key = parts[i];
				var kbd = document.createElement('kbd');
				kbd.innerText = key;
				tooltip.appendChild(kbd);
				if (i < parts.length - 1) {
					tooltip.appendChild(document.createTextNode('+'));
				}
			}
			tag.appendChild(tooltip);
		}
	}
	enableDragDrop();
	connectSocket(window.location.pathname);

	if (window.localStorage.getItem('editing') == '1') {
		edit();
	} else {
		closeEditor();
	}
	if (window.localStorage.getItem('files') == '1') {
		showFiles();
	} else {
		hideFiles();
	}
	if (window.localStorage.getItem('stats') == '1') {
		stats();
	} else {
		closeStats();
	}
});