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

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

function isSafeNavUrl(url) {
	const value = String(url || "").trim();
	if (!value || value.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(value)) {
		return false;
	}
	return true;
}

function articleDir(articlePath) {
	const clean = String(articlePath || "").replace(/^\/+/, "");
	const slash = clean.lastIndexOf("/");
	return slash === -1 ? "" : clean.slice(0, slash + 1);
}

function resolveRelative(articlePath, href) {
	const base = new URL(articleDir(articlePath), "https://blog.invalid/");
	let resolved;
	try {
		resolved = new URL(href, base);
	} catch (_ex) {
		return null;
	}
	if (resolved.origin !== "https://blog.invalid") {
		return null;
	}
	let path = resolved.pathname.replace(/^\/+/, "");
	path += resolved.search;
	path += resolved.hash;
	return path;
}

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, articlePath) {
	for (const img of root.querySelectorAll("img[src]")) {
		const src = img.getAttribute("src");
		if (!src || !isRelativeResource(src)) {
			continue;
		}
		const resolved = resolveRelative(articlePath, src);
		if (resolved) {
			img.setAttribute("src", contentUrl(contentBase, resolved));
		}
	}
	for (const anchor of root.querySelectorAll("a[href]")) {
		const href = anchor.getAttribute("href");
		if (!href || !isRelativeResource(href)) {
			continue;
		}
		const resolved = resolveRelative(articlePath, href);
		if (!resolved) {
			continue;
		}
		const hashIndex = resolved.indexOf("#");
		const beforeHash = hashIndex === -1 ? resolved : resolved.slice(0, hashIndex);
		const hash = hashIndex === -1 ? "" : resolved.slice(hashIndex);
		const pathOnly = beforeHash.split("?")[0];
		if (/\.(md|markdown)$/i.test(pathOnly)) {
			anchor.setAttribute("href", "?article=" + encodeURIComponent(pathOnly) + hash);
		} else {
			anchor.setAttribute("href", contentUrl(contentBase, resolved));
		}
	}
}

function addLineNumbers(root) {
	for (const block of root.querySelectorAll("pre > code:not(.graphviz)")) {
		const lines = block.textContent.split("\n");
		if (lines.length && lines[lines.length - 1] === "") {
			lines.pop();
		}
		if (lines.length <= 1) {
			continue;
		}
		const pre = block.parentElement;
		if (!pre) {
			continue;
		}
		const wrap = document.createElement("div");
		wrap.className = "blog-code";
		const langClass = block.className;
		for (let i = 0; i < lines.length; i++) {
			const row = document.createElement("div");
			row.className = "blog-code-line";
			const num = document.createElement("span");
			num.className = "blog-code-lineno";
			num.setAttribute("aria-hidden", "true");
			num.textContent = String(i + 1);
			const code = document.createElement("code");
			if (langClass) {
				code.className = langClass;
			}
			code.textContent = lines[i];
			row.append(num, code);
			wrap.append(row);
		}
		pre.replaceWith(wrap);
	}
}

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.replaceChildren();
		const failure = document.createElement("p");
		failure.className = "text-danger";
		failure.textContent = "Failed to load article.";
		articleEl.append(failure);
		return;
	}
	const md = await resp.text();
	articleEl.innerHTML = converter.makeHtml(md);
	rewriteRelativeResources(articleEl, contentBase, path);
	addLineNumbers(articleEl);
	if (window.location.hash !== "") {
		try {
			const target = articleEl.querySelector(window.location.hash);
			if (target) {
				target.scrollIntoView();
			}
		} catch (_ex) {
		}
	}
	runWhenReady(() => {
		if (typeof MathJax !== "undefined" && MathJax.typeset) {
			MathJax.typeset();
		}
	}, "MathJax-script");
	runWhenReady(() => {
		if (typeof hljs === "undefined" || !hljs.highlightElement) {
			return;
		}
		for (const block of articleEl.querySelectorAll(".blog-code-line > code, pre > code:not(.graphviz)")) {
			hljs.highlightElement(block);
		}
	}, "Highlight-script");
	runWhenReady(() => renderGraphs(articleEl), "GraphViz-script");
}

function appendTextBlock(parent, className, text) {
	const el = document.createElement("div");
	el.className = className;
	el.textContent = text;
	parent.append(el);
}

function appendArticleLink(parent, item, dateStr) {
	const p = document.createElement("p");
	p.append(document.createTextNode(dateStr + ": "));
	const anchor = document.createElement("a");
	const url = String(item.url || "");
	anchor.href = isSafeNavUrl(url) ? url : "#";
	anchor.textContent = String(item.name || "");
	p.append(anchor);
	parent.append(p);
}

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

	let opened = false;
	const dateOpts = { weekday: "long", year: "numeric", month: "long", day: "numeric" };
	const years = Object.keys(nestedByDate).sort(sortNum).reverse();
	for (const y of years) {
		const yearDetails = document.createElement("details");
		if (!opened) {
			yearDetails.open = true;
		}
		const yearSummary = document.createElement("summary");
		yearSummary.textContent = y;
		yearDetails.append(yearSummary);
		const yearQuote = document.createElement("blockquote");
		yearQuote.append(yearDetails);
		const months = Object.keys(nestedByDate[y]).sort(sortNum).reverse();
		for (const m of months) {
			const monthDetails = document.createElement("details");
			if (!opened) {
				monthDetails.open = true;
				opened = true;
			}
			const monthSummary = document.createElement("summary");
			monthSummary.textContent = new Date(2000, Number(m), 1).toLocaleString("default", { month: "long" });
			monthDetails.append(monthSummary);
			const days = Object.keys(nestedByDate[y][m]).sort(sortNum).reverse();
			for (const d of days) {
				for (const item of nestedByDate[y][m][d]) {
					const dateStr = new Date(item.created).toLocaleString("default", dateOpts);
					appendArticleLink(monthDetails, item, dateStr);
				}
			}
			const monthQuote = document.createElement("blockquote");
			monthQuote.append(monthDetails);
			yearDetails.append(monthQuote);
		}
		articleEl.append(yearQuote);
	}
	if (undated.length) {
		const details = document.createElement("details");
		const summary = document.createElement("summary");
		summary.textContent = "Undated";
		details.append(summary);
		for (const item of undated) {
			appendArticleLink(details, item, "Undated");
		}
		const quote = document.createElement("blockquote");
		quote.append(details);
		articleEl.append(quote);
	}
}

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

function searchChange(searchEl, resultsEl, searchManifest) {
	resultsEl.replaceChildren();
	const q = searchEl.value.trim().toLowerCase();
	if (!q) {
		hideSearchResults(resultsEl);
		return;
	}
	for (const item of searchManifest) {
		const name = String(item.name || "");
		const tags = String(item.tags || "");
		if (!name.toLowerCase().includes(q) && !tags.toLowerCase().includes(q)) {
			continue;
		}
		const row = document.createElement("div");
		row.className = "blog-search-result";
		row.setAttribute("role", "option");
		appendTextBlock(row, "blog-search-result-title", name);
		appendTextBlock(row, "blog-search-result-snippet", tags);
		appendTextBlock(row, "blog-search-result-date", String(item.created || ""));
		const url = String(item.url || "");
		if (isSafeNavUrl(url)) {
			row.addEventListener("click", () => {
				window.location.href = url;
			});
		}
		resultsEl.appendChild(row);
	}
	if (resultsEl.childElementCount) {
		resultsEl.classList.add("show");
	} else {
		hideSearchResults(resultsEl);
	}
}

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();