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},
			tab: {type: String},
			broadcasts: {type: Array},
			connections: {type: Array},
			loading: {type: Boolean},
			loading_about: {type: Number},
			loaded: {type: Boolean},
			following: {type: Array},
			users: {type: Object},
			ids: {type: Array},
			channels: {type: Array},
			channels_unread: {type: Object},
			channels_latest: {type: Object},
			guest: {type: Boolean},
			url: {type: String},
			private_closed: {type: Object},
			private_messages: {type: Array},
			grouped_private_messages: {type: Object},
			recent_reactions: {type: Array},
			is_administrator: {type: Boolean},
			stay_connected: {type: Boolean},
			progress: {type: Number},
		};
	}

	static styles = styles;

	constructor() {
		super();
		let self = this;
		this.hash = '#';
		this.tab = 'news';
		this.broadcasts = [];
		this.connections = [];
		this.following = [];
		this.users = {};
		this.loaded = false;
		this.loading_about = 0;
		this.channels = [];
		this.channels_unread = {};
		this.channels_latest = {};
		this.loading_latest = 0;
		this.loading_latest_scheduled = 0;
		this.recent_reactions = [];
		this.private_closed = {};
		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);
			self.reset_progress();
		});
		tfrpc.register(async function notifyNewMessage(id) {
			await self.fetch_new_message(id);
		});
		tfrpc.register(async function notifyNewBlob(id) {
			window.dispatchEvent(
				new CustomEvent('blob-stored', {
					bubbles: true,
					composed: true,
					detail: {
						id: id,
					},
				})
			);
		});
		tfrpc.register(function set(name, value) {
			if (name === 'broadcasts') {
				self.broadcasts = value;
			} else if (name === 'connections') {
				self.connections = value;
			} else if (name === 'identity') {
				self.whoami = value;
			}
		});
		this.initial_load();
	}

	async initial_load() {
		let whoami = await tfrpc.rpc.getActiveIdentity();
		let ids = (await tfrpc.rpc.getIdentities()) || [];
		this.is_administrator = await tfrpc.rpc.isAdministrator();
		this.stay_connected =
			this.is_administrator &&
			(await tfrpc.rpc.globalSettingsGet('stay_connected'));
		this.url = await tfrpc.rpc.url();
		this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
		this.guest = !this.whoami?.length;
		this.ids = ids;
		let private_closed =
			(await tfrpc.rpc.databaseGet('private_closed')) ?? '{}';
		this.private_closed = JSON.parse(private_closed);
		await this.load_channels();
	}

	async close_private_chat(event) {
		let update = {};
		update[event.detail.key] = true;
		this.private_closed = Object.assign(update, this.private_closed);
		await tfrpc.rpc.databaseSet(
			'private_closed',
			JSON.stringify(this.private_closed)
		);
	}

	async load_channels() {
		let channels = await tfrpc.rpc.query(
			`
			SELECT
				content ->> 'channel' AS channel,
				content ->> 'subscribed' AS subscribed
			FROM
				messages
			WHERE
				author = ? AND
				content ->> 'type' = 'channel'
			ORDER BY sequence
		`,
			[this.whoami]
		);
		let channel_map = {};
		for (let row of channels) {
			if (row.subscribed) {
				channel_map[row.channel] = true;
			} else {
				delete channel_map[row.channel];
			}
		}
		this.channels = Object.keys(channel_map).sort();
	}

	connectedCallback() {
		super.connectedCallback();
		this._keydown = this.keydown.bind(this);
		window.addEventListener('keydown', this._keydown);
	}

	disconnectedCallback() {
		super.disconnectedCallback();
		window.removeEventListener('keydown', this._keydown);
	}

	keydown(event) {
		if (event.altKey && event.key == 'ArrowUp') {
			this.next_channel(-1);
			event.preventDefault();
		} else if (event.altKey && event.key == 'ArrowDown') {
			this.next_channel(1);
			event.preventDefault();
		}
	}

	visible_private() {
		if (!this.grouped_private_messages || !this.private_closed) {
			return [];
		}
		let self = this;
		return Object.fromEntries(
			Object.entries(this.grouped_private_messages).filter(([key, value]) => {
				let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
				let grouped_latest = Math.max(...value.map((x) => x.rowid));
				return (
					!self.private_closed[key] ||
					self.channels_unread[channel] === undefined ||
					grouped_latest > self.channels_unread[channel]
				);
			})
		);
	}

	next_channel(delta) {
		let channel_names = [
			'',
			'@',
			'👍',
			...Object.keys(this.visible_private())
				.sort()
				.map((x) => '🔐' + JSON.parse(x).join(',')),
			...this.channels.map((x) => '#' + x),
		];
		let index = channel_names.indexOf(this.hash.substring(1));
		index = index != -1 ? index + delta : 0;
		tfrpc.rpc.setHash(
			'#' +
				encodeURIComponent(
					channel_names[(index + channel_names.length) % channel_names.length]
				)
		);
	}

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

	async fetch_about(following, users) {
		this.loading_about++;
		let ids = Object.keys(following).sort();
		const k_cache_version = 3;
		let cache = await tfrpc.rpc.databaseGet('about');
		let original_cache = cache;
		cache = cache ? JSON.parse(cache) : {};
		if (cache.version !== k_cache_version) {
			cache = {
				version: k_cache_version,
				about: {},
			};
		}

		let ids_out_of_date = ids.filter(
			(x) =>
				(users[x]?.seq && !cache.about[x]?.seq) ||
				(users[x]?.seq && users[x]?.seq > cache.about[x].seq)
		);

		for (let id of Object.keys(cache.about)) {
			if (ids.indexOf(id) == -1) {
				delete cache.about[id];
			} else {
				users[id] = Object.assign(cache.about[id], users[id] || {});
			}
		}

		console.log(
			'loading about for',
			ids.length,
			'accounts',
			ids_out_of_date.length,
			'out of date'
		);
		if (ids_out_of_date.length) {
			try {
				let rows = await tfrpc.rpc.query(
					`
						SELECT all_abouts.author, json(json_group_object(all_abouts.key, all_abouts.value)) AS about
						FROM (
							SELECT
								messages.author,
								fields.key,
								RANK() OVER (PARTITION BY messages.author, fields.key ORDER BY messages.sequence DESC) AS rank,
								fields.value
							FROM messages JOIN json_each(messages.content) AS fields
							WHERE
								messages.content ->> '$.type' = 'about' AND
								messages.content ->> '$.about' = messages.author AND
								NOT fields.key IN ('about', 'type')) all_abouts
						JOIN json_each(?) AS following ON all_abouts.author = following.value
						WHERE rank = 1
						GROUP BY all_abouts.author
					`,
					[JSON.stringify(ids_out_of_date)]
				);
				users = users || {};
				for (let row of rows) {
					users[row.author] = Object.assign(
						users[row.author] || {},
						JSON.parse(row.about)
					);
					cache.about[row.author] = Object.assign(
						{seq: users[row.author].seq},
						JSON.parse(row.about)
					);
				}
			} catch (e) {
				console.log(e);
			}
		}

		for (let id of ids_out_of_date) {
			if (!cache.about[id]?.seq) {
				cache.about[id] = Object.assign(cache.about[id] ?? {}, {
					seq: users[id]?.seq ?? 0,
				});
			}
		}

		this.loading_about--;

		let new_cache = JSON.stringify(cache);
		if (new_cache != original_cache) {
			let start_time = new Date();
			tfrpc.rpc.databaseSet('about', new_cache).then(function () {
				console.log('saving about took', (new Date() - start_time) / 1000);
			});
		}

		return Object.assign({}, users);
	}

	async fetch_new_message(id) {
		let messages = await tfrpc.rpc.query(
			`
				SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
				FROM messages
				JOIN json_each(?) AS following ON messages.author = following.value
				WHERE messages.id = ?
			`,
			[JSON.stringify(this.following), id]
		);
		for (let message of messages) {
			if (
				message.author == this.whoami &&
				JSON.parse(message.content)?.type == 'channel'
			) {
				this.load_channels();
			}
		}
		this.schedule_load_latest();
	}

	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 get_latest_private(following) {
		const k_version = 1;
		// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
		let cache = JSON.parse(
			(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
		);
		if (cache.version !== k_version) {
			cache = {
				version: k_version,
				messages: [],
				range: [],
			};
		}
		let latest = (
			await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
		)[0].latest;
		let ranges = [];
		const k_chunk_size = 512;
		if (cache.range.length) {
			for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
				ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
			}
			for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
				ranges.push([Math.max(i - k_chunk_size, 0), i, false]);
			}
		} else {
			for (let i = 0; i < latest; i += k_chunk_size) {
				ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
			}
		}
		for (let range of ranges) {
			let messages = await tfrpc.rpc.query(
				`
					SELECT messages.rowid, messages.id, json(content) AS content
						FROM messages
						WHERE
							messages.rowid > ?1 AND
							messages.rowid <= ?2 AND
							json(messages.content) LIKE '"%'
						ORDER BY messages.rowid DESC
					`,
				[range[0], range[1]]
			);
			messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
			if (messages.length) {
				cache.latest = Math.max(
					cache.latest ?? 0,
					...messages.map((x) => x.rowid)
				);
				if (range[2]) {
					cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
				} else {
					cache.messages = [...messages.map((x) => x.id), ...cache.messages];
				}
			}
			cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
			cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
			await tfrpc.rpc.databaseSet(
				`private:${this.whoami}`,
				JSON.stringify(cache)
			);
		}
		return [cache.latest, cache.messages];
	}

	async query_timed(sql, args) {
		let start = new Date();
		let result = await tfrpc.rpc.query(sql, args);
		let end = new Date();
		console.log((end - start) / 1000, sql.replaceAll(/\s+/g, ' ').trim());
		return result;
	}

	async group_private_messages(messages) {
		let groups = {};
		let result = await this.decrypt(
			await tfrpc.rpc.query(
				`
				SELECT messages.rowid, messages.id, author, timestamp, json(content) AS content
				FROM messages
				JOIN json_each(?) AS ids
				WHERE messages.id = ids.value
				ORDER BY timestamp DESC
			`,
				[JSON.stringify(messages)]
			)
		);
		for (let message of result) {
			let key = JSON.stringify(
				[
					...new Set(
						message?.decrypted?.recps?.filter((x) => x != this.whoami)
					),
				].sort() ?? []
			);
			if (!groups[key]) {
				groups[key] = [];
			}
			groups[key].push(message);
		}
		return groups;
	}

	async load_channels_latest(following) {
		let start_time = new Date();
		let latest_private = this.get_latest_private(following);
		const k_args = [
			JSON.stringify(this.channels),
			JSON.stringify(following),
			'"' + this.whoami.replace('"', '""') + '"',
			this.whoami,
		];
		let channels = (
			await Promise.all([
				this.query_timed(
					`
					SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
					JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
					JOIN json_each(?2) AS following ON messages.author = following.value
					WHERE
						messages.content ->> 'type' = 'post' AND
						messages.content ->> 'root' IS NULL AND
						messages.author != ?4
					GROUP by channel
				`,
					k_args
				),
				this.query_timed(
					`
					SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
					JOIN messages_refs ON messages.id = messages_refs.message
					JOIN json_each(?1) AS channels ON messages_refs.ref = '#' || channels.value
					JOIN json_each(?2) AS following ON messages.author = following.value
					WHERE
						messages.content ->> 'type' = 'post' AND
						messages.content ->> 'root' IS NULL AND
						messages.author != ?4
					GROUP by channel
				`,
					k_args
				),
				this.query_timed(
					`
					SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
					JOIN json_each(?2) AS following ON messages.author = following.value
					WHERE
						messages.content ->> 'type' = 'post' AND
						messages.content ->> 'root' IS NULL AND
						messages.author != ?4
				`,
					k_args
				),
				this.query_timed(
					`
					SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
					JOIN messages ON messages.rowid = messages_fts.rowid
					JOIN json_each(?2) AS following ON messages.author = following.value
					WHERE messages.author != ?4
				`,
					k_args
				),
			])
		).flat();
		let latest = {};
		for (let row of channels) {
			if (!latest[row.channel]) {
				latest[row.channel] = row.rowid;
			} else {
				latest[row.channel] = Math.max(row.rowid, latest[row.channel]);
			}
		}
		this.channels_latest = latest;
		console.log('channels took', (new Date() - start_time) / 1000.0);
		let self = this;
		start_time = new Date();
		latest_private.then(async function (latest) {
			self.channels_latest = Object.assign({}, self.channels_latest, {
				'🔐': latest[0],
			});
			console.log('private took', (new Date() - start_time) / 1000.0);
			self.private_messages = latest[1];
			self.grouped_private_messages = await self.group_private_messages(
				latest[1]
			);
		});
	}

	_schedule_load_latest_timer() {
		--this.loading_latest_scheduled;
		this.schedule_load_latest();
	}

	reset_progress() {
		if (this.progress === undefined) {
			this._progress_start = new Date();
			requestAnimationFrame(this.update_progress.bind(this));
		}
	}

	update_progress() {
		if (
			!this.loading_latest &&
			!this.loading_latest_scheduled &&
			!this.shadowRoot.getElementById('tf-tab-news')?.is_loading()
		) {
			this.progress = undefined;
			return;
		}
		this.progress = (new Date() - this._progress_start).valueOf();
		requestAnimationFrame(this.update_progress.bind(this));
	}

	schedule_load_latest() {
		this.reset_progress();
		if (!this.loading_latest) {
			this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
			this.load();
		} else if (!this.loading_latest_scheduled) {
			this.loading_latest_scheduled++;
			setTimeout(this._schedule_load_latest_timer.bind(this), 5000);
		}
	}

	async fetch_user_info(users) {
		let info = await tfrpc.rpc.query(
			`
				SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
				JOIN json_each(?) AS following
				ON messages_stats.author = following.value
			`,
			[JSON.stringify(Object.keys(users))]
		);
		for (let row of info) {
			users[row.author] = Object.assign(users[row.author], {
				seq: row.max_sequence,
				ts: row.max_ts,
			});
		}
		return users;
	}

	async load_recent_reactions() {
		this.recent_reactions = (
			await tfrpc.rpc.query(
				`
			SELECT DISTINCT content ->> '$.vote.expression' AS value
			FROM messages
			WHERE author = ? AND
			content ->> '$.type' = 'vote'
			ORDER BY timestamp DESC LIMIT 10
		`,
				[this.whoami]
			)
		).map((x) => x.value);
	}

	async load() {
		this.loading_latest = true;
		this.reset_progress();
		try {
			let start_time = new Date();
			let whoami = this.whoami;
			let following = await tfrpc.rpc.following([whoami], 2);
			let old_users = this.users ?? {};
			let users = {};
			let by_count = [];
			for (let [id, v] of Object.entries(following)) {
				users[id] = Object.assign(
					{
						following: v.of,
						blocking: v.ob,
						followed: v.if,
						blocked: v.ib,
						follow_depth: following[id]?.d,
					},
					old_users[id]
				);
				by_count.push({count: v.of, id: id});
			}
			let reactions = this.load_recent_reactions();
			this.load_channels_latest(Object.keys(following));
			this.channels_unread = JSON.parse(
				(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
			);
			this.following = Object.keys(following);
			let about_start_time = new Date();
			start_time = new Date();
			users = await this.fetch_user_info(users);
			console.log(
				'user info took',
				(new Date() - start_time) / 1000.0,
				'seconds'
			);
			this.users = users;

			let self = this;
			this.fetch_about(following, users).then(function (result) {
				self.users = result;
				console.log(
					'about took',
					(new Date() - about_start_time) / 1000.0,
					'seconds for',
					Object.keys(users).length,
					'users'
				);
			});
			console.log(
				`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
			);
			await reactions;
			this.whoami = whoami;
			this.loaded = whoami;
		} finally {
			this.loading_latest = false;
		}
	}

	channel_set_unread(event) {
		this.channels_unread[event.detail.channel ?? ''] = event.detail.unread;
		this.channels_unread = Object.assign({}, this.channels_unread);
		tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread));
	}

	async decrypt(messages) {
		let whoami = this.whoami;
		return Promise.all(
			messages.map(async function (message) {
				let content;
				try {
					content = JSON.parse(message?.content);
				} catch {}
				if (typeof content === 'string') {
					let decrypted;
					try {
						decrypted = await tfrpc.rpc.try_decrypt(whoami, content);
					} catch {}
					if (decrypted) {
						try {
							message.decrypted = JSON.parse(decrypted);
						} catch {
							message.decrypted = decrypted;
						}
					}
				}
				return message;
			})
		);
	}

	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}
					?loading=${this.loading || this.loading_about != 0}
					.channels=${this.channels}
					.channels_latest=${this.channels_latest}
					.channels_unread=${this.channels_unread}
					@channelsetunread=${this.channel_set_unread}
					@refresh=${this.refresh}
					@toggle_stay_connected=${this.toggle_stay_connected}
					@loadmessages=${this.reset_progress}
					@closeprivatechat=${this.close_private_chat}
					.connections=${this.connections}
					.private_messages=${this.private_messages}
					.grouped_private_messages=${this.visible_private()}
					.recent_reactions=${this.recent_reactions}
					?is_administrator=${this.is_administrator}
					?stay_connected=${this.stay_connected}
				></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 === '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=')
						? this.hash.substring(5)
						: null}
				></tf-tab-query>
			`;
		}
	}

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

	refresh() {
		tfrpc.rpc.sync();
	}

	async toggle_stay_connected() {
		let stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
		let new_stay_connected = !this.stay_connected;
		try {
			if (new_stay_connected != stay_connected) {
				await tfrpc.rpc.globalSettingsSet('stay_connected', new_stay_connected);
			}
		} finally {
			this.stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
		}
	}

	render() {
		let self = this;

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

		const k_tabs = {
			'📰': 'news',
			'📡': 'connections',
			'🔍': 'search',
			'👩‍💻': 'query',
		};

		let tabs = html`
			<div
				class="w3-bar w3-theme-l1"
				style="position: static; top: 0; z-index: 10"
			>
				${this.is_administrator
					? html`
							<button
								class=${'w3-bar-item w3-button w3-circle w3-ripple' +
								(this.connections?.some((x) => x.flags.one_shot)
									? ' w3-spin'
									: '')}
								@click=${this.refresh}
							>
								↻
							</button>
							<button
								class="w3-bar-item w3-button w3-ripple"
								@click=${this.toggle_stay_connected}
							>
								${this.stay_connected ? '🔗' : '⛓️‍💥'}
							</button>
						`
					: undefined}
				${Object.entries(k_tabs).map(
					([k, v]) => html`
						<button
							title=${v}
							class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
								? 'w3-theme-l2'
								: 'w3-theme-l1'}"
							@click=${() => self.set_tab(v)}
						>
							${k}
							<span class=${self.tab == v ? '' : 'w3-hide-small'}
								>${v.charAt(0).toUpperCase() + v.substring(1)}</span
							>
						</button>
					`
				)}
			</div>
		`;
		let contents = this.guest
			? html`<div
					class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container"
				>
					<p>⚠️🦀 Must be logged in to Tilde Friends to scuttle here. 🦀⚠️</p>
					<footer class="w3-center">
						<a
							class="w3-button w3-theme-d1"
							href=${`/login?return=${encodeURIComponent(this.url)}`}
							>Login</a
						>
					</footer>
				</div>`
			: !this.loaded || this.loading
				? html`<div
						class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
					>
						<span class="w3-spin" style="display: inline-block">🦀</span>
						Loading...
					</div>`
				: this.render_tab();
		let progress =
			this.progress !== undefined
				? html`
						<div style="position: absolute; width: 100%" id="progress">
							<div
								class="w3-theme-l3"
								style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`}
							></div>
						</div>
					`
				: undefined;
		return html`
			<div
				style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
				class="w3-theme-dark"
			>
				${progress}
				<div style="flex: 0 0">${tabs}</div>
				<div style="flex: 1 1; overflow: auto; contain: layout">
					${contents}
				</div>
			</div>
		`;
	}
}

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