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

class TfElement extends LitElement {
	static get properties() {
		return {
			whoami: {type: String},
			hash: {type: String},
			unread: {type: Array},
			tab: {type: String},
			broadcasts: {type: Array},
			connections: {type: Array},
			loading: {type: Boolean},
			loaded: {type: Boolean},
			following: {type: Array},
			users: {type: Object},
			ids: {type: Array},
			tags: {type: Array},
		};
	}

	static styles = styles;

	constructor() {
		super();
		let self = this;
		this.hash = '#';
		this.unread = [];
		this.tab = 'news';
		this.broadcasts = [];
		this.connections = [];
		this.following = [];
		this.users = {};
		this.loaded = false;
		this.tags = [];
		tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
		tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
		tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
		tfrpc.register(function hashChanged(hash) {
			self.set_hash(hash);
		});
		tfrpc.register(async function notifyNewMessage(id) {
			await self.fetch_new_message(id);
		});
		tfrpc.register(function set(name, value) {
			if (name === 'broadcasts') {
				self.broadcasts = value;
			} else if (name === 'connections') {
				self.connections = value;
			}
		});
		this.initial_load();
	}

	async initial_load() {
		let whoami = await tfrpc.rpc.localStorageGet('whoami');
		let ids = (await tfrpc.rpc.getIdentities()) || [];
		this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
		this.ids = ids;
	}

	set_hash(hash) {
		this.hash = hash || '#';
		if (this.hash.startsWith('#q=')) {
			this.tab = 'search';
		} else if (this.hash === '#connections') {
			this.tab = 'connections';
		} else if (this.hash === '#mentions') {
			this.tab = 'mentions';
		} else if (this.hash.startsWith('#sql=')) {
			this.tab = 'query';
		} else {
			this.tab = 'news';
		}
	}

	async contacts_internal(id, last_row_id, following, max_row_id) {
		let result = Object.assign({}, following[id] || {});
		result.following = result.following || {};
		result.blocking = result.blocking || {};
		let contacts = await tfrpc.rpc.query(
			`
				SELECT content FROM messages
				WHERE author = ? AND
				rowid > ? AND
				rowid <= ? AND
				json_extract(content, '$.type') = 'contact'
				ORDER BY sequence
			`,
			[id, last_row_id, max_row_id]);
		for (let row of contacts) {
			let contact = JSON.parse(row.content);
			if (contact.following === true) {
				result.following[contact.contact] = true;
			} else if (contact.following === false) {
				delete result.following[contact.contact];
			} else if (contact.blocking === true) {
				result.blocking[contact.contact] = true;
			} else if (contact.blocking === false) {
				delete result.blocking[contact.contact];
			}
		}
		following[id] = result;
		return result;
	}

	async contact(id, last_row_id, following, max_row_id) {
		return await this.contacts_internal(id, last_row_id, following, max_row_id);
	}

