import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js';

let cm6;
let gSocket;

let gCurrentFile;
let gFiles = {};
let gApp = {files: {}, emoji: '๐Ÿ“ฆ'};
let gEditor;
let gOriginalInput;

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

/* Functions that server-side app code can call through the app object. */
const k_api = {
	setDocument: {args: ['content'], func: api_setDocument},
	postMessage: {args: ['message'], func: api_postMessage},
	error: {args: ['error'], func: api_error},
	localStorageSet: {args: ['key', 'value'], func: api_localStorageSet},
	localStorageGet: {args: ['key'], func: api_localStorageGet},
	requestPermission: {args: ['permission', 'id'], func: api_requestPermission},
	print: {args: ['...'], func: api_print},
	setHash: {args: ['hash'], func: api_setHash},
};

const k_global_style = css`
	a:link {
		color: #268bd2;
	}

	a:visited {
		color: #6c71c4;
	}

	a:hover {
		color: #859900;
	}

	a:active {
		color: #2aa198;
	}
`;

class TfNavigationElement extends LitElement {
	static get properties() {
		return {
			credentials: {type: Object},
			permissions: {type: Object},
			show_permissions: {type: Boolean},
			status: {type: Object},
			spark_lines: {type: Object},
			version: {type: Object},
			show_version: {type: Boolean},
		};
	}

	constructor() {
		super();
		this.permissions = {};
		this.show_permissions = false;
		this.status = {};
		this.spark_lines = {};
	}

	toggle_edit(event) {
		event.preventDefault();
		if (editing()) {
			closeEditor();
		} else {
			edit();
		}
	}

	reset_permission(key) {
		send({action: "resetPermission", permission: key});
	}

	get_spark_line(key, options) {
		if (!this.spark_lines[key]) {
			let spark_line = document.createElement('tf-sparkline');
			spark_line.style.display = 'flex';
			spark_line.style.flexDirection = 'row';
			spark_line.style.flex = '0 50 5em';
			spark_line.title = key;
			if (options) {
				if (options.max) {
					spark_line.max = options.max;
				}
			}
			this.spark_lines[key] = spark_line;
			this.requestUpdate();
		}
		return this.spark_lines[key];
	}

	render_login() {
		if (this?.credentials?.session?.name) {
			return html`<a id="login" href="/login/logout?return=${url() + hash()}">logout ${this.credentials.session.name}</a>`;
		} else {
			return html`<a id="login" href="/login?return=${url() + hash()}">login</a>`;
		}
	}

	render_permissions() {
		if (this.show_permissions) {
			return html`
				<div style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%">
					<div style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff">
						<div>This app has the following permissions:</div>
						${Object.keys(this.permissions).map(key => html`
							<div>
								<span>${key}</span>: ${this.permissions[key] ? 'โœ… Allowed' : 'โŒ Denied'}
								<button @click=${() => this.reset_permission(key)} style='min-width: 3em; min-height: 3em">Reset</button>
							</div>
						`)}
						<button @click=${() => this.show_permissions = false} style="min-width: 3em; min-height: 3em">Close</button>
					</div>
				</div>
			`;
		}
	}

	render() {
		let self = this;
		return html`
			<style>
				${k_global_style}
				.tooltip {
					position: absolute;
					z-index: 1;
					display: none;
					border: 1px solid black;
					padding: 4px;
					color: black;
					background: white;
				}

				.tooltip_parent:hover .tooltip {
					display: inline-block;
				}
			</style>
			<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center">
				<span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>๐Ÿ˜Ž</span>
				<span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span>
				<a accesskey="h" @mouseover=${set_access_key_title} data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
				<a accesskey="a" @mouseover=${set_access_key_title} data-tip="Open apps list." href="/~core/apps/">apps</a>
				<a accesskey="e" @mouseover=${set_access_key_title} data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
				<a accesskey="p" @mouseover=${set_access_key_title} data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>๐ŸŽ›๏ธ</a>
				<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
				<span id="requests"></span>
				${this.render_permissions()}
				<span style="flex: 1 1; display: flex; flex-direction: row; white-space: nowrap; margin: 0; padding: 0">${Object.keys(this.spark_lines).sort().map(x => this.spark_lines[x]).map(x => [html`<span style="font-size: xx-small">${x.dataset.emoji}</span>`, x])}</span>
				<span style="flex: 0 0; white-space: nowrap">${this.render_login()}</span>
			</div>
		`;
	}
}
customElements.define('tf-navigation', TfNavigationElement);

