import { Editor, Element, Transforms, Text, Path, Node, Range, Point, } from "slate";
import { SLATE_MARKS, SLATE_BLOCKS, } from "../types";
import { normalizeUrl } from "../../../utils/urlUtils";
// Hot key utility function
export const isHotkey = (hotkey, event) => {
    const keys = hotkey.split("+");
    const modKeys = keys.slice(0, -1);
    const key = keys[keys.length - 1]?.toLowerCase();
    let hasCtrlOrCmd = false;
    let hasShift = false;
    let hasAlt = false;
    for (const modKey of modKeys) {
        switch (modKey) {
            case "mod":
                hasCtrlOrCmd = event.ctrlKey || event.metaKey;
                break;
            case "ctrl":
                hasCtrlOrCmd = event.ctrlKey;
                break;
            case "cmd":
                hasCtrlOrCmd = event.metaKey;
                break;
            case "shift":
                hasShift = event.shiftKey;
                break;
            case "alt":
                hasAlt = event.altKey;
                break;
        }
    }
    return (hasCtrlOrCmd ===
        (modKeys.includes("mod") ||
            modKeys.includes("ctrl") ||
            modKeys.includes("cmd")) &&
        hasShift === modKeys.includes("shift") &&
        hasAlt === modKeys.includes("alt") &&
        event.key.toLowerCase() === key);
};
// Mark plugin with strict typing
export const withMark = (editor) => {
    editor.isMarkActive = (format) => {
        try {
            const marks = Editor.marks(editor);
            return marks ? marks[format] === true : false;
        }
        catch {
            // Editor.marks() can throw when cursor is at a void element (like line-break)
            // In that case, no marks are active
            return false;
        }
    };
    editor.toggleMark = (format) => {
        const isActive = editor.isMarkActive(format);
        if (isActive) {
            Editor.removeMark(editor, format);
        }
        else {
            Editor.addMark(editor, format, true);
        }
    };
    return editor;
};
// Block plugin with strict typing
export const withBlock = (editor) => {
    editor.isBlockActive = (format) => {
        const { selection } = editor;
        if (!selection)
            return false;
        const [match] = Array.from(Editor.nodes(editor, {
            at: Editor.unhangRange(editor, selection),
            match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === format,
        }));
        return !!match;
    };
    editor.toggleBlock = (format) => {
        const isActive = editor.isBlockActive(format);
        Transforms.setNodes(editor, { type: isActive ? "paragraph" : format }, { match: (n) => !Editor.isEditor(n) && Element.isElement(n) });
    };
    return editor;
};
// Alignment plugin with strict typing
export const withAlignment = (editor) => {
    editor.isAlignActive = (align) => {
        const { selection } = editor;
        if (!selection)
            return false;
        const [match] = Array.from(Editor.nodes(editor, {
            at: Editor.unhangRange(editor, selection),
            match: (n) => !Editor.isEditor(n) &&
                Element.isElement(n) &&
                n.align === align,
        }));
        return !!match;
    };
    editor.toggleAlign = (align) => {
        const isActive = editor.isAlignActive(align);
        Transforms.setNodes(editor, { align: isActive ? undefined : align }, { match: (n) => !Editor.isEditor(n) && Element.isElement(n) });
    };
    return editor;
};
// Link plugin
export const withLink = (editor) => {
    const { insertData, insertText, isInline, isElementReadOnly, isSelectable } = editor;
    editor.isInline = (element) => {
        return element.type === "link" ? true : isInline(element);
    };
    editor.isElementReadOnly = (element) => {
        return element.type === "link" ? false : isElementReadOnly(element);
    };
    editor.isSelectable = (element) => {
        return element.type === "link" ? true : isSelectable(element);
    };
    editor.insertText = (text) => {
        if (text && isUrl(text)) {
            wrapLink(editor, normalizeUrl(text));
        }
        else {
            insertText(text);
        }
    };
    editor.insertData = (data) => {
        const text = data.getData("text/plain");
        if (text && isUrl(text)) {
            wrapLink(editor, normalizeUrl(text));
        }
        else {
            insertData(data);
        }
    };
    editor.isLinkActive = () => {
        const [link] = Editor.nodes(editor, {
            match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "link",
        });
        return !!link;
    };
    editor.insertLink = (options) => {
        const { selection } = editor;
        if (!selection || Range.isCollapsed(selection)) {
            return;
        }
        if (editor.isLinkActive()) {
            unwrapLink(editor);
        }
        else {
            if (options?.onOpenLinkDialog) {
                const linkSelection = selection;
                options.onOpenLinkDialog((linkData) => {
                    Transforms.select(editor, linkSelection);
                    wrapLink(editor, linkData.url || linkData.link?.url || "", linkData);
                });
            }
        }
    };
    return editor;
};
// List plugin with strict typing
export const withList = (editor) => {
    editor.isListActive = (listType) => {
        const { selection } = editor;
        if (!selection)
            return false;
        const [match] = Array.from(Editor.nodes(editor, {
            at: Editor.unhangRange(editor, selection),
            match: (n) => !Editor.isEditor(n) &&
                Element.isElement(n) &&
                n.type === "list-item" &&
                (!listType || n.listType === listType),
        }));
        return !!match;
    };
    editor.toggleList = (listType) => {
        const isActive = editor.isListActive(listType);
        if (isActive) {
            // Convert list items back to paragraphs
            Transforms.setNodes(editor, { type: "paragraph", listType: undefined, indent: undefined }, {
                match: (n) => !Editor.isEditor(n) &&
                    Element.isElement(n) &&
                    n.type === "list-item",
            });
        }
        else {
            // Check if we're switching between list types (preserve indentation)
            // or converting from non-list elements (reset to indent 0)
            const selectedNodes = Array.from(Editor.nodes(editor, {
                match: (n) => !Editor.isEditor(n) && Element.isElement(n),
            }));
            selectedNodes.forEach(([node, path]) => {
                const element = node;
                if (element.type === "list-item") {
                    // Switching between list types - preserve existing indentation
                    Transforms.setNodes(editor, { type: "list-item", listType }, { at: path });
                }
                else {
                    // Converting from non-list element - start at indent 0
                    Transforms.setNodes(editor, { type: "list-item", listType, indent: 0 }, { at: path });
                }
            });
        }
    };
    editor.indentList = () => {
        const { selection } = editor;
        if (!selection)
            return;
        const [match] = Array.from(Editor.nodes(editor, {
            match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "list-item",
        }));
        if (match) {
            const [node, path] = match;
            const currentIndent = node.indent || 0;
            // Check if this is the first list item in the document
            const allListItems = Array.from(Editor.nodes(editor, {
                at: [],
                match: (n) => !Editor.isEditor(n) &&
                    Element.isElement(n) &&
                    n.type === "list-item",
            }));
            const currentIndex = allListItems.findIndex(([, itemPath]) => Path.equals(itemPath, path));
            // Don't allow indenting the first list item
            if (currentIndex === 0) {
                return;
            }
            // Find the previous list item to check maximum allowed indent
            if (currentIndex > 0) {
                const prevEntry = allListItems[currentIndex - 1];
                if (prevEntry) {
                    const [prevNode] = prevEntry;
                    const prevIndent = prevNode.indent || 0;
                    const maxAllowedIndent = prevIndent + 1;
                    // Only allow indenting if it doesn't exceed previous item's indent + 1
                    if (currentIndent < maxAllowedIndent) {
                        Transforms.setNodes(editor, {
                            indent: Math.min(currentIndent + 1, Math.min(5, maxAllowedIndent)),
                        }, { at: path });
                    }
                }
            }
        }
    };
    editor.outdentList = () => {
        const { selection } = editor;
        if (!selection)
            return;
        const [match] = Array.from(Editor.nodes(editor, {
            match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "list-item",
        }));
        if (match) {
            const [node, path] = match;
            const currentIndent = node.indent || 0;
            if (currentIndent > 0) {
                Transforms.setNodes(editor, { indent: currentIndent - 1 }, { at: path });
            }
            else {
                // Convert back to paragraph if at root level
                Transforms.setNodes(editor, { type: "paragraph", listType: undefined, indent: undefined }, { at: path });
            }
        }
    };
    return editor;
};
// Pre-computed constants for normalization performance
// These are calculated once at module load time instead of on every normalizeNode call
const VALID_MARKS = Object.keys(SLATE_MARKS);
const VALID_BLOCK_TYPES = Object.keys(SLATE_BLOCKS);
const SPECIAL_ELEMENT_TYPES = [
    "link",
    "list-item",
    "horizontal-rule",
    "line-break",
];
const ALL_VALID_ELEMENT_TYPES = [
    ...VALID_BLOCK_TYPES,
    ...SPECIAL_ELEMENT_TYPES,
];
// Normalization plugin
export const withNormalization = (editor) => {
    const { normalizeNode } = editor;
    editor.normalizeNode = (entry) => {
        const [node, path] = entry;
        // Ensure all text nodes have proper mark structure
        if (Text.isText(node)) {
            const nodeMarks = Object.keys(node).filter((key) => key !== "text");
            for (const mark of nodeMarks) {
                // Allow isRawHtml as special text node property
                if (mark === "isRawHtml") {
                    continue;
                }
                if (!VALID_MARKS.includes(mark)) {
                    // Remove invalid marks
                    Transforms.unsetNodes(editor, mark, { at: path });
                    return;
                }
            }
        }
        // Ensure all elements have valid types
        if (Element.isElement(node)) {
            if (!ALL_VALID_ELEMENT_TYPES.includes(node.type)) {
                // Convert invalid element types to paragraphs
                Transforms.setNodes(editor, { type: "paragraph" }, { at: path });
                return;
            }
        }
        normalizeNode(entry);
    };
    return editor;
};
// Void elements plugin for elements that cannot contain children
export const withVoidElements = (editor) => {
    const { isVoid, isInline } = editor;
    editor.isVoid = (element) => {
        return element.type === "horizontal-rule" || element.type === "line-break"
            ? true
            : isVoid(element);
    };
    editor.isInline = (element) => {
        return element.type === "line-break" ? true : isInline(element);
    };
    return editor;
};
// Insertion plugin for content insertion features
export const withInsertion = (editor) => {
    editor.insertHorizontalRule = () => {
        const hrElement = {
            type: "horizontal-rule",
            children: [{ text: "" }],
        };
        Transforms.insertNodes(editor, hrElement);
        // Move cursor to after the hr element by inserting a new paragraph
        const newParagraph = {
            type: "paragraph",
            children: [{ text: "" }],
        };
        Transforms.insertNodes(editor, newParagraph);
    };
    return editor;
};
// Formatting plugin for text formatting operations
export const withFormatting = (editor) => {
    editor.stripFormatting = () => {
        const { selection } = editor;
        if (!selection)
            return;
        // Remove all marks from the selection
        // Editor.marks() can throw when cursor is at a void element (like line-break)
        try {
            const marks = Editor.marks(editor);
            if (marks) {
                Object.keys(marks).forEach((mark) => {
                    Editor.removeMark(editor, mark);
                });
            }
        }
        catch {
            // Ignore errors at void elements - no marks to remove
        }
        // Remove marks from selected text nodes using transforms
        Transforms.unsetNodes(editor, Object.keys(SLATE_MARKS), {
            at: selection,
            match: (n) => Text.isText(n),
            split: true,
        });
    };
    return editor;
};
// Keyboard handler function
export const createKeyboardHandler = (editor, simplifiedProfile) => {
    return (event) => {
        // Handle hotkeys for marks
        Object.entries(SLATE_MARKS).forEach(([markId, config]) => {
            if ("hotkey" in config &&
                config.hotkey &&
                isHotkey(config.hotkey, event)) {
                event.preventDefault();
                editor.toggleMark(markId);
            }
        });
        // Handle Shift+Enter for line break
        if (isHotkey("shift+enter", event)) {
            event.preventDefault();
            const { selection } = editor;
            if (!selection || !Range.isCollapsed(selection))
                return;
            // Insert line break and immediately move cursor
            const lineBreakElement = {
                type: "line-break",
                children: [{ text: "" }],
            };
            // Insert the void element
            Transforms.insertNodes(editor, lineBreakElement);
            // Ensure cursor moves past the void element
            Transforms.move(editor, { distance: 1, unit: "offset" });
            return;
        }
        // Block normal Enter key behavior if only no-tag is available
        if (event.key === "Enter" && !event.shiftKey && simplifiedProfile) {
            const availableBlocks = simplifiedProfile.blocks;
            // If only "no-tag" is available, prevent default Enter behavior
            if (availableBlocks.length === 1 && availableBlocks[0] === "no-tag") {
                event.preventDefault();
                return;
            }
        }
        // Handle Enter to escape from links
        if (event.key === "Enter") {
            const { selection } = editor;
            if (selection && Range.isCollapsed(selection)) {
                const [linkNode] = Editor.nodes(editor, {
                    match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "link",
                });
                if (linkNode) {
                    const [, linkPath] = linkNode;
                    const linkEnd = Editor.end(editor, linkPath);
                    // Check if cursor is at the end of the link
                    if (Point.equals(selection.anchor, linkEnd)) {
                        event.preventDefault();
                        // Move cursor after the link
                        const after = Editor.after(editor, linkPath);
                        if (after) {
                            Transforms.select(editor, after);
                            editor.insertBreak();
                        }
                        return;
                    }
                }
            }
        }
        // Handle Right arrow to escape from links
        if (event.key === "ArrowRight") {
            const { selection } = editor;
            if (selection && Range.isCollapsed(selection)) {
                const [linkNode] = Editor.nodes(editor, {
                    match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "link",
                });
                if (linkNode) {
                    const [, linkPath] = linkNode;
                    const linkEnd = Editor.end(editor, linkPath);
                    // Check if cursor is at the end of the link
                    if (Point.equals(selection.anchor, linkEnd)) {
                        event.preventDefault();
                        // Move cursor after the link
                        const after = Editor.after(editor, linkPath);
                        if (after) {
                            Transforms.select(editor, after);
                        }
                        return;
                    }
                }
            }
        }
        // Handle Tab for list indentation
        if (event.key === "Tab") {
            event.preventDefault();
            if (event.shiftKey) {
                editor.outdentList();
            }
            else {
                editor.indentList();
            }
            return;
        }
        // Handle Backspace/Delete for empty list items
        if (event.key === "Backspace" || event.key === "Delete") {
            const { selection } = editor;
            if (selection && Range.isCollapsed(selection)) {
                const [match] = Array.from(Editor.nodes(editor, {
                    match: (n) => !Editor.isEditor(n) &&
                        Element.isElement(n) &&
                        n.type === "list-item",
                }));
                if (match) {
                    const [node] = match;
                    const text = Node.string(node);
                    if (text === "") {
                        event.preventDefault();
                        editor.outdentList();
                    }
                }
            }
        }
    };
};
// Helper functions
const isUrl = (text) => {
    try {
        new URL(text);
        return true;
    }
    catch {
        // Check if it looks like a domain without protocol (like www.nivea.de, nivea.com)
        const domainPattern = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
        return domainPattern.test(text);
    }
};
export const generateInternalLinkUrl = (itemId, targetItemLongId, queryString) => {
    if (!itemId && !targetItemLongId) {
        return "#";
    }
    // Use itemId if available, otherwise fall back to targetItemLongId
    const id = itemId || targetItemLongId;
    let url = `/items/${id}`;
    if (queryString) {
        url += `?${queryString}`;
    }
    return url;
};
const wrapLink = (editor, url, linkData) => {
    if (editor.isLinkActive()) {
        unwrapLink(editor);
    }
    const { selection } = editor;
    const isCollapsed = selection && Range.isCollapsed(selection);
    const link = {
        type: "link",
        url,
        link: linkData || {
            type: "external",
            url,
            target: "_blank",
        },
        children: isCollapsed ? [{ text: url }] : [{ text: "" }], // Use placeholder for selected text
    };
    if (isCollapsed) {
        Transforms.insertNodes(editor, link);
    }
    else {
        Transforms.wrapNodes(editor, link, { split: true });
        Transforms.collapse(editor, { edge: "end" });
    }
};
const unwrapLink = (editor) => {
    Transforms.unwrapNodes(editor, {
        match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === "link",
    });
};
//# sourceMappingURL=plugins.js.map