	async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
		let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
		let result = {};
		for (let i = 0; i < ids.length; i++) {
			let id = ids[i];
			let contact = contacts[i];
			let all_blocking = Object.assign({}, contact.blocking, blocking);
			let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
			let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
			result[id] = [id, ...found, ...deeper];
		}
		return [...new Set(Object.values(result).flat())];
	}

	async following_deep(ids, depth, blocking) {
		const k_cache_version = 5;
		let cache = await tfrpc.rpc.databaseGet('following');
		cache = cache ? JSON.parse(cache) : {};
		if (cache.version !== k_cache_version) {
			cache = {
				version: k_cache_version,
				following: {},
				last_row_id: 0,
			};
		}
		let max_row_id = (await tfrpc.rpc.query(`
			SELECT MAX(rowid) AS max_row_id FROM messages
		`, []))[0].max_row_id;
		let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
		cache.last_row_id = max_row_id;
		let store = JSON.stringify(cache);
		/* 2023-02-20: Exceeding message size. */
		//if (store.length < 512 * 1024) {
			await tfrpc.rpc.databaseSet('following', store);
		//}
		return [result, cache.following];
	}

	async fetch_about(ids, users) {
		const k_cache_version = 1;
		let cache = await tfrpc.rpc.databaseGet('about');
		cache = cache ? JSON.parse(cache) : {};
		if (cache.version !== k_cache_version) {
			cache = {
				version: k_cache_version,
				about: {},
				last_row_id: 0,
			};
		}
		let max_row_id = (await tfrpc.rpc.query(`
			SELECT MAX(rowid) AS max_row_id FROM messages
		`, []))[0].max_row_id;
		for (let id of Object.keys(cache.about)) {
			if (ids.indexOf(id) == -1) {
				delete cache.about[id];
			}
		}

		let abouts = await tfrpc.rpc.query(
			`
				SELECT
					messages.*
				FROM
					messages,
					json_each(?1) AS following
				WHERE
					messages.author = following.value AND
					messages.rowid > ?3 AND
					messages.rowid <= ?4 AND
					json_extract(messages.content, '$.type') = 'about'
				UNION
				SELECT
					messages.*
				FROM
					messages,
					json_each(?2) AS following
				WHERE
					messages.author = following.value AND
					messages.rowid <= ?4 AND
					json_extract(messages.content, '$.type') = 'about'
				ORDER BY messages.author, messages.sequence
			`,
			[
				JSON.stringify(ids.filter(id => cache.about[id])),
				JSON.stringify(ids.filter(id => !cache.about[id])),
				cache.last_row_id,
				max_row_id,
			]);
		for (let about of abouts) {
			let content = JSON.parse(about.content);
			if (content.about === about.author) {
				delete content.type;
				delete content.about;
				cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
			}
		}
		cache.last_row_id = max_row_id;
		await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
		users = users || {};
		for (let id of Object.keys(cache.about)) {
			users[id] = Object.assign(users[id] || {}, cache.about[id]);
		}
		return Object.assign({}, users);
	}

	async fetch_new_message(id) {
		let messages = await tfrpc.rpc.query(
			`
				SELECT messages.*
				FROM messages
				JOIN json_each(?) AS following ON messages.author = following.value
				WHERE messages.id = ?
			`,
			[
				JSON.stringify(this.following),
				id,
			]);
		if (messages && messages.length) {
			this.unread = [...this.unread, ...messages];
		}
	}

	async _handle_whoami_changed(event) {
		let old_id = this.whoami;
		let new_id = event.srcElement.selected;
		console.log('received', new_id);
		if (this.whoami !== new_id) {
			console.log(event);
			this.whoami = new_id;
			console.log(`whoami ${old_id} => ${new_id}`);
			await tfrpc.rpc.localStorageSet('whoami', new_id);
		}
	}

	async create_identity() {
		if (confirm("Are you sure you want to create a new identity?")) {
			await tfrpc.rpc.createIdentity();
			this.ids = (await tfrpc.rpc.getIdentities()) || [];
			if (this.ids && !this.whoami) {
				this.whoami = this.ids[0];
			}
		}
	}

	render_id_picker() {
		return html`
			<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
			<button @click=${this.create_identity} id="create_identity">Create Identity</button>
		`;
	}

	async load_recent_tags() {
		let start = new Date();
		this.tags = await tfrpc.rpc.query(`
			WITH
				recent AS (SELECT id, content FROM messages
					WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
					ORDER BY timestamp DESC LIMIT 1024),
				recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
					FROM recent
					WHERE json_extract(content, '$.channel') IS NOT NULL),
				recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
					FROM recent, json_each(recent.content, '$.mentions') AS mention
					WHERE json_valid(mention.value) AND tag LIKE '#%'),
				combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
				by_message AS (SELECT DISTINCT id, tag FROM combined)
			SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
		`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
		console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
	}

	async load() {
		let whoami = this.whoami;
		let tags = this.load_recent_tags();
		let [following, users] = await this.following_deep([whoami], 2, {});
		users = await this.fetch_about(following.sort(), users);
		this.following = following;
		this.users = users;
		await tags;
		console.log(`load finished ${whoami} => ${this.whoami}`);
		this.whoami = whoami;
		this.loaded = whoami;
	}

	render_tab() {
		let following = this.following;
		let users = this.users;
		if (this.tab === 'news') {
			return html`
				<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
			`;
		} else if (this.tab === 'connections') {
			return html`
				<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
			`;
		} else if (this.tab === 'mentions') {
			return html`
				<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
			`;
		} else if (this.tab === 'search') {
			return html`
				<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
			`;
		} else if (this.tab === 'query') {
			return html`
				<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
			`;
		}
	}

	async set_tab(tab) {
		this.tab = tab;
		if (tab === 'news') {
			await tfrpc.rpc.setHash('#');
		} else if (tab === 'connections') {
			await tfrpc.rpc.setHash('#connections');
		} else if (tab === 'mentions') {
			await tfrpc.rpc.setHash('#mentions');
		} else if (tab === 'query') {
			await tfrpc.rpc.setHash('#sql=');
		}
	}

	render() {
		let self = this;

		if (!this.loading && this.whoami && this.loaded !== this.whoami) {
			this.loading = true;
			this.load().finally(function() {
				self.loading = false;
			});
		}

		let tabs = html`
			<div>
				<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
				<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
				<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
				<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
				<input type="button" class="tab" value="Query" ?disabled=${self.tab == 'query'} @click=${() => self.set_tab('query')}></input>
			</div>
		`;
		let contents =
				!this.loaded ?
					this.loading ?
						html`<div>Loading...</div>` :
						html`<div>Select or create an identity.</div>` :
					this.render_tab();
		return html`
			${this.render_id_picker()}
			${tabs}
			${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
			${contents}
		`;
	}
}

customElements.define('tf-app', TfElement);