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) {
		const k_flag_sequence_before_author = 1;
		let out = {
			previous: message.previous ?? null,
		};
		if (message.flags & k_flag_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 author, id, sequence, timestamp, hash, json(content) AS content, signature, flags
				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 messages_refs.ref AS id
			FROM messages
			JOIN messages_refs ON messages.id = messages_refs.message
			WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
			[id]
		);
		let blobs_done = 0;
		for (let row of blobs) {
			this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
			let blob;
			try {
				blob = await tfrpc.rpc.get_blob(row.id);
			} catch (e) {
				console.log(`Failed to get ${row.id}: ${e.message}`);
			}
			if (blob) {
				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);