class TfFilesElement extends LitElement {
	static get properties() {
		return {
			current: {type: String},
			files: {type: Object},
			dropping: {type: Number},
		};
	}

	constructor() {
		super();
		this.files = {};
		this.dropping = 0;
	}

	file_click(file) {
		this.dispatchEvent(new CustomEvent('file_click', {
			detail: {
				file: file,
			},
			bubbles: true,
			composed: true,
		}));
	}

	render_file(file) {
		let classes = ['file'];
		if (file == this.current) {
			classes.push('current');
		}
		if (!this.files[file].clean) {
			classes.push('dirty');
		}
		return html`<div class="${classes.join(' ')}" @click=${x => this.file_click(file)}>${file}</div>`;
	}

	async drop(event) {
		event.preventDefault();
		event.stopPropagation();
		this.dropping = 0;
		for (let file of event.dataTransfer.files) {
			let buffer = await file.arrayBuffer();
			let text = new TextDecoder('latin1').decode(buffer);
			gFiles[file.name] = {
				doc: new cm6.EditorState.create({doc: text, extensions: cm6.extensions}),
				buffer: buffer,
				generation: -1,
				isNew: true,
			};
			gCurrentFile = file.name;
		}
		openFile(gCurrentFile);
		updateFiles();
	}

	drag_enter(event) {
		this.dropping++;
		event.preventDefault();
	}

	drag_leave(event) {
		this.dropping--;
	}

	render() {
		let self = this;
		return html`
			<style>
				div.file {
					padding: 0.5em;
					cursor: pointer;
				}
				div.file:hover {
					background-color: #1a9188;
				}
				div.file::before {
					content: '๐Ÿ“„ ';
				}

				div.file.current {
					font-weight: bold;
					background-color: #2aa198;
				}

				div.file.dirty::after {
					content: '*';
				}
			</style>
			<div @drop=${this.drop} @dragenter=${this.drag_enter} @dragleave=${this.drag_leave}>
				${Object.keys(this.files).sort().map(x => self.render_file(x))}
			</div>
			<div
				?hidden=${this.dropping == 0}
				@drop=${this.drop} @dragenter=${this.drag_enter} @dragleave=${this.drag_leave}
				style="text-align: center; vertical-align: middle; outline: 16px solid red; margin: -8px; background-color: rgba(255, 0, 0, 0.5); position: absolute; left: 16px; top: 16px; width: calc(100% - 16px); height: calc(100% - 16px); z-index: 1000">
				Drop File(s)
			</div>
		`;
	}
}
customElements.define('tf-files', TfFilesElement);

class TfFilesPaneElement extends LitElement {
	static get properties() {
		return {
			expanded: {type: Boolean},
			current: {type: String},
			files: {type: Object},
		};
	}

	constructor() {
		super();
		this.expanded = window.localStorage.getItem('files') != '0';
		this.files = {};
	}

	set_expanded(expanded) {
		this.expanded = expanded;
		window.localStorage.setItem('files', expanded ? '1' : '0');
	}

