function contentUrl(contentBase, path) {
	const clean = String(path || "").replace(/^\/+/, "");
	return contentBase.replace(/\/?$/, "/") + clean;
}

function sortNum(a, b) {
	return a - b;
}

function renderGraphs(root) {
	if (typeof Viz !== "function") {
		return;
	}
	for (const c of root.querySelectorAll("code.graphviz")) {
		const p = document.createElement("p");
		p.innerHTML = Viz(c.innerText, { format: "svg" });
		p.className = "graph";
		const pre = c.parentElement;
		if (pre) {
			pre.replaceWith(p);
		}
	}
}

function isRelativeResource(href) {
	if (!href || href.charAt(0) === "#") {
		return false;
	}
	if (href.charAt(0) === "/") {
		return false;
	}
	if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {
		return false;
	}
	if (href.indexOf("//") === 0) {
		return false;
	}
	if (href.charAt(0) === "?") {
		return false;
	}
	return true;
}

function rewriteRelativeResources(root, contentBase) {
	for (const img of root.querySelectorAll("img[src]")) {
		const src = img.getAttribute("src");
		if (src && isRelativeResource(src)) {
			img.setAttribute("src", contentUrl(contentBase, src.replace(/^\.\//, "")));
		}
	}
}

function addLineNumbers(root) {
	for (const block of root.querySelectorAll("code:not(.graphviz)")) {
		const len = block.textContent.split("\n").length;
		if (len <= 1) {
			continue;
		}
		const lineref = [...Array(len).keys()].slice(1).join("\n");
		const lineno = document.createElement("code");
		lineno.className = "lineno";
		lineno.textContent = lineref;
		if (block.parentElement) {
			block.parentElement.prepend(lineno);
		}
	}
}

function runWhenReady(tryFn, scriptId) {
	try {
		tryFn();
	} catch (_ex) {
		const script = document.querySelector("#" + scriptId);
		if (script) {
			script.addEventListener("load", tryFn);
		}
	}
}

async function loadmd(articleEl, contentBase, path) {
	const converter = new showdown.Converter();
	const resp = await fetch(contentUrl(contentBase, path));
	if (!resp.ok) {
		articleEl.innerHTML = "<p class=\"text-danger\">Failed to load article.</p>";
		return;
	}
	const md = await resp.text();
	articleEl.innerHTML = converter.makeHtml(md);
	rewriteRelativeResources(articleEl, contentBase);
	addLineNumbers(articleEl);
	if (window.location.hash !== "") {
		const target = articleEl.querySelector(window.location.hash);
		if (target) {
			target.scrollIntoView();
		}
	}
	runWhenReady(() => {
		if (typeof MathJax !== "undefined" && MathJax.typeset) {
			MathJax.typeset();
		}
	}, "MathJax-script");
	runWhenReady(() => {
		if (typeof hljs !== "undefined" && hljs.highlightAll) {
			hljs.highlightAll();
		}
	}, "Highlight-script");
	runWhenReady(() => renderGraphs(articleEl), "GraphViz-script");
}

function archive(articleEl, manifest) {
	const nestedByDate = {};
	for (const item of manifest) {
		const cdate = new Date(item.created);
		const y = cdate.getFullYear();
		const m = cdate.getMonth();
		const d = cdate.getDate();
		if (!nestedByDate[y]) {
			nestedByDate[y] = {};
		}
		if (!nestedByDate[y][m]) {
			nestedByDate[y][m] = {};
		}
		nestedByDate[y][m][d] = item;
	}

	let htmlStr = "";
	let suffix = " open";
	const dateOpts = { weekday: "long", year: "numeric", month: "long", day: "numeric" };
	Object.keys(nestedByDate)
		.sort(sortNum)
		.reverse()
		.forEach(function (y) {
			htmlStr += `<blockquote><details${suffix}><summary>${y}</summary>`;
			Object.keys(nestedByDate[y])
				.sort(sortNum)
				.reverse()
				.forEach(function (m) {
					const mName = new Date(2000, m, 1).toLocaleString("default", { month: "long" });
					htmlStr += `<blockquote><details${suffix}><summary>${mName}</summary>`;
					Object.keys(nestedByDate[y][m])
						.sort(sortNum)
						.reverse()
						.forEach(function (d) {
							const item = nestedByDate[y][m][d];
							const dateStr = new Date(item.created).toLocaleString("default", dateOpts);
							htmlStr += `<p>${dateStr}: <a href="${item.url}">${item.name}</a></p>`;
						});
					htmlStr += "</details></blockquote>";
					suffix = "";
				});
			htmlStr += "</details></blockquote>";
		});
	articleEl.insertAdjacentHTML("beforeend", htmlStr);
}

function hideSearchResults(resultsEl) {
	resultsEl.classList.remove("show");
}

function searchChange(searchEl, resultsEl, searchManifest) {
	const s = searchEl.value;
	resultsEl.innerHTML = "";
	const q = s.toLowerCase();
	for (const item of searchManifest) {
		const name = String(item.name || "");
		const tags = String(item.tags || "");
		if (name.toLowerCase().includes(q) || tags.toLowerCase().includes(q)) {
			const row = document.createElement("div");
			row.className = "blog-search-result";
			row.setAttribute("role", "option");
			row.innerHTML =
				`<div class="blog-search-result-title">${name}</div>` +
				`<div class="blog-search-result-snippet">${tags}</div>` +
				`<div class="blog-search-result-date">${item.created || ""}</div>`;
			row.addEventListener("click", () => {
				window.location.href = item.url;
			});
			resultsEl.appendChild(row);
		}
	}
	resultsEl.classList.add("show");
}

async function populateManifest(contentBase, path) {
	const resp = await fetch(contentUrl(contentBase, path));
	if (!resp.ok) {
		return [];
	}
	return await resp.json();
}

async function loadManifest(articleEl, contentBase, path) {
	const manifest = await populateManifest(contentBase, path);
	archive(articleEl, manifest);
	return manifest;
}

function bindSearch(root, searchManifest) {
	const searchEl = root.querySelector(".blog-search");
	const resultsEl = root.querySelector(".blog-search-results");
	const container = root.querySelector(".blog-search-container");
	if (!searchEl || !resultsEl || !container) {
		return;
	}
	searchEl.addEventListener("input", () => searchChange(searchEl, resultsEl, searchManifest));
	document.addEventListener("keydown", (event) => {
		if (event.key === "Escape") {
			hideSearchResults(resultsEl);
		}
	});
	document.addEventListener("click", (event) => {
		if (!container.contains(event.target) && !resultsEl.contains(event.target)) {
			hideSearchResults(resultsEl);
		}
	});
}

async function boot() {
	const root = document.getElementById("blog-root");
	const articleEl = document.getElementById("article");
	if (!root || !articleEl) {
		return;
	}
	const contentBase = root.dataset.contentBase || "";
	const manifestPath = root.dataset.manifest || "articles/manifest.json";
	const u = new URL(window.location.href);
	const article = u.searchParams.get("article");
	let searchManifest = [];
	if (article) {
		await loadmd(articleEl, contentBase, article);
		searchManifest = await populateManifest(contentBase, manifestPath);
	} else {
		searchManifest = await loadManifest(articleEl, contentBase, manifestPath);
	}
	bindSearch(root, searchManifest);
}

boot();