import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';

class TfSneakerAppElement extends LitElement {
	static get properties() {
		return {
			feeds: {type: Object},
			progress: {type: Object},
			result: {type: String},
		};
	}

	constructor() {
		super();
		this.feeds = [];
		this.progress = undefined;
		this.result = undefined;
	}

	async search() {
		let q = this.renderRoot.getElementById('search').value;
		let result = await tfrpc.rpc.query(`
			SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
			FROM messages_fts(?)
			JOIN messages ON messages.rowid = messages_fts.rowid
			WHERE
				json_extract(messages.content, '$.type') = 'about' AND
				json_extract(messages.content, '$.about') = messages.author AND
				json_extract(messages.content, '$.name') IS NOT NULL
			GROUP BY messages.author
			HAVING MAX(messages.sequence)
			ORDER BY COUNT(*) DESC
			`,
			[`"${q.replaceAll('"', '""')}"`]);
		this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
	}

	format_message(message) {
		let out = {
			previous: message.previous ?? null,
		};
		if (message.sequence_before_author) {
			out.sequence = message.sequence;
			out.author = message.author;
		} else {
			out.author = message.author;
			out.sequence = message.sequence;
		}
		out.timestamp = message.timestamp;
		out.hash = message.hash;
		out.content = JSON.parse(message.content);
		out.signature = message.signature;
		return {key: message.id, value: out};
	}

	sanitize(value) {
		return value.replaceAll('/', '_').replaceAll('+', '-');
	}

	guess_ext(data) {
		function startsWith(prefix) {
			if (data.length < prefix.length) {
				return false;
			}
			for (let i = 0; i < prefix.length; i++) {
				if (prefix[i] !== null && data[i] !== prefix[i]) {
					return false;
				}
			}
			return true;
		}

		if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
			startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
			startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
			startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
			return '.jpg';
		} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
			return '.png';
		} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
			startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
			return '.gif';
		} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
			return '.webp';
		} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
			return '.svg';
		} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
			return '.mp3';
		} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
			startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
			return '.mp4';
		} else {
			return '.bin';
		}
	}

	async export(id) {
		let all_messages = '';
		let sequence = -1;
		let messages_done = 0;
		let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
		while (true) {
			let messages = await tfrpc.rpc.query(
					'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
					[id, sequence]
			);
			if (messages?.length) {
				all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
				sequence = messages[messages.length - 1].sequence;
				messages_done += messages.length;
				this.progress = {name: 'messages', value: messages_done, max: messages_max};
			} else {
				break;
			}
		}

		let zip = new JSZip();
		zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);

		let blobs = await tfrpc.rpc.query(
			`SELECT blobs.id
			FROM messages
			JOIN messages_refs ON messages.id = messages_refs.message
			JOIN blobs ON messages_refs.ref = blobs.id
			WHERE messages.author = ?`,
			[id]);
		let blobs_done = 0;
		for (let row of blobs) {
			this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
			let blob = await tfrpc.rpc.get_blob(row.id);
			zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
			blobs_done++;
		}

		this.progress = {name: 'saving'};
		let blob = await zip.generateAsync({type: 'blob'});
		saveAs(blob, `${this.sanitize(id)}.zip`);
		this.progress = null;
	}

	keypress(event) {
		if (event.key == 'Enter') {
			this.search();
		}
	}

	async import(event) {
		let file = event.target.files[0];
		if (!file) {
			return;
		}

		this.progress = {name: 'loading'};
		let zip = new JSZip();
		file = await zip.loadAsync(file);
		let messages = [];
		let blobs = [];
		file.forEach(function(path, entry) {
			if (!entry.dir) {
				if (path.startsWith('message/classic/')) {
					messages.push(entry);
				} else {
					blobs.push(entry);
				}
			}
		});
		let success = {messages: 0, blobs: 0};
		let progress = 0;
		let total_messages = 0;
		for (let entry of messages) {
			let lines = (await entry.async('string')).split('\n');
			total_messages += lines.length;
			for (let line of lines) {
				if (!line.length) {
					continue;
				}
				let message = JSON.parse(line);
				this.progress = {name: 'messages', value: progress++, max: total_messages};
				if (await tfrpc.rpc.store_message(message.value)) {
					success.messages++;
				}
			}
		}
		progress = 0;
		for (let blob of blobs) {
			this.progress = {name: 'blobs', value: progress++, max: blobs.length};
			if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) {
				success.blobs++;
			}
		}
		this.progress = undefined;
		this.result = `imported ${success.messages} messages and ${success.blobs} blobs`;
	}

	render() {
		let progress;
		if (this.progress) {
			if (this.progress.max) {
				progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
			} else {
				progress = html`<div><span>${this.progress.name}</span></div>`;
			}
		}
		return html`<h1>SSB 👟net</h1>
			<code>${this.result}</code>
			${progress}

			<h2>Import</h2>
			<input type="file" id="import" @change=${this.import}></input>

			<h2>Export</h2>
			<input type="text" id="search" @keypress=${this.keypress}></input>
			<input type="button" value="Search Users" @click=${this.search}></input>
			<ul>
				${Object.entries(this.feeds).map(([id, name]) => html`
					<li>
						${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
						${name}
						<code style="color: #ccc">${id}</code>
					</li>
				`)}
			</ul>
		`;
	}
}
customElements.define('tf-sneaker-app', TfSneakerAppElement);