import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
import { Fragment } from "prosemirror-model";
import { EditorMenuTypes, MouseButtons } from "../../menu/types";
import { getLinkAttributes } from "./utils";
export const linkPluginKey = new PluginKey('linkPlugin');
const updateLink = (view, updateLinkCallback) => {
    const { from, to } = view.state.selection;
    let text = '';
    let href = '';
    view.state.doc.nodesBetween(from, to, (node, pos) => {
        if (node.type.name !== 'text') {
            return;
        }
        const fromInNode = Math.max(0, from - pos);
        const toInNode = Math.min(node.text.length, to - pos);
        text += node.text.slice(fromInNode, toInNode);
        // eslint-disable-next-line unicorn/no-array-for-each
        node.marks.forEach((mark) => {
            if (mark.type.name === 'link') {
                href = mark.attrs.href;
            }
        });
    });
    if (updateLinkCallback) {
        updateLinkCallback(text, href);
    }
};
/**
 * Finds the start position of the link node ensuring the href matches the original link's href.
 * @param doc - The ProseMirror document.
 * @param pos - The position to start searching from.
 * @param href - The href attribute of the original link mark.
 * @returns The start position of the link node.
 */
const findStart = (doc, pos, href) => {
    while (pos > 0) {
        const node = doc.nodeAt(pos - 1);
        if (!(node === null || node === void 0 ? void 0 : node.isText) ||
            !node.marks.some((mark) => mark.type.name === EditorMenuTypes.Link &&
                mark.attrs.href === href)) {
            break;
        }
        pos--;
    }
    return pos;
};
/**
 * Finds the end position of the link node ensuring the href matches the original link's href.
 * @param doc - The ProseMirror document.
 * @param pos - The position to start searching from.
 * @param href - The href attribute of the original link mark.
 * @returns The end position of the link node.
 */
const findEnd = (doc, pos, href) => {
    while (pos < doc.content.size) {
        const node = doc.nodeAt(pos);
        if (!(node === null || node === void 0 ? void 0 : node.isText) ||
            !node.marks.some((mark) => mark.type.name === EditorMenuTypes.Link &&
                mark.attrs.href === href)) {
            break;
        }
        pos++;
    }
    return pos;
};
/**
 * Gets the link data at the specified position.
 * @param view - The ProseMirror editor view.
 * @param event - The mouse event.
 * @returns An object containing the link data or null if no link is found.
 */
const getLinkDataAtPosition = (view, event) => {
    const pos = view.posAtCoords({ left: event.clientX, top: event.clientY });
    const node = view.state.doc.nodeAt(pos === null || pos === void 0 ? void 0 : pos.pos);
    if (!node) {
        return null;
    }
    const linkMark = node.marks.find((mark) => mark.type.name === EditorMenuTypes.Link);
    if (!linkMark) {
        return null;
    }
    const href = linkMark.attrs.href;
    const from = findStart(view.state.doc, pos.pos, href);
    const to = findEnd(view.state.doc, pos.pos, href);
    const text = view.state.doc.textBetween(from, to, ' ');
    return { href: href, text: text, from: from, to: to };
};
const processModClickEvent = (view, event) => {
    const linkData = getLinkDataAtPosition(view, event);
    if (!linkData.href) {
        return false;
    }
    event.preventDefault();
    const { href } = linkData;
    if (href) {
        window.open(href, '_blank', 'noopener,noreferrer');
        return true;
    }
    return false;
};
const openLinkMenu = (view, href, text) => {
    const event = new CustomEvent('open-editor-link-menu', {
        detail: { href: href, text: text },
        bubbles: true,
        composed: true,
    });
    view.dom.dispatchEvent(event);
};
const processDoubleClickEvent = (view, event) => {
    const linkData = getLinkDataAtPosition(view, event);
    if (!linkData) {
        return false;
    }
    const { href, text, from, to } = linkData;
    const transaction = view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to));
    view.dispatch(transaction);
    openLinkMenu(view, href, text);
    return true;
};
/**
 * Regular expression for matching URLs, mailto links, phone links, and bare www-links
 */
