import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfutils from './tf-utils.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
import Tribute from './tribute.esm.js';

class TfComposeElement extends LitElement {
	static get properties() {
		return {
			whoami: {type: String},
			users: {type: Object},
			root: {type: String},
			branch: {type: String},
			apps: {type: Object},
			drafts: {type: Object},
		};
	}

	static styles = styles;

	constructor() {
		super();
		this.users = {};
		this.root = undefined;
		this.branch = undefined;
		this.apps = undefined;
		this.drafts = {};
	}

	process_text(text) {
		if (!text) {
			return '';
		}
		/* Update mentions. */
		let draft = this.get_draft();
		let updated = false;
		for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
			let name = match[1];
			let link = match[2];
			let balance = 0;
			let bracket_end = match.index + match[1].length + '[]'.length - 1;
			for (let i = bracket_end; i >= 0; i--) {
				if (text.charAt(i) == ']') {
					balance++;
				} else if (text.charAt(i) == '[') {
					balance--;
				}
				if (balance <= 0) {
					name = text.substring(i + 1, bracket_end);
					break;
				}
			}
			if (!draft.mentions) {
				draft.mentions = {};
			}
			if (!draft.mentions[link]) {
				draft.mentions[link] = {
					link: link,
				};
			}
			draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
			updated = true;
		}
		if (updated) {
			this.requestUpdate();
		}
		return tfutils.markdown(text);
	}

	input(event) {
		let edit = this.renderRoot.getElementById('edit');
		let preview = this.renderRoot.getElementById('preview');
		preview.innerHTML = this.process_text(edit.value);
		let content_warning = this.renderRoot.getElementById('content_warning');
		let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
		if (content_warning && content_warning_preview) {
			content_warning_preview.innerText = content_warning.value;
		}
	}

	notify(draft) {
		this.dispatchEvent(new CustomEvent('tf-draft', {
			bubbles: true,
			composed: true,
			detail: {
				id: this.branch,
				draft: draft
			},
		}));
	}

	change() {
		let draft = this.get_draft();
		draft.text = this.renderRoot.getElementById('edit')?.value;
		draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
		this.notify(draft);
	}

	convert_to_format(buffer, type, mime_type) {
		return new Promise(function(resolve, reject) {
			let img = new Image();
			img.onload = function() {
				let canvas = document.createElement('canvas');
				let width_scale = Math.min(img.width, 1024) / img.width;
				let height_scale = Math.min(img.height, 1024) / img.height;
				let scale = Math.min(width_scale, height_scale);
				canvas.width = img.width * scale;
				canvas.height = img.height * scale;
				let context = canvas.getContext('2d');
				context.drawImage(img, 0, 0, canvas.width, canvas.height);
				let data_url = canvas.toDataURL(mime_type);
				let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
				resolve(result);
			};
			img.onerror = function(event) {
				reject(new Error('Failed to load image.'));
			};
			let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
			let original = `data:${type};base64,${btoa(raw)}`;
			img.src = original;
		});
	}

	async add_file(file) {
		try {
			let draft = this.get_draft();
			let self = this;
			let buffer = await file.arrayBuffer();
			let type = file.type;
			if (type.startsWith('image/')) {
				let best_buffer;
				let best_type;
				for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
					let test_buffer = await self.convert_to_format(buffer, file.type, format);
					if (!best_buffer || test_buffer.length < best_buffer.length) {
						best_buffer = test_buffer;
						best_type = format;
					}
				}
				buffer = best_buffer;
				type = best_type;
			} else {
				buffer = Array.from(new Uint8Array(buffer));
			}
			let id = await tfrpc.rpc.store_blob(buffer);
			let name = type.split('/')[0] + ':' + file.name;
			if (!draft.mentions) {
				draft.mentions = {};
			}
			draft.mentions[id] = {
				link: id,
				name: name,
				type: type,
				size: buffer.length ?? buffer.byteLength,
			};
			let edit = self.renderRoot.getElementById('edit');
			edit.value += `\n![${name}](${id})`;
			self.change();
			self.input();
		} catch(e) {
			alert(e?.message);
		}
	}

	paste(event) {
		let self = this;
		for (let item of event.clipboardData.items) {
			if (item.type?.startsWith('image/')) {
				let file = item.getAsFile();
				if (!file) {
					continue;
				}
				self.add_file(file);
				break;
			}
		}
	}

	async submit() {
		let self = this;
		let draft = this.get_draft();
		let edit = this.renderRoot.getElementById('edit');
		let message = {
			type: 'post',
			text: edit.value,
		};
		if (this.root || this.branch) {
			message.root = this.root;
			message.branch = this.branch;
		}
		if (Object.values(draft.mentions || {}).length) {
			message.mentions = Object.values(draft.mentions);
		}
		if (draft.content_warning !== undefined) {
			message.contentWarning = draft.content_warning;
		}
		console.log('Would post:', message);
		if (draft.encrypt_to) {
			let to = new Set(draft.encrypt_to);
			to.add(this.whoami);
			to = [...to];
			message.recps = to;
			console.log('message is now', message);
			message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
			console.log('encrypted as', message);
		}
		try {
			await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
				edit.value = '';
				self.change();
				self.notify(undefined);
				self.requestUpdate();
			});
		} catch (error) {
			alert(error.message);
		}
	}

	discard() {
		let edit = this.renderRoot.getElementById('edit');
		edit.value = '';
		this.change();
		let preview = this.renderRoot.getElementById('preview');
		preview.innerHTML = '';
		this.notify(undefined);
	}

	attach() {
		let self = this;
		let edit = this.renderRoot.getElementById('edit');
		let input = document.createElement('input');
		input.type = 'file';
		input.onchange = function(event) {
			let file = event.target.files[0];
			self.add_file(file);
		};
		input.click();
	}

	async autocomplete(text, callback) {
		this.last_autocomplete = text;
		let results = [];
		try {
			let rows = await tfrpc.rpc.query(`
				SELECT messages.content FROM messages_fts(?)
				JOIN messages ON messages.rowid = messages_fts.rowid
				WHERE messages.content LIKE ?
				ORDER BY timestamp DESC LIMIT 10
			`, ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]);
			for (let row of rows) {
				for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
					if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
						results.push({key: match[1], value: match[2]});
					}
				}
			}
		} finally {
			if (this.last_autocomplete === text) {
				callback(results);
			}
		}
	}

	firstUpdated() {
		let tribute = new Tribute({
			collection: [
				{
					values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
					selectTemplate: function(item) {
						return `[@${item.original.key}](${item.original.value})`;
					},
				},
				{
					trigger: '&',
					values: this.autocomplete,
					selectTemplate: function(item) {
						return `![${item.original.key}](${item.original.value})`;
					},
				},
			],
		});
		tribute.attach(this.renderRoot.getElementById('edit'));
	}

	updated() {
		super.updated();
		let edit = this.renderRoot.getElementById('edit');
		if (this.last_updated_text !== edit.value) {
			let preview = this.renderRoot.getElementById('preview');
			preview.innerHTML = this.process_text(edit.value);
			this.last_updated_text = edit.value;
		}
		let encrypt = this.renderRoot.getElementById('encrypt_to');
		if (encrypt) {
			let tribute = new Tribute({
				values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
				selectTemplate: function(item) {
					return item.original.value;
				},
			});
			tribute.attach(encrypt);
		}
	}

	remove_mention(id) {
		let draft = this.get_draft();
		delete draft.mentions[id];
		this.notify(draft);
		this.requestUpdate();
	}

	render_mention(mention) {
		let self = this;
		return html`
			<div style="display: flex; flex-direction: row">
				<div style="align-self: center; margin: 0.5em">
					<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
				</div>
				<div style="display: flex; flex-direction: column">
					<h3>${mention.name}</h3>
					<div style="padding-left: 1em">
						${Object.entries(mention)
							.filter(x => x[0] != 'name')
							.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
					</div>
				</div>
			</div>`;
	}

	render_attach_app() {
		let self = this;

		async function attach_selected_app() {
			let name = self.renderRoot.getElementById('select').value;
			let id = self.apps[name];
			let mentions = {};
			mentions[id] = {
				name: name,
				link: id,
				type: 'application/tildefriends',
			};
			if (name && id) {
				let app = JSON.parse(await tfrpc.rpc.get_blob(id));
				for (let entry of Object.entries(app.files)) {
					mentions[entry[1]] = {
						name: entry[0],
						link: entry[1],
					};
				}
			}
			let draft = self.get_draft();
			draft.mentions = Object.assign(draft.mentions || {}, mentions);
			self.requestUpdate();
			self.notify(draft);
			self.apps = null;
		}

		if (this.apps) {
			return html`
				<div>
					<select id="select">
						${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
					</select>
					<input type="button" value="Attach" @click=${attach_selected_app}></input>
					<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
				</div>
				`;
		}
	}

	render_attach_app_button() {
		let self = this;
		async function attach_app() {
			self.apps = await tfrpc.rpc.apps();
		}
		if (!this.apps) {
			return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
		} else {
			return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
		}
	}

	set_content_warning(value) {
		let draft = this.get_draft();
		draft.content_warning = value;
		this.notify(draft);
		this.requestUpdate();
	}

	render_content_warning() {
		let self = this;
		let draft = this.get_draft();
		if (draft.content_warning !== undefined) {
			return html`
				<div>
					<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
					<label for="cw">CW</label>
					<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
				</div>
			`;
		} else {
			return html`
				<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
				<label for="cw">CW</label>
			`;
		}
	}

	get_draft() {
		return this.drafts[this.branch || ''] || {};
	}

	update_encrypt(event) {
		let input = event.srcElement;
		let matches = input.value.match(/@.*?\.ed25519/g);
		if (matches) {
			let draft = this.get_draft();
			let to = [...new Set(matches.concat(draft.encrypt_to))];
			this.set_encrypt(to);
			input.value = '';
		}
	}

	render_encrypt() {
		let draft = this.get_draft();
		if (draft.encrypt_to === undefined) {
			return;
		}
		return html`
			<div style="display: flex; flex-direction: row; width: 100%">
				<label for="encrypt_to">🔐 To:</label>
				<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
				<input type="button" value="🚮" @click=${() => this.set_encrypt(undefined)}></input>
			</div>
			<ul>
				${draft.encrypt_to.map(x => html`
					<li>
						<tf-user id=${x} .users=${this.users}></tf-user>
						<input type="button" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
					</li>`)}
			</ul>
		`;
	}

	set_encrypt(encrypt) {
		let draft = this.get_draft();
		draft.encrypt_to = encrypt;
		this.notify(draft);
		this.requestUpdate();
	}

	render() {
		let self = this;
		let draft = self.get_draft();
		let content_warning =
			draft.content_warning !== undefined ?
			html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
			undefined;
		let encrypt = draft.encrypt_to !== undefined ?
			undefined :
			html`<input type="button" value="🔐" @click=${() => this.set_encrypt([])}></input>`;
		let result = html`
			${this.render_encrypt()}
			<div style="display: flex; flex-direction: row; width: 100%">
				<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
				<div style="flex: 1 0 50%">
					${content_warning}
					<div id="preview"></div>
				</div>
			</div>
			${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
			${this.render_content_warning()}
			${this.render_attach_app()}
			<input type="button" id="submit" value="Submit" @click=${this.submit}></input>
			<input type="button" value="Attach" @click=${this.attach}></input>
			${this.render_attach_app_button()}
			${encrypt}
			<input type="button" value="Discard" @click=${this.discard}></input>
		`;
		return result;
	}
}

customElements.define('tf-compose', TfComposeElement);