// vim: set ts=4 sw=4:
/*
 * @file htmlview.js  html view for node + item display
 *
 * Copyright (C) 2021-2026 Lars Windolf <lars.windolf@gmx.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

import { render, template } from './helpers/render.js';

window.debugflags = 0;

function prepare(baseURL, title) {
	if (title) {
		/* Set title for it to appear in e.g. desktop MPRIS playback controls */
		document.title = title;
	}
	if (baseURL && baseURL !== '(null)') {
		let base = document.createElement("base");
		base.setAttribute("href", baseURL);
		document.head.appendChild(base);
	}
}

// returns first occurence of a given metadata type
function metadata_get(obj, key) {
	let metadata = obj.metadata;

	if (!metadata)
		return null;

	let results = metadata.filter((e) => key in e);
	if (results.length == 0)
		return null;

	return results[0][key];
}

function templateFix(str, data) {
	// sadly libxslt translating the handlebar templates causes
	// attribute escaping and thereby destroying template expressions
	// in attributes, which we need to restore
	return template(str
		.replace(/"%7B%7B/g, "\"{{")
		.replace(/%7D%7D"/g, "}}\"")
		.replace(/{{&gt;/g, "{{>")
		.replace(/%5B/g, "[")
		.replace(/%5D/g, "]"),
		data);
}

function escapeHTML(str){
	return new Option(str).innerHTML;
}

function debug(text, obj) {
	if(window.debugflags > 0)
		if (obj)
			console.log(text, obj);
		else
			console.log(text);
}

function parse_opml(blogrollData) {
	try {
		const parser = new DOMParser();
		const xmlDoc = parser.parseFromString(blogrollData, "application/xml");
		const parseError = xmlDoc.querySelector("parsererror");
		if (!parseError) {
			const root = xmlDoc.documentElement;
			// Transform OPML to JSON recursively
			const parseOutline = (outline) => {
				const result = {
					title: outline.getAttribute('title') || outline.getAttribute('text') || outline.getAttribute('description'),
					type: outline.getAttribute('type'),
					xmlUrl: outline.getAttribute('xmlUrl'),
					htmlUrl: outline.getAttribute('htmlUrl'),
					categories: outline.getAttribute('category') ? outline.getAttribute('category').split(',').map(s => s.trim()) : []

				};
				const children = Array.from(outline.children).filter(el => el.tagName === 'outline');
				if (children.length > 0) {
					result.children = children.map(parseOutline);
				}
				return result;
			};
			const body = root.querySelector('body') || root;
			const blogrollParsed = {
				title: root.querySelector('head > title')?.textContent,
				lastUpdated: root.querySelector('head > dateModified')?.textContent,
				children: Array.from(body.children).filter(el => el.tagName === 'outline').map(parseOutline)
			};
			debug("Parsed blogroll OPML", blogrollParsed);
			return blogrollParsed;
		}
	} catch (e) {
		debug("Error parsing blogroll XML", e);
	}
	return null;
}

async function load_node(data, baseURL, direction) {
	try {
		if(window.hookPreNodeRendering) {
			data = window.hookPreNodeRendering(data);
		}
	} catch (e) {
		debug("Error in hookPreNodeRendering", e);
	}

	try {
		const node = JSON.parse(decodeURIComponent(data));
		debug("node", node);

		// parse blogroll data
		const blogrollData = metadata_get(node, "blogrollData");
		const blogrollParsed = blogrollData ? parse_opml(blogrollData) : null;

		prepare(baseURL, node.title);
		render("body", templateFix(document.getElementById('template').innerHTML), {
			node,
			direction,
			publisher  		: metadata_get(node, "publisher"),
			author			: metadata_get(node, "author"),
			copyright		: metadata_get(node, "copyright"),
			description		: metadata_get(node, "description"),
			homepage		: metadata_get(node, "homepage"),
			blogroll		: metadata_get(node, "blogroll"),
			blogrollData		: blogrollParsed
		});

		contentCleanup ();

		if(window.debugflags > 0)
			document.body.innerHTML += '<pre>' + escapeHTML(JSON.stringify(node, null, 2)) + '</pre>';

		if(window.hookPostNodeRendering)
			window.hookPostNodeRendering();
	} catch (e) {
		document.body.innerHTML = `<div id="errors">Error: Failed to load node! Exception: ${escapeHTML(e)}</div>` + document.body.innerHTML;
		return false;
	}
}