	render() {
		let self = this;
		let expander = this.expanded ?
			html`<span @click=${() => self.set_expanded(false)} class="expander">ยซ</span>` :
			html`<span @click=${() => self.set_expanded(true)} class="expander">ยป</span>`;
		let content = html`
			<div id="files_content">
				<tf-files .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
				<br>
				<div><button @click=${() => newFile()} accesskey="n" @mouseover=${set_access_key_title} data-tip="Add a new, empty file to the app">New File</button></div>
				<div><button @click=${() => removeFile()} accesskey="r" @mouseover=${set_access_key_title} data-tip="Remove the selected file from the app">Remove File</button></div>
			</div>
		`;
		return html`
			<style>
				.expander {
					font-weight: bold;
					width: 100%;
					right: 0;
					flex: 0;
					padding: 0.25em;
					cursor: pointer;
				}
			</style>
			<div>
				<div style="display: flex; flex-direction: row">
					${this.expanded ? html`<span style="font-weight: bold; text-align: center; flex: 1">Files</span>` : undefined}
					${expander}
				</div>
				${this.expanded ? content : undefined}
			</div>
		`;
	}
}
customElements.define('tf-files-pane', TfFilesPaneElement);

class TfSparkLineElement extends LitElement {
	static get properties() {
		return {
			lines: {type: Array},
			min: {type: Number},
			max: {type: Number},
		};
	}

	constructor() {
		super();
		this.min = 0;
		this.max = 1.0;
		this.lines = [];
		this.k_values_max = 100;
	}

	append(key, value) {
		let line = null;
		for (let it of this.lines) {
			if (it.name == key) {
				line = it;
				break;
			}
		}
		if (!line) {
			const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
			line = {
				name: key,
				style: k_colors[this.lines.length % k_colors.length],
				values: Array(this.k_values_max).fill(0),
			};
			this.lines.push(line);
		}
		if (line.values.length >= this.k_values_max) {
			line.values.shift();
		}
		line.values.push(value);
		this.requestUpdate();
	}

	render_line(line) {
		if (line?.values?.length >= 2) {
			let max = Math.max(this.max, ...line.values);
			let points = [].concat(...line.values.map((x, i) => [50.0 * i / (line.values.length - 1), 10.0 - 10.0 * (x - this.min) / (max - this.min)]));
			return svg`<polyline points=${points.join(' ')} stroke=${line.style} fill="none"/>`;
		}
	}

	render() {
		let max = Math.round(10.0 * Math.max(...this.lines.map(line => line.values[line.values.length - 1]))) / 10.0;
		return html`
			<svg style="max-width: 7.5em; max-height: 1.5em; margin: 0; padding: 0; background: #000" viewBox="0 0 50 10" xmlns="http://www.w3.org/2000/svg">
				${this.lines.map(x => this.render_line(x))}
				<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">${max}</text>
			</svg>
		`;
	}
}
customElements.define('tf-sparkline', TfSparkLineElement);

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

	let search = nodes.shift();
	let head = document.head;
	let found = false;
	for (let i = 0; i < head.childNodes.length; i++) {
		if (head.childNodes[i].tagName == search.tagName) {
			let match = true;
			for (let 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 {
		let node = document.createElement(search.tagName);
		node.onreadystatechange = node.onload = function() {
			ensureLoaded(nodes, callback);
		};
		for (let 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 is_edit_only() {
	return window.location.search == '?editonly=1' || window.innerWidth < 1024;
}

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

	window.localStorage.setItem('editing', '1');
	document.getElementById("editPane").style.display = 'flex';
	document.getElementById('viewPane').style.display =  is_edit_only() ? 'none' : 'flex';

	try {
		cm6 = await import('/codemirror/cm6.js');
		gEditor = cm6.TildeFriendsEditorView(document.getElementById("editor"));
		gEditor.onDocChange = updateFiles;
		await load();
	} catch (error) {
		alert(`${error.message}\n\n${error.stack}`);
		closeEditor();
	}
}

function trace() {
	window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
}

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) {
			alert(`Request failed for ${name}: ${response.status} ${response.statusText}`);
			return 'missing file!';
		}
		return response.text();
	}).then(function(text) {
		gFiles[name].doc = cm6.EditorState.create({doc: text, extensions: cm6.extensions});
		gFiles[name].original = gFiles[name].doc.doc.toString();
		if (!Object.values(gFiles).some(x => !x.doc)) {
			openFile(Object.keys(gFiles).sort()[0]);
		}
	});
}

