// retoor <retoor@molodetz.nl>

export class ContentRenderer {
    constructor() {
        this.emojiMap = {};
        this.emojiLoaded = false;
        this.imageExtRe = /\.(jpg|jpeg|png|gif|webp|svg|bmp|webp)(\?.*)?$/i;
        this.videoExtRe = /\.(mp4|webm|ogg|ogv|mov|m4v)(\?.*)?$/i;
        this.audioExtRe = /\.(mp3|wav|flac|ogg|aac|m4a|wma|opus)(\?.*)?$/i;
        this.youtubeRe = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
    }

    replaceShortcodes(text) {
        return text.replace(/:([a-zA-Z0-9_+\-]+):/g, (match, name) => {
            return this.emojiMap[name.toLowerCase()] || match;
        });
    }

    normalizeDashes(text) {
        return text.replace(/\u2014/g, "-");
    }

    render(text, authorAdmin = false) {
        if (!text) return "";

        let widgets = [];
        const widgetRe = /<dp-widget>([\s\S]*?)<\/dp-widget>/gi;
        if (authorAdmin && widgetRe.test(text)) {
            widgetRe.lastIndex = 0;
            let m;
            let idx = 0;
            let parts = [];
            let last = 0;
            while ((m = widgetRe.exec(text)) !== null) {
                if (m.index > last) {
                    parts.push(text.substring(last, m.index));
                }
                parts.push("\x00WIDGET_" + idx + "\x00");
                widgets.push(m[1]);
                idx++;
                last = m.index + m[0].length;
            }
            if (last < text.length) {
                parts.push(text.substring(last));
            }
            text = parts.join("");
        }

        text = this.normalizeDashes(text);
        text = this.replaceShortcodes(text);

        let html;
        if (typeof marked !== "undefined") {
            html = marked.parse(text, { breaks: true, gfm: true });
        } else {
            html = "<p>" + text.replace(/\n/g, "<br>") + "</p>";
        }

        if (typeof DOMPurify === "undefined") {
            throw new Error("DOMPurify not loaded; refusing to render untrusted HTML");
        }
        html = DOMPurify.sanitize(html);

        for (let i = 0; i < widgets.length; i++) {
            html = html.replace("\x00WIDGET_" + i + "\x00", widgets[i]);
        }

        html = this.processMedia(html);

        return html;
    }

    plainText(text) {
        if (!text) return "";
        let html;
        try {
            html = this.render(text);
        } catch (err) {
            return String(text).replace(/\s+/g, " ").trim();
        }
        const div = document.createElement("div");
        div.innerHTML = html;
        return (div.textContent || "").replace(/\s+/g, " ").trim();
    }

    preview(text, length = 60) {
        const plain = this.plainText(text);
        if (plain.length <= length) return plain;
        return plain.slice(0, length).replace(/\s+$/, "") + "...";
    }

    escapeHtml(text) {
        const div = document.createElement("div");
        div.textContent = text;
        return div.innerHTML;
    }

    renderInline(text, authorAdmin = false) {
        if (!text) return "";

        let widgets = [];
        const widgetRe = /<dp-widget>([\s\S]*?)<\/dp-widget>/gi;
        if (authorAdmin && widgetRe.test(text)) {
            widgetRe.lastIndex = 0;
            let m;
            let idx = 0;
            let parts = [];
            let last = 0;
            while ((m = widgetRe.exec(text)) !== null) {
                if (m.index > last) {
                    parts.push(text.substring(last, m.index));
                }
                parts.push("\x00WIDGET_" + idx + "\x00");
                widgets.push(m[1]);
                idx++;
                last = m.index + m[0].length;
            }
            if (last < text.length) {
                parts.push(text.substring(last));
            }
            text = parts.join("");
        }

        text = this.normalizeDashes(text);
        text = this.replaceShortcodes(text);

        let html;
        if (typeof marked !== "undefined" && typeof marked.parseInline === "function") {
            html = marked.parseInline(text, { gfm: true });
        } else {
            html = this.escapeHtml(text);
        }

        if (typeof DOMPurify === "undefined") {
            throw new Error("DOMPurify not loaded; refusing to render untrusted HTML");
        }

        html = DOMPurify.sanitize(html, {
            ALLOWED_TAGS: ["b", "strong", "i", "em", "code", "del", "s", "mark", "sub", "sup", "span", "br"],
            ALLOWED_ATTR: [],
        });

        for (let i = 0; i < widgets.length; i++) {
            html = html.replace("\x00WIDGET_" + i + "\x00", widgets[i]);
        }

        return html;
    }

