/*
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/>.
*/
import { combineTransactionSteps, findChildrenInRange, getChangedRanges, getMarksBetween } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { find } from "linkifyjs";
export function autolink(options) {
    return new Plugin({
        key: new PluginKey("autolink"),
        appendTransaction: (transactions, oldState, newState) => {
            const docChanges = transactions.some((transaction) => transaction.docChanged) &&
                !oldState.doc.eq(newState.doc);
            const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink"));
            if (!docChanges || preventAutolink) {
                return;
            }
            const { tr } = newState;
            const transform = combineTransactionSteps(oldState.doc, [
                ...transactions
            ]);
            const changes = getChangedRanges(transform);
            changes.forEach(({ newRange }) => {
                // Now let’s see if we can add new links.
                const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, (node) => node.isTextblock);
                let textBlock;
                let textBeforeWhitespace;
                if (nodesInChangedRanges.length > 1) {
                    // Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
                    textBlock = nodesInChangedRanges[0];
                    textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, textBlock.pos + textBlock.node.nodeSize, undefined, " ");
                }
                else if (nodesInChangedRanges.length &&
                    // We want to make sure to include the block seperator argument to treat hard breaks like spaces.
                    newState.doc
                        .textBetween(newRange.from, newRange.to, " ", " ")
                        .endsWith(" ")) {
                    textBlock = nodesInChangedRanges[0];
                    textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, undefined, " ");
                }
                if (textBlock && textBeforeWhitespace) {
                    const wordsBeforeWhitespace = textBeforeWhitespace
                        .split(" ")
                        .filter((s) => s !== "");
                    if (wordsBeforeWhitespace.length <= 0) {
                        return false;
                    }
                    const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
                    const lastWordAndBlockOffset = textBlock.pos +
                        textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
                    if (!lastWordBeforeSpace) {
                        return false;
                    }
                    find(lastWordBeforeSpace)
                        .filter((link) => link.isLink)
                        // Calculate link position.
                        .map((link) => (Object.assign(Object.assign({}, link), { from: lastWordAndBlockOffset + link.start + 1, to: lastWordAndBlockOffset + link.end + 1 })))
                        // ignore link inside code mark
                        .filter((link) => {
                        if (!newState.schema.marks.code) {
                            return true;
                        }
                        return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
                    })
                        // validate link
                        .filter((link) => {
                        if (options.validate) {
                            return options.validate(link.value);
                        }
                        return true;
                    })
                        // Add link mark.
                        .forEach((link) => {
                        if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
                            return;
                        }
                        tr.addMark(link.from, link.to, options.type.create({
                            href: link.href
                        }));
                    });
                }
            });
            if (!tr.steps.length) {
                return;
            }
            return tr;
        }
    });
}