async function load(path) {
	let response = await fetch((path || url()) + 'view');
	if (!response.ok) {
		if (response.status == 404) {
			return null;
		} else {
			throw new Error(response.status + ' ' + response.statusText);
		}
	}
	let json = await response.json();
	gFiles = {};
	let isApp = false;
	let 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;
		gApp.emoji = gApp.emoji || '๐Ÿ“ฆ';
		document.getElementById('icon').innerHTML = gApp.emoji;
	}
	if (!isApp) {
		document.getElementById("editPane").style.display = 'flex';
		let text = '// New script.\n';
		gCurrentFile = 'app.js';
		gFiles[gCurrentFile] = {
			doc: cm6.EditorState.create({doc: text, extensions: cm6.extensions}),
		};
		openFile(gCurrentFile);
	}
	return Promise.all(promises);
}

function closeEditor() {
	window.localStorage.setItem('editing', '0');
	document.getElementById("editPane").style.display = 'none';
	document.getElementById('viewPane').style.display = 'flex';
}

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

function save(save_to) {
	document.getElementById("save").disabled = true;
	if (gCurrentFile) {
		gFiles[gCurrentFile].doc = gEditor.state;
		if (!gFiles[gCurrentFile].isNew && !gFiles[gCurrentFile].doc.doc.toString() == gFiles[gCurrentFile].original) {
			delete gFiles[gCurrentFile].buffer;
		}
	}

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

	let promises = [];
	for (let name of Object.keys(gFiles)) {
		let file = gFiles[name];
		if (!file.isNew && file.doc.doc.toString() == file.original) {
			continue;
		}

		delete file.id;
		delete file.isNew;
		promises.push(fetch('/save', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/binary',
			},
			body: file.buffer ?? file.doc.doc.toString(),
		}).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() {
		let app = {
			type: "tildefriends-app",
			files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])),
			emoji: gApp.emoji || '๐Ÿ“ฆ',
		};
		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;
		Object.values(gFiles).forEach(function(file) {
			file.original = file.doc.doc.toString();
		});
		updateFiles();
	});
}

function changeIcon() {
	let value = prompt('Enter a new app icon emoji:');
	if (value !== undefined) {
		gApp.emoji = value || '๐Ÿ“ฆ';
		document.getElementById('icon').innerHTML = gApp.emoji;
	}
}

function deleteApp() {
	let name = document.getElementById("name");
	let path = name && name.value ? name.value : url();

	if (confirm(`Are you sure you want to delete the app '${path}'?`)) {
		fetch(path + 'delete').then(function(response) {
			if (!response.ok) {
				throw new Error(response.status + ' ' + response.statusText);
			}
			alert('Deleted.');
		}).catch(function(error) {
			alert(error);
		});
	}
}

