/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

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 3 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, see <http://www.gnu.org/licenses/>.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { findChildren, findParentNodeClosestToPos } from "@tiptap/core";
import { refractor } from "refractor/lib/core";
import { toCaretPosition, toCodeLines } from "./code-block";
import Languages from "./languages.json";
import { isLanguageLoaded, loadLanguage } from "./loader";
import { getChangedNodes } from "../../utils/prosemirror";
function parseNodes(nodes, className = []) {
    return nodes.reduce((result, node) => {
        if (node.type === "comment" || node.type === "doctype")
            return result;
        const classes = [...className];
        if (node.type === "element" && node.properties)
            classes.push(...node.properties.className);
        // this is required so that even plain text is wrapped in a span
        // during highlighting. Without this, Prosemirror's selection acts
        // weird for the first highlighted node/span.
        // else classes.push("token", "text");
        if (node.type === "element") {
            result.push(...parseNodes(node.children, classes));
        }
        else {
            result.push({ classes, text: node.value });
        }
        return result;
    }, []);
}
function getHighlightNodes(result) {
    return result.children || [];
}
function getDecorations({ block, defaultLanguage }) {
    const decorations = [];
    const languages = refractor.listLanguages();
    const { node, pos } = block;
    const code = node.textContent;
    const language = node.attrs.language || defaultLanguage;
    const nodes = languages.includes(language)
        ? getHighlightNodes(refractor.highlight(code, language))
        : null;
    if (!nodes)
        return;
    let from = pos + 1;
    parseNodes(nodes).forEach((node) => {
        const to = from + node.text.length;
        if (node.classes.length) {
            const decoration = Decoration.inline(from, to, {
                class: node.classes.join(" ")
            });
            decorations.push(decoration);
        }
        from = to;
    });
    return decorations;
}
export function HighlighterPlugin({ name, defaultLanguage }) {
    const HIGHLIGHTER_PLUGIN_KEY = new PluginKey("highlighter");
    const HIGHLIGHTED_BLOCKS = new Set();
    return new Plugin({
        key: HIGHLIGHTER_PLUGIN_KEY,
        view() {
            return {
                destroy() {
                    HIGHLIGHTED_BLOCKS.clear();
                },
                update(view) {
                    return __awaiter(this, void 0, void 0, function* () {
                        const pluginState = HIGHLIGHTER_PLUGIN_KEY.getState(view.state);
                        if (!pluginState)
                            return;
                        const changedBlocks = new Set();
                        for (const blockKey in pluginState.languages) {
                            const language = pluginState.languages[blockKey];
                            if (HIGHLIGHTED_BLOCKS.has(blockKey) &&
                                refractor.registered(language)) {
                                continue;
                            }
                            const languageDefinition = Languages.find((l) => { var _a; return l.filename === language || ((_a = l.alias) === null || _a === void 0 ? void 0 : _a.some((a) => a === language)); });
                            if (!languageDefinition)
                                continue;
                            if (isLanguageLoaded(languageDefinition.filename) ||
                                refractor.registered(languageDefinition.filename)) {
                                if (!HIGHLIGHTED_BLOCKS.has(blockKey)) {
                                    HIGHLIGHTED_BLOCKS.add(blockKey);
                                    changedBlocks.add(blockKey);
                                }
                                continue;
                            }
                            changedBlocks.add(blockKey);
                            HIGHLIGHTED_BLOCKS.add(blockKey);
                            try {
                                const syntax = yield loadLanguage(languageDefinition.filename);
                                if (!syntax) {
                                    throw new Error("Failed to load language definition for " +
                                        languageDefinition.filename);
                                }
                                refractor.register(syntax);
                            }
                            catch (err) {
                                console.error(err);
                                HIGHLIGHTED_BLOCKS.delete(blockKey);
                                changedBlocks.delete(blockKey);
                            }
                        }
                        if (changedBlocks.size > 0) {
                            const { tr } = view.state;
                            const changedNodes = findChildren(tr.doc, (n) => n.type.name === name && changedBlocks.has(n.attrs.id));
                            changedNodes.forEach(({ node, pos }) => {
                                tr.setNodeMarkup(pos, node.type, node.attrs);
                            });
                            tr.setMeta("preventUpdate", true);
                            tr.setMeta("addToHistory", false);
                            view.dispatch(tr);
                        }
                    });
                }
            };
        },
        state: {
            init: (_config, state) => {
                const languages = {};
                findChildren(state.doc, (node) => node.type.name === name).forEach((block) => {
                    const { id, language } = block.node.attrs;
                    if (id && language)
                        languages[id] = language;
                });
                return { decorations: DecorationSet.empty, languages };
            },
            apply: (tr, pluginState) => {
                let { decorations } = pluginState;
                const { languages } = pluginState;
                if (tr.docChanged && !tr.getMeta("selectionUpdate")) {
                    const changedBlocks = getChangedNodes(tr, {
                        descend: true,
                        predicate: (n) => n.type.name === name
                    });
                    if (changedBlocks.length > 0) {
                        const updated = new Set();
                        let hasChanges = false;
                        changedBlocks.forEach((block) => {
                            if (updated.has(block.pos))
                                return;
                            updated.add(block.pos);
                            const { id, language } = block.node.attrs;
                            if (!languages[id] ||
                                (language && !refractor.registered(language))) {
                                languages[id] = language;
                                hasChanges = true;
                            }
                            else {
                                const newDecorations = getDecorations({
                                    block,
                                    defaultLanguage
                                });
                                if (!newDecorations)
                                    return;
                                decorations = decorations.map(tr.mapping, tr.doc);
                                const oldDecorations = decorations.find(block.pos, block.pos + block.node.nodeSize);
                                const { toAdd, toRemove } = diffDecorations(oldDecorations, newDecorations);
                                if (toRemove.length > 0)
                                    decorations = decorations.remove(toRemove);
                                if (toAdd.length > 0)
                                    decorations = decorations.add(tr.doc, toAdd);
                                hasChanges = true;
                            }
                        });
                        if (hasChanges) {
                            return { decorations, languages };
                        }
                    }
                }
                return {
                    decorations: decorations.map(tr.mapping, tr.doc),
                    languages
                };
            }
        },
        props: {
            decorations(state) {
                var _a;
                return (_a = HIGHLIGHTER_PLUGIN_KEY.getState(state)) === null || _a === void 0 ? void 0 : _a.decorations;
            }
        },
        appendTransaction(transactions, oldState, newState) {
            const isDocChanged = transactions.some((tr) => tr.docChanged);
            return updateSelection(name, oldState, newState, isDocChanged);
        }
    });
}
function updateSelection(name, oldState, newState, isDocChanged) {
    var _a;
    const oldNodeName = oldState.selection.$head.parent.type.name;
    const newNodeName = newState.selection.$head.parent.type.name;
    const isInsideCodeblock = oldNodeName === name || newNodeName === name;
    const isSelectionChanged = isInsideCodeblock && !oldState.selection.eq(newState.selection);
    if (isDocChanged || isSelectionChanged) {
        const block = findParentNodeClosestToPos(newState.selection.$head, (node) => node.type.name === name);
        if (!block)
            return null;
        const { node, pos } = block;
        const attributes = Object.assign({}, node.attrs);
        if (isDocChanged || !((_a = attributes.lines) === null || _a === void 0 ? void 0 : _a.length)) {
            const lines = toCodeLines(node.textContent, pos);
            attributes.lines = lines.slice();
        }
        const position = toCaretPosition(newState.selection, isDocChanged ? toCodeLines(node.textContent, pos) : undefined);
        attributes.caretPosition = position;
        const { tr } = newState;
        tr.setMeta("preventUpdate", true);
        tr.setMeta("addToHistory", false);
        tr.setMeta("selectionUpdate", true);
        tr.setNodeMarkup(pos, node.type, attributes);
        return tr;
    }
}
function diffDecorations(oldDecorations, newDecorations) {
    const toAdd = [];
    const toRemove = [];
    for (let i = 0; i < oldDecorations.length; ++i) {
        const oldDecoration = oldDecorations[i];
        const newDecoration = newDecorations[i];
        if (!newDecoration) {
            toRemove.push(oldDecoration);
        }
        else if (oldDecoration.from !== newDecoration.from ||
            oldDecoration.to !== newDecoration.to ||
            oldDecoration.spec.class !== newDecoration.spec.class) {
            toAdd.push(newDecoration);
            toRemove.push(oldDecoration);
        }
    }
    const extraDecorations = newDecorations.length - oldDecorations.length;
    if (extraDecorations > 0) {
        toAdd.push(...newDecorations.slice(newDecorations.length - extraDecorations));
    }
    return { toAdd, toRemove };
}