async function load_item(data, baseURL, direction) {
	try {
		if(window.hookPreItemRendering) {
			data = window.hookPreItemRendering(data);
		}
	} catch (e) {
		debug("Error in hookPreItemRendering", e);
	}

	try {
		const item = JSON.parse(decodeURIComponent(data));
		const richContent = metadata_get(item, "richContent");
		const mediathumb = metadata_get(item, "mediathumbnail");
		const mediadesc = metadata_get(item, "mediadescription");
		let title = item.title;
		let article;
		let debugfooter = `<hr/>DEBUG: item_id=${item?.id} `;

		if (richContent) {
			let shadowDoc = document.implementation.createHTMLDocument();
			shadowDoc.body.innerHTML = richContent;
			debugfooter += " Scrape";

			article = new window.Readability(shadowDoc, {charThreshold: 100}).parse();
			if (article) {
				// Use rich content from Readability if available and better!
				if (article.content.length > item.description.length) {
					debug("Using Readability content");
					debugfooter += " Readability";
					item.description = article.content;
				}
			}
		}

		// Using article.title is important, as often the item.title is just a summary
		// or something slightly different. Using the article.title allows for better
		// title duplicate elimination (further below)
		if(article?.title?.length > 5) {
			title = article.title;
			debugfooter += " articleTitle";
		}

		debug("item", item);
		debug("article", article);

		prepare(baseURL, title);
		render("body", templateFix (document.getElementById('template').innerHTML), {
			item,
			direction,
			title,
			author			: metadata_get(item, "author"),
			creator			: metadata_get(item, "creator"),
			sharedby		: metadata_get(item, "sharedby"),
			via			: metadata_get(item, "via"),
			slashSection		: metadata_get(item, "slashSection"),
			slashDepartment		: metadata_get(item, "slashDepartment"),
			categories		: item.metadata.filter((m) => m.category).map((m) => m.category),
			mediathumb		: mediathumb,
			mediadesc		: mediadesc,
			videos			: item.enclosures.filter((enclosure) => enclosure.mime?.startsWith('video/')),
			audios			: item.enclosures.filter((enclosure) => enclosure.mime?.startsWith('audio/'))
			// FIXME: use this too
			/*let related		= metadata_get(item, "related");
			let point		= metadata_get(item, "point");
			let mediaviews	= metadata_get(item, "mediaviews");
			let ratingavg	= metadata_get(item, "mediastarRatingavg");
			let ratingmax	= metadata_get(item, "mediastarRatingMax");
			let gravatar	= metadata_get(item, "gravatar");*/
		});

		// Title duplicate elimination:
		// Check if there is an element which contains exactly the text from item.title
		let el = Array.from(document.getElementById('content').querySelectorAll('*')).find(el => el.textContent.trim() === item.title);
		if (el) {
			let innermost = el;
			while (innermost.children.length > 0) {
				innermost = Array.from(innermost.children).find(child => child.textContent.trim() === item.title) || innermost;
			}
			innermost.remove();
			debugfooter += " titleDuplicateRemove";
		}

		// If there are no images and we have a thumbnail, add it
		if (document.querySelectorAll('img').length == 0 && mediathumb) {
			let img = document.createElement('img');
			img.src = mediathumb;
			img.alt = mediadesc;
			document.getElementById('description').prepend(img);
			debugfooter += " thumbnailAdd";
		}

		// Convert all lazy-loaded images
		document.querySelectorAll('img[data-src]').forEach((img) => {
			img.src = img.getAttribute('data-src');
		});

		// Setup audio/video <select> handler
		document.querySelector('#enclosureVideo select')?.addEventListener("change", (e) => {
			document.querySelector('#enclosureVideo video').src = e.target.options[e.target.selectedIndex].value;
			document.querySelector('#enclosureVideo video').play();
		});
		document.querySelector('#enclosureAudio select')?.addEventListener("change", (e) => {
			document.querySelector('#enclosureAudio audio').src = e.target.options[e.target.selectedIndex].value;
			document.querySelector('#enclosureAudio audio').play();
		});

		// Gopher monospace rendering and link expansion
		if (item.source?.startsWith('gopher://')) {
			const div = document.getElementById('content');
			div.classList.add('gopher');

			// Replace all text HTTP and Gopher URIs in div with a link
			const uriRegex = /\b(https?:\/\/\S+|gopher:\/\/\S+)/g;
			div.innerHTML = div.innerHTML.replace(uriRegex, match => {
				return `<a href="${match}" target="_blank">${match}</a>`;
			});

			debugfooter += " gopher";
		}

		contentCleanup ();

		if(window.debugflags > 0)
			document.body.innerHTML += debugfooter + '<pre>' + escapeHTML(JSON.stringify(item, null, 2)) + '</pre>';

		if(window.hookPostItemRendering)
			window.hookPostItemRendering();

		return true;
	} catch (e) {
		document.body.innerHTML = `<div id="errors">Error: Failed to load item! Exception: ${escapeHTML(e)}</div>` + document.body.innerHTML;
		console.error(e.stack);
		window.data = data;
		return false;
	}
}

/**
 * Different content cleanup tasks
 */
function contentCleanup() {

	// Run DOMPurify
	let content = document.getElementById('content').innerHTML;
	document.getElementById('content').innerHTML = window.DOMPurify.sanitize(content);

	// Fix inline SVG sizes
	const svgMinWidth = 50;
	document.getElementById('content')
		.querySelectorAll('svg')
		.forEach((el) => {
			const h = el.getAttribute('height');
			const w = el.getAttribute('width');
			if(h && w) {
				if(w < svgMinWidth)
					el.parentNode.removeChild(el);
				return;
			}

			const viewbox = el.getAttribute('viewBox');
			if(!viewbox)
				return;

			const size = viewbox.split(/\s+/);
			if(size.length != 4)
				return;

			// Drop smaller SVGs that are usually just layout decorations
			if((size[2] - size[0]) < svgMinWidth) {
				el.parentNode.removeChild(el);
				return;
			}

			// Properly size larger SVGs
			el.width = size[2] - size[0];
			el.heigth = size[3] - size[1];
		});

	// Drop empty elements (to get rid of empty picture/video/iframe divs)
	document.getElementById('content')
		.querySelectorAll(":only-child")
		.forEach((el) => {
			if(el.innerHTML.length == 1)
				el.parentNode.removeChild(el);
		});
}

export { load_node, load_item };