function url() {
	let hash = window.location.href.indexOf('#');
	let question = window.location.href.indexOf('?');
	let 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 api_setDocument(content) {
	let iframe = document.getElementById("document");
	iframe.srcdoc = content;
}

function api_postMessage(message) {
	let iframe = document.getElementById("document");
	iframe.contentWindow.postMessage(message, "*");
}

function api_error(error) {
	if (error) {
		if (typeof(error) == 'string') {
			setStatusMessage('โš ๏ธ ' + error, '#f00');
		} else {
			setStatusMessage('โš ๏ธ ' + error.message + '\n' + error.stack, '#f00');
		}
	}
	console.log('error', error);
}

function api_localStorageSet(key, value) {
	window.localStorage.setItem('app:' + key, value);
}

function api_localStorageGet(key) {
	return window.localStorage.getItem('app:' + key);
}

function api_requestPermission(permission, id) {
	let outer = document.createElement('div');
	outer.classList.add('permissions');

	let container = document.createElement('div');
	container.classList.add('permissions_contents');

	let div = document.createElement('div');
	div.appendChild(document.createTextNode('This app is requesting the following permission:'));
	let span = document.createElement('span');
	span.style = 'font-weight: bold';
	span.appendChild(document.createTextNode(permission));
	div.appendChild(span);
	container.appendChild(div);

	div = document.createElement('div');
	div.style = 'padding: 1em';
	let check = document.createElement('input');
	check.id = 'permissions_remember_check';
	check.type = 'checkbox';
	div.appendChild(check);
	let label = document.createElement('label');
	label.htmlFor = check.id;
	label.appendChild(document.createTextNode('Remember this decision.'));
	div.appendChild(label);
	container.appendChild(div);

	const k_options = [
		{
			id: 'allow',
			text: 'โœ… Allow',
			grant: ['allow once', 'allow'],

		},
		{
			id: 'deny',
			text: 'โŒ Deny',
			grant: ['deny once', 'deny'],
		},
	];

	return new Promise(function(resolve, reject) {
		div = document.createElement('div');
		for (let option of k_options) {
			let button = document.createElement('button');
			button.style.minWidth = '3em';
			button.style.minHeight = '3em';
			button.innerText = option.text;
			button.id = option.id;
			button.onclick = function() {
				resolve(option.grant[check.checked ? 1 : 0]);
				document.body.removeChild(outer);
			}
			div.appendChild(button);
		}
		container.appendChild(div);
		outer.appendChild(container);

		document.body.appendChild(outer);
	});
}

function api_print() {
	console.log('app>', ...arguments);
}

function api_setHash(hash) {
	window.location.hash = hash;
}

function _receive_websocket_message(message) {
	if (message && message.action == "session") {
		setStatusMessage("๐ŸŸข Executing...", kStatusColor);
		document.getElementsByTagName('tf-navigation')[0].credentials = message.credentials;
	} else if (message && message.action == 'permissions') {
		document.getElementsByTagName('tf-navigation')[0].permissions = message.permissions ?? {};
	} else if (message && message.action == "ready") {
		setStatusMessage(null);
		if (window.location.hash) {
			send({event: "hashChange", hash: window.location.hash});
		}
		document.getElementsByTagName('tf-navigation')[0].version = message.version;
		document.getElementById('viewPane').style.display = message.edit_only ? 'none' : 'flex';
		send({action: 'enableStats', enabled: true});
	} else if (message && message.action == "ping") {
		send({action: "pong"});
	} else if (message && message.action == "stats") {
		let now = new Date().getTime();
		for (let key of Object.keys(message.stats)) {
			const k_groups = {
				rpc_in: {group: 'rpc', name: 'in'},
				rpc_out: {group: 'rpc', name: 'out'},

				cpu_percent: {group: 'cpu', name: 'main'},
				thread_percent: {group: 'cpu', name: 'work'},

				arena_percent: {group: 'memory', name: 'm'},
				js_malloc_percent: {group: 'memory', name: 'js'},
				memory_percent: {group: 'memory', name: 'tot'},
				sqlite3_memory_percent: {group: 'memory', name: 'sql'},
				tf_malloc_percent: {group: 'memory', name: 'tf'},
				tls_malloc_percent: {group: 'memory', name: 'tls'},
				uv_malloc_percent: {group: 'memory', name: 'uv'},

				messages_stored: {group: 'store', name: 'messages'},
				blobs_stored: {group: 'store', name: 'blobs'},

				socket_count: {group: 'socket', name: 'total'},
				socket_open_count: {group: 'socket', name: 'open'},

				import_count: {group: 'functions', name: 'imports'},
				export_count: {group: 'functions', name: 'exports'},
			};
			const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
			let graph_key = k_groups[key]?.group || key;
			if (['cpu', 'rpc', 'store', 'memory'].indexOf(graph_key) != -1) {
				let line = document.getElementsByTagName('tf-navigation')[0].get_spark_line(graph_key, { max: 100 });
				line.dataset.emoji = {
					'cpu': '๐Ÿ’ป',
					'rpc': '๐Ÿ”',
					'store': '๐Ÿ’พ',
					'memory': '๐Ÿ',
				}[graph_key];
				line.append(key, message.stats[key]);
			}
		}
	} else if (message &&
		message.message === 'tfrpc' &&
		message.method) {
		let api = k_api[message.method];
		let id = message.id;
		let params = message.params;
		if (api) {
			Promise.resolve(api.func(...params)).then(function(result) {
				send({
					message: 'tfrpc',
					id: id,
					result: result,
				});
			}).catch(function(error) {
				send({
					message: 'tfrpc',
					id: id,
					error: error,
				});
			});
		}
	}
}

function setStatusMessage(message, color) {
	document.getElementsByTagName('tf-navigation')[0].status = {message: message, color: color};
}

function send(value) {
	try {
		if (gSocket && gSocket.readyState == gSocket.OPEN) {
			gSocket.send(JSON.stringify(value));
		}
	} catch (error) {
		setStatusMessage('๐Ÿคท Send failed: ' + error.toString(), kErrorColor);
	}
}

function fixImage(sourceData, maxWidth, maxHeight, callback) {
	let result = sourceData;
	let image = new Image();
	image.crossOrigin = "anonymous";
	image.referrerPolicy = "no-referrer";
	image.onload = function() {
		if (image.width > maxWidth || image.height > maxHeight) {
			let downScale = Math.min(maxWidth / image.width, maxHeight / image.height);
			let canvas = document.createElement("canvas");
			canvas.width = image.width * downScale;
			canvas.height = image.height * downScale;
			let 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 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) {
		let 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) {
			let 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
	if (oldSocket) {
		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);
		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);
			let connect_path = path ?? window.location.pathname;
			gSocket.send(JSON.stringify({
				action: "hello",
				path: connect_path,
				url: window.location.href,
				edit_only: editing() && is_edit_only(),
				api: Object.entries(k_api).map(([key, value]) => [].concat([key], value.args)),
			}));
		}
		gSocket.onmessage = function(event) {
			_receive_websocket_message(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("๐Ÿ”ด Closed: " + (k_codes[event.code] || event.code), kErrorColor);
		}
	}
}

function openFile(name) {
	let newDoc = (name && gFiles[name]) ? gFiles[name].doc : cm6.EditorState.create({doc: "", extensions: cm6.extensions});
	let oldDoc = gEditor.state;
	gEditor.setState(newDoc);

	if (gFiles[gCurrentFile]) {
		gFiles[gCurrentFile].doc = oldDoc;
		if (!gFiles[gCurrentFile].isNew && gFiles[gCurrentFile].doc.doc.toString() == oldDoc.doc.toString()) {
			delete gFiles[gCurrentFile].buffer;
		}
	}
	gCurrentFile = name;
	updateFiles();
	gEditor.focus();
}

function updateFiles() {
	let files = document.getElementsByTagName("tf-files-pane")[0];
	if (files) {
		files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, {
			clean: (file == gCurrentFile ? gEditor.state.doc.toString() : gFiles[file].doc.doc.toString()) == gFiles[file].original,
		}]));
		files.current = gCurrentFile;
	}
	gEditor.focus();
}

function makeNewFile(name) {
	gFiles[name] = {
		doc: cm6.EditorState.create({extensions: cm6.extensions}),
		generation: -1,
	};
	openFile(name);
}

function newFile() {
	let 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;
	document.getElementById('closeEditor').addEventListener('click', () => closeEditor());
	document.getElementById('save').addEventListener('click', () => save());
	document.getElementById('icon').addEventListener('click', () => changeIcon());
	document.getElementById('delete').addEventListener('click', () => deleteApp());
	document.getElementById('trace_button').addEventListener('click', function(event) {
		event.preventDefault();
		trace();
	});
	connectSocket(window.location.pathname);

	if (window.localStorage.getItem('editing') == '1') {
		edit();
	} else {
		closeEditor();
	}
});