const URL_REGEX = /(https?:\/\/[^\s<>"']+|mailto:[^\s<>"']+|tel:[^\s<>"']+|www\.[^\s<>"']+)/g;
/**
 * Checks if the text contains any URLs, mailto links, or phone links
 * @param text
 */
const hasUrls = (text) => {
    // Reset regex before use
    URL_REGEX.lastIndex = 0;
    return URL_REGEX.test(text);
};
/**
 * Creates a text node with the provided content
 * @param schema
 * @param content
 */
const createTextNode = (schema, content) => {
    return schema.text(content);
};
/**
 * Creates a link node with the provided URL
 * @param schema
 * @param url
 */
const createLinkNode = (schema, url) => {
    const normalizeUrlForLinkMark = (input) => {
        let output = input.trim();
        while (output.endsWith('\\')) {
            output = output.slice(0, -1);
        }
        if (output.toLowerCase().startsWith('www.')) {
            output = `https://${output}`;
        }
        return output;
    };
    const normalizedUrl = normalizeUrlForLinkMark(url);
    const linkMark = schema.marks.link.create(getLinkAttributes(normalizedUrl, normalizedUrl));
    return schema.text(normalizedUrl, [linkMark]);
};
/**
 * Finds all link matches in the provided text
 * @param text
 */
const findLinkMatches = (text) => {
    const matches = [];
    let match;
    // Reset regex before use
    URL_REGEX.lastIndex = 0;
    while ((match = URL_REGEX.exec(text)) !== null) {
        matches.push({
            url: match[0],
            start: match.index,
            end: match.index + match[0].length,
        });
    }
    return matches;
};
/**
 * Creates nodes for the pasted text while preserving soft line breaks.
 * - Each newline becomes a `hard_break`.
 * - Empty lines are preserved (consecutive newlines => multiple `hard_break`s).
 * - URLs inside each line are converted to link-marked text.
 * @param text - Raw pasted text
 * @param schema - ProseMirror schema
 */
const createNodesWithLinksAndBreaks = (text, schema) => {
    // Split preserves empty lines between consecutive newlines
    const lines = text.split(/\r\n|\r|\n/);
    const nodes = [];
    for (const [index, line] of lines.entries()) {
        if (line.length > 0) {
            nodes.push(...createNodesWithLinks(line, schema));
        }
        if (index < lines.length - 1) {
            const hb = schema.nodes.hard_break;
            if (hb) {
                nodes.push(hb.create());
            }
            else {
                // Fallback: if schema lacks hard_break, defer to default paste behavior
                // (Do NOT throw; keep behavior stable across versions)
                console.warn('hard_break node not found in schema');
            }
        }
    }
    return nodes;
};
/**
 * Creates text nodes with links for any URLs, mailto links, or phone links found in the text
 * @param text
 * @param schema
 */
const createNodesWithLinks = (text, schema) => {
    const nodes = [];
    const matches = findLinkMatches(text);
    if (matches.length === 0) {
        // No links found, just return the text as a single node
        return [createTextNode(schema, text)];
    }
    let lastIndex = 0;
    // Process each match
    for (const match of matches) {
        // Add text before the current link if any
        if (match.start > lastIndex) {
            nodes.push(createTextNode(schema, text.slice(lastIndex, match.start)));
        }
        // Add the link node
        nodes.push(createLinkNode(schema, match.url));
        lastIndex = match.end;
    }
    // Add any remaining text after the last link
    if (lastIndex < text.length) {
        nodes.push(createTextNode(schema, text.slice(lastIndex)));
    }
    return nodes;
};
/**
 * Pastes nodes at the current selection
 * @param view - The editor view
 * @param nodes - Array of nodes to paste
 */
const pasteAsLink = (view, nodes) => {
    if (nodes.length === 0) {
        return;
    }
    if (isSingleLinkNode(nodes)) {
        insertSingleLink(view, nodes[0]);
    }
    else {
        insertNodeFragment(view, nodes);
    }
};
/**
 * Checks if the nodes array contains just a single link node
 * @param nodes
 */
const isSingleLinkNode = (nodes) => {
    if (nodes.length !== 1) {
        return false;
    }
    const node = nodes[0];
    // Must be text with non-empty content
    if (!node.isText || !node.text || node.text.trim() === '') {
        return false;
    }
    // Must have a link mark (even if there are other marks, we just care about link presence)
    return node.marks.some((mark) => mark.type.name === 'link');
};
/**
 * Inserts a single link node, applying it to selected text if present
 * @param view
 * @param linkNode
 */
const insertSingleLink = (view, linkNode) => {
    const { state, dispatch } = view;
    const { from, to } = state.selection;
    const linkMark = linkNode.marks.find((mark) => mark.type.name === 'link');
    // Use selected text if there's a selection, otherwise use the URL
    const selectedText = state.doc.textBetween(from, to, ' ') || linkMark.attrs.href;
    // Insert the text and add the link mark
    dispatch(state.tr
        .insertText(selectedText, from, to)
        .addMark(from, from + selectedText.length, linkMark));
};
/**
 * Inserts multiple nodes as a fragment at the current selection
 * @param view - The editor view
 * @param nodes - Array of nodes to insert
 */
const insertNodeFragment = (view, nodes) => {
    const { state, dispatch } = view;
    const { from, to } = state.selection;
    // Create a fragment from the array of nodes
    const fragment = Fragment.fromArray(nodes);
    // Replace the current selection with the fragment
    dispatch(state.tr.replaceWith(from, to, fragment));
};
/**
 * Handles pasted content, converting URLs to links
 * @param view
 * @param event
 */
const processPasteEvent = (view, event) => {
    var _a;
    const text = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text/plain');
    if (!text || !hasUrls(text)) {
        return false;
    }
    const nodes = createNodesWithLinksAndBreaks(text, view.state.schema);
    event.preventDefault();
    pasteAsLink(view, nodes);
    return true;
};
export const createLinkPlugin = (updateLinkCallback) => {
    return new Plugin({
        key: linkPluginKey,
        props: {
            handlePaste: (view, event) => {
                return processPasteEvent(view, event);
            },
            handleDOMEvents: {
                mousedown: (view, event) => {
                    if ((event.metaKey || event.ctrlKey) &&
                        event.button === 0) {
                        return processModClickEvent(view, event);
                    }
                },
                dblclick: (view, event) => {
                    if (event.button !== MouseButtons.Right) {
                        // We want to ignore right-clicks
                        return processDoubleClickEvent(view, event);
                    }
                },
                click: (_view, event) => {
                    if (!(event.target instanceof HTMLElement)) {
                        return;
                    }
                    // Prevent unhandled navigation and bubbling for link clicks
                    const link = event.target.closest('a');
                    if (link) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                },
            },
        },
        view: () => ({
            update: (view) => {
                updateLink(view, updateLinkCallback);
            },
        }),
    });
};