    processMedia(html) {
        const temp = document.createElement("div");
        temp.innerHTML = html;

        this.walkNodes(temp, (node) => {
            if (node.nodeType !== 3) return;
            const text = node.textContent;
            if (!text || text.trim().length === 0) return;

            const parts = [];
            let lastIndex = 0;
            const urlRe = /(https?:\/\/[^\s<]+)/g;
            const mentionRe = /(?:^|\s|\x28)@([a-zA-Z0-9_-]+)/g;
            let match;

            while ((match = urlRe.exec(text)) !== null) {
                if (match.index > lastIndex) {
                    const chunk = text.substring(lastIndex, match.index);
                    this.splitMentions(chunk, parts);
                }
                parts.push({ type: "url", value: match[0], start: match.index, end: match.index + match[0].length });
                lastIndex = match.index + match[0].length;
            }
            if (lastIndex < text.length) {
                const chunk = text.substring(lastIndex);
                this.splitMentions(chunk, parts);
            }

            if (parts.length <= 1 && parts[0]?.type === "text") return;

            const fragment = document.createDocumentFragment();
            for (const part of parts) {
                if (part.type === "text") {
                    fragment.appendChild(document.createTextNode(part.value));
                } else if (part.type === "mention") {
                    const a = document.createElement("a");
                    a.href = "/profile/" + part.username;
                    a.className = "mention-link";
                    a.textContent = "@" + part.username;
                    fragment.appendChild(a);
                } else {
                    const el = this.urlToEmbed(part.value);
                    if (el) {
                        fragment.appendChild(el);
                    } else {
                        const a = document.createElement("a");
                        a.href = part.value;
                        a.target = "_blank";
                        a.rel = "noopener noreferrer";
                        a.textContent = part.value;
                        fragment.appendChild(a);
                    }
                }
            }

            node.parentNode.replaceChild(fragment, node);
        });

        return temp.innerHTML;
    }

    splitMentions(text, parts) {
        const mentionRe = /(?:^|\s|\x28)@([a-zA-Z0-9_-]+)/g;
        let m;
        let last = 0;
        while ((m = mentionRe.exec(text)) !== null) {
            if (m.index > last) {
                parts.push({ type: "text", value: text.substring(last, m.index) });
            }
            const atIdx = m[0].lastIndexOf('@');
            const prefix = m[0].slice(0, atIdx);
            if (prefix) {
                parts.push({ type: "text", value: prefix });
            }
            parts.push({ type: "mention", username: m[1] });
            last = m.index + m[0].length;
        }
        if (last < text.length) {
            parts.push({ type: "text", value: text.substring(last) });
        }
    }

    altFromUrl(url) {
        try {
            const path = new URL(url, window.location.origin).pathname;
            const file = decodeURIComponent(path.split("/").pop() || "");
            const name = file.replace(/\.[^.]+$/, "").replace(/[-_]/g, " ").trim();
            return name || "Embedded image";
        } catch {
            return "Embedded image";
        }
    }

    urlToEmbed(url) {
        const ytMatch = url.match(this.youtubeRe);
        if (ytMatch) {
            const videoId = ytMatch[1];
            const wrap = document.createElement("div");
            wrap.className = "embed-youtube";
            wrap.innerHTML = (
                '<iframe src="https://www.youtube.com/embed/' + videoId + '" ' +
                'frameborder="0" allowfullscreen ' +
                'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture">' +
                '</iframe>'
            );
            return wrap;
        }

        if (this.imageExtRe.test(url)) {
            const img = document.createElement("img");
            img.src = url;
            img.alt = this.altFromUrl(url);
            img.loading = "lazy";
            return img;
        }

        if (this.videoExtRe.test(url)) {
            const video = document.createElement("video");
            video.src = url;
            video.controls = true;
            video.preload = "metadata";
            return video;
        }

        if (this.audioExtRe.test(url)) {
            const audio = document.createElement("audio");
            audio.src = url;
            audio.controls = true;
            audio.preload = "metadata";
            return audio;
        }

        return null;
    }

    walkNodes(root, callback) {
        const skipTags = new Set(["CODE", "PRE", "A", "IFRAME", "IMG", "VIDEO", "SCRIPT", "STYLE"]);
        const iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null, false);
        let node;
        while ((node = iter.nextNode())) {
            let parent = node.parentNode;
            let skip = false;
            while (parent) {
                if (skipTags.has(parent.tagName)) { skip = true; break; }
                parent = parent.parentNode;
            }
            if (!skip) callback(node);
        }
    }

    applyTo(element) {
        if (!element) return;
        const authorAdmin = element.hasAttribute("data-author-admin");
        const text = element.textContent || element.innerText || "";
        const rendered = this.render(text, authorAdmin);
        element.innerHTML = rendered;
    }

    highlightAll() {
        if (typeof hljs !== "undefined") {
            document.querySelectorAll("pre code").forEach((el) => {
                hljs.highlightElement(el);
            });
        }
    }
}

export const contentRenderer = new ContentRenderer();
contentRenderer.ready = import("./emoji-shortcodes.js")
    .then((mod) => {
        contentRenderer.emojiMap = mod.EMOJI_SHORTCODES;
    })
    .catch(() => {})
    .finally(() => {
        contentRenderer.emojiLoaded = true;
    });
window.contentRenderer = contentRenderer;
