let smarten_feeds_debounce_timeout;
function smarten_feeds() {
	if (smarten_feeds_debounce_timeout) {
		clearTimeout(smarten_feeds_debounce_timeout);
	}

	smarten_feeds_debounce_timeout = setTimeout(() => {
		smarten_feeds_debounce_timeout = undefined;

		const feeds = document?.body?.querySelectorAll("[data-feed]:not([data-smartened])") ?? [];
		for (const feed of feeds) {
			if (!feed.dataset.source) {
				console.warn("No source url for smart feed: " + feed);
				continue;
			}

			feed.__started = false;
			feed.__newest_id = undefined;
			feed.__oldest_id = undefined;

			feed.__templates = feed
				.querySelectorAll("template[data-for_type]")
				.values()
				.reduce((_templates, template) => {
					_templates[template.dataset.for_type] = template;
					return _templates;
				}, {});

			feed.__start = () => {
				feed.__started = true;
				feed.__update();
			};

			feed.__stop = async () => {
				feed.__started = false;

				if (feed.__request_abort_controller) {
					await feed.__request_abort_controller.abort("smartfeed:stopped");
					delete feed.__request_abort_controller;
				}

				if (feed.__refresh_interval) {
					clearInterval(feed.__refresh_interval);
					delete feed.__refresh_interval;
				}
			};

			feed.__clear = () => {
				const children_to_remove = Array.from(feed.childNodes).filter(
					(node) => !["script", "template"].includes(node.tagName),
				);
				children_to_remove.forEach((element) => feed.removeChild(element));

				feed.__newest_id = undefined;
				feed.__oldest_id = undefined;
			};

			feed.__reset = async () => {
				const was_started = feed.__started;

				await feed.__stop();
				feed.__clear();

				if (was_started) {
					setTimeout(feed.__start, 1);
				}
			};

			feed.__precheck = () => {
				return feed.dataset.precheck ? eval(feed.dataset.precheck) : true;
			};

			feed.__target = (item) => {
				if (!feed.__target_element) {
					return feed;
				}

				return feed.__target_element(item);
			};

			feed.__autoscroll_debounce_timeout = undefined;
			feed.__render = async (item) => {
				const [item_type, item_id] = item.id?.split(":", 2) ?? [];

				feed.__newest_id =
					typeof item_id === "string" && item_id > (feed.__newest_id ?? "")
						? item_id
						: feed.__newest_id;
				feed.__oldest_id =
					typeof item_id === "string" &&
					item_id <
						(feed.__oldest_id ??
							"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")
						? item_id
						: feed.__oldest_id;

				const template = feed.__templates[item.type];
				if (!template) {
					return;
				}

				feed.__context =
					feed.__context ??
					(feed.dataset.context ? new Function(feed.dataset.context) : undefined);
				const context = feed.__context ? await feed.__context(item, feed) : {};

				const rendered_html = eval("`" + template.innerHTML.trim() + "`");

				const existing_element =
					feed.querySelector("#" + item.id?.replace(/([:\.])/g, "\\$1")) ??
					feed.querySelector("#" + item.temp_id?.replace(/([:\.])/g, "\\$1")) ??
					feed.querySelector("#" + item.meta?.temp_id?.replace(/([:\.])/g, "\\$1")) ??
					feed.querySelector(
						`[data-temp_id='${item.temp_id ?? item.meta?.temp_id ?? ""}']`,
					);

				if (existing_element) {
					if (
						existing_element.id !== item.id ||
						rendered_html !== existing_element.outerHTML
					) {
						existing_element.outerHTML = rendered_html;
					}
				} else {
					const target = feed.__target(item);
					if (!target) {
						return;
					}

					switch (feed.dataset.insert ?? "append") {
						case "prepend":
							target.insertAdjacentHTML("afterbegin", rendered_html);
							break;
						case "append":
							target.insertAdjacentHTML("beforeend", rendered_html);
							break;
						default:
							throw new Error(
								'data-insert must be "append" or "prepend" for smart feeds',
							);
							break;
					}

					if (target === feed && feed.dataset.autoscroll) {
						if (feed.__autoscroll_debounce_timeout) {
							clearTimeout(feed.__autoscroll_debounce_timeout);
						}

						feed.__autoscroll_debounce_timeout = setTimeout(() => {
							feed.scrollTo({
								behavior: "auto",
								left: 0,
								top:
									(feed.dataset.insert ?? "append") === "append"
										? feed.scrollHeight
										: 0,
							});
							feed.__autoscroll_debounce_timeout = undefined;
						}, 15);
					}
				}
			};

			// TODO: if the feed is scrolled to one extreme, load more elements?
			feed.__update = function () {
				if (!feed.__started) {
					return;
				}

				feed.__attempts_since_last_successful_update =
					1 + (feed.__attempts_since_last_successful_update ?? 0);

				if (!feed.__precheck()) {
					return;
				}

				feed.__request_abort_controller =
					feed.__request_abort_controller || new AbortController();

				const url = eval("`" + feed.dataset.source + "`");

				api.fetch(url, {
					signal: feed.__request_abort_controller.signal,
				})
					.then(async (new_items_response) => {
						if (!new_items_response.ok) {
							const body = await new_items_response.json();
							throw new Error(body.message ?? body.error?.message ?? "Uknown error.");
						}

						const new_items = await new_items_response.json();

						const sorted_items = feed.dataset.reverse ? new_items.reverse() : new_items;
						for await (const item of sorted_items) {
							await feed.__render(item);
						}

						if (/^\d+$/.test(feed.dataset.maxlength ?? "")) {
							const maxlength = parseInt(feed.dataset.maxlength, 10);
							switch (feed.dataset.insert ?? "append") {
								case "prepend":
									/* remove children at the end */
									target.children.forEach((element, index) =>
										index > maxlength ? element.remove() : null,
									);
									break;
								case "append":
									/* remove children at the beginning */
									const cutoff = target.children.length - maxlength;
									target.children.forEach((element, index) =>
										index < cutoff ? element.remove() : null,
									);
									break;
								default:
									throw new Error(
										'data-insert must be "append" or "prepend" for smart feeds',
									);
									break;
							}
						}

						feed.dataset.hydrated = true;
						feed.dataset.last_update = new Date().toISOString();
						feed.__attempts_since_last_successful_update = 0;
					})
					.catch((error) => {
						if (error === "smartfeed:stopped") {
							return;
						}

						if ( error.name === 'TypeError' && error.message === 'NetworkError when attempting to fetch resource.' ) {
							console.log( error.message );
							return;
						}

						feed.dataset.error = JSON.stringify(error);
					})
					.finally(() => {
						if (feed.__started && feed.dataset.longpolling) {
							setTimeout(
								feed.__update,
								/* logarithmic backoff */
								Math.log(feed.__attempts_since_last_successful_update || 1) *
									10 /* scale it a bit */ *
									1_000 /* 1s in ms */,
							);
						}
					});
			};

			if (feed.dataset.refresh) {
				const refresh_frequency = parseInt(feed.dataset.refresh, 10);
				feed.__refresh_interval = setInterval(() => {
					feed.__update();
				}, refresh_frequency);
			}

			feed.dataset.smartened = true;

			feed.__start();
		}
	}, 10);
}

const smarten_feeds_observer = new MutationObserver(smarten_feeds);
smarten_feeds_observer.observe(document, {
	childList: true,
	subtree: true,
});
