import { Host, h, } from "@stencil/core";
import { EditorState, Selection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Schema, DOMParser } from "prosemirror-model";
import { schema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
import { exampleSetup } from "prosemirror-example-setup";
import { keymap } from "prosemirror-keymap";
import { MenuCommandFactory } from "./menu/menu-commands";
import { menuTranslationIDs, getTextEditorMenuItems } from "./menu/menu-items";
import { MarkdownConverter } from "../utils/markdown-converter";
import { HTMLConverter } from "../utils/html-converter";
import { EditorMenuTypes, editorMenuTypesArray, } from "./menu/types";
import translate from "../../../global/translations";
import { createRandomString } from "../../../util/random-string";
import { isItem } from "../../action-bar/is-item";
import { cloneDeep, debounce } from "lodash-es";
import { strikethrough } from "./menu/menu-schema-extender";
import { createLinkPlugin } from "./plugins/link/link-plugin";
import { linkMarkSpec } from "./plugins/link/link-mark";
import { createImageInserterPlugin } from "./plugins/image/inserter";
import { createImageViewPlugin } from "./plugins/image/view";
import { createMenuStateTrackingPlugin } from "./plugins/menu-state-tracking-plugin";
import { createActionBarInteractionPlugin } from "./plugins/menu-action-interaction-plugin";
import { createNodeSpec } from "../utils/plugin-factory";
import { createTriggerPlugin } from "./plugins/trigger/factory";
import { getTableNodes, getTableEditingPlugins } from "./plugins/table-plugin";
import { getImageNode, imageCache } from "./plugins/image/node";
import { getMetadataFromDoc, hasMetadataChanged, } from "../utils/metadata-utils";
const DEBOUNCE_TIMEOUT = 300;
/**
 * The ProseMirror adapter offers a rich text editing experience with markdown support.
 * [Read more...](https://prosemirror.net/)
 *
 * @exampleComponent limel-example-prosemirror-adapter-basic
 * @exampleComponent limel-example-prosemirror-adapter-with-custom-menu
 * @beta
 * @private
 */
export class ProsemirrorAdapter {
    constructor() {
        /**
         * The type of content that the editor should handle and emit, defaults to `markdown`
         *
         * Assumed to be set only once, so not reactive to changes
         */
        this.contentType = 'markdown';
        /**
         * Set to `true` to disable the field.
         * Use `disabled` to indicate that the field can normally be interacted
         * with, but is currently disabled. This tells the user that if certain
         * requirements are met, the field may become enabled again.
         */
        this.disabled = false;
        /**
         * set to private to avoid usage while under development
         *
         * @private
         * @alpha
         */
        this.customElements = [];
        /**
         * set to private to avoid usage while under development
         *
         * @private
         * @alpha
         */
        this.triggerCharacters = [];
        /**
         * Specifies the visual appearance of the editor.
         */
        this.ui = 'standard';
        this.actionBarItems = [];
        this.link = { href: 'https://' };
        /**
         * Open state of the dialog
         */
        this.isLinkMenuOpen = false;
        this.changeWaiting = false;
        this.transactionFired = false;
        this.lastClickedPos = null;
        this.metadata = { images: [], links: [] };
        /**
         *  Used to stop change event emitting as result of getting updated value from consumer
         */
        this.suppressChangeEvent = false;
        this.getActionBarItems = () => {
            this.actionBarItems = getTextEditorMenuItems().map(this.getTranslatedItem);
        };
        this.getTranslatedItem = (item) => {
            const newItem = cloneDeep(item);
            if (isItem(item)) {
                const translationId = menuTranslationIDs[item.value];
                if (translationId) {
                    newItem.text = translate.get(translationId, this.language);
                }
            }
            return newItem;
        };
        this.updateActiveActionBarItems = (activeTypes, allowedTypes) => {
            const newItems = getTextEditorMenuItems().map((item) => {
                if (isItem(item)) {
                    return Object.assign(Object.assign({}, item), { selected: activeTypes[item.value], allowed: allowedTypes[item.value] });
                }
                return item;
            });
            this.actionBarItems = newItems.filter((item) => isItem(item) ? item.allowed : true);
        };
        this.handleTransaction = (transaction) => {
            this.transactionFired = true;
            const newState = this.view.state.apply(transaction);
            this.view.updateState(newState);
            if (this.suppressChangeEvent || transaction.getMeta('pointer')) {
                return;
            }
            const content = this.contentConverter.serialize(this.view, this.schema);
            if (content === this.lastEmittedValue) {
                return;
            }
            const metadata = getMetadataFromDoc(newState.doc);
            this.metadataEmitter(metadata);
            this.lastEmittedValue = content;
            this.changeWaiting = true;
            this.changeEmitter(content);
        };
        this.handleActionBarItem = (event) => {
            event.preventDefault();
            event.stopImmediatePropagation();
            const { value } = event.detail;
            if (value === EditorMenuTypes.Link) {
                this.isLinkMenuOpen = true;
                return;
            }
            const actionBarEvent = new CustomEvent('actionBarItemClick', {
                detail: event.detail,
            });
            this.view.dom.dispatchEvent(actionBarEvent);
        };
        this.handleCancelLinkMenu = (event) => {
            event.preventDefault();
            event.stopPropagation();
            this.isLinkMenuOpen = false;
            this.link = { text: '', href: 'https://' };
        };
        this.handleSaveLinkMenu = () => {
            this.isLinkMenuOpen = false;
            const saveLinkEvent = new CustomEvent('saveLinkMenu', {
                detail: {
                    type: EditorMenuTypes.Link,
                    link: this.link,
                },
            });
            this.view.dom.dispatchEvent(saveLinkEvent);
            this.link = { href: 'https://' };
        };
        this.handleLinkChange = (event) => {
            this.link = event.detail;
        };
        this.handleFocus = () => {
            var _a;
            if (!this.disabled) {
                (_a = this.view) === null || _a === void 0 ? void 0 : _a.focus();
                // Workaround: On some focus interactions (especially clicking the first line and the last line),
                // ProseMirror does not dispatch a transaction or update the selection. This can cause
                // the cursor to fall back to the end of the document instead of placing it where the user clicked.
                //
                // To detect this, we wait one tick after focus. If no transaction has fired by then,
                // we assume the selection is unresolved and manually move the cursor to the last clicked position.
                this.transactionFired = false;
                setTimeout(() => {
                    if (!this.transactionFired && this.lastClickedPos) {
                        const { doc, tr } = this.view.state;
                        const resolvedPos = doc.resolve(this.lastClickedPos);
                        const selection = Selection.near(resolvedPos);
                        tr.setMeta('pointer', true);
                        this.view.dispatch(tr.setSelection(selection));
                    }
                }, 0);
            }
        };
        this.handleNewLinkSelection = (text, href) => {
            this.link.text = text;
            this.link.href = href || 'https://';
        };
        this.handleOpenLinkMenu = (event) => {
            event.stopImmediatePropagation();
            const { href, text } = event.detail;
            this.link = { href: href, text: text };
            this.isLinkMenuOpen = true;
        };
        this.handleMouseDown = (event) => {
            var _a;
            const coords = { left: event.clientX, top: event.clientY };
            const result = this.view.posAtCoords(coords);
            this.lastClickedPos = (_a = result === null || result === void 0 ? void 0 : result.pos) !== null && _a !== void 0 ? _a : null;
        };
        this.changeEmitter = debounce((value) => {
            this.change.emit(value);
            this.changeWaiting = false;
        }, DEBOUNCE_TIMEOUT);
        this.handleBlur = () => {
            this.changeEmitter.flush();
        };
        this.portalId = createRandomString();
    }
    watchValue(newValue) {
        if (!this.view) {
            return;
        }
        if (this.changeWaiting) {
            // A change is pending; do not update the editor's content
            return;
        }
        const currentContent = this.contentConverter.serialize(this.view, this.schema);
        // If the new value is the same as the current content, do nothing
        if (newValue === currentContent) {
            return;
        }
        // Update the editor's content with the new value
        this.updateView(newValue);
    }
    componentWillLoad() {
        this.getActionBarItems();
        this.setupContentConverter();
    }
    componentDidLoad() {
        // Stencil complains loudly about triggering rerenders in
        // componentDidLoad, but we have to, so we're using setTimeout to
        // suppress the warning. /Ads
        setTimeout(() => {
            this.initializeTextEditor();
        }, 0);
    }
    connectedCallback() {
        if (this.view) {
            this.initializeTextEditor();
        }
        this.host.addEventListener('open-editor-link-menu', this.handleOpenLinkMenu);
    }
    disconnectedCallback() {
        var _a, _b, _c, _d, _e;
        imageCache.clear();
        this.host.removeEventListener('open-editor-link-menu', this.handleOpenLinkMenu);
        (_b = (_a = this.view) === null || _a === void 0 ? void 0 : _a.dom) === null || _b === void 0 ? void 0 : _b.removeEventListener('blur', this.handleBlur);
        (_d = (_c = this.view) === null || _c === void 0 ? void 0 : _c.dom) === null || _d === void 0 ? void 0 : _d.removeEventListener('mousedown', this.handleMouseDown);
        (_e = this.view) === null || _e === void 0 ? void 0 : _e.destroy();
    }
    render() {
        return (h(Host, { key: 'b7dbbd5a73e68e2df2b52f36f6d57bc0885f87a3', onFocus: this.handleFocus }, h("div", { key: 'a630b7eacb1b5be9460b85944e2a3a5955661437', id: "editor" }), this.renderToolbar(), this.renderLinkMenu()));
    }
    renderToolbar() {
        if (this.actionBarItems.length === 0 || this.ui === 'no-toolbar') {
            return;
        }
        return (h("div", { class: "toolbar" }, h("limel-action-bar", { ref: (el) => (this.actionBarElement = el), accessibleLabel: "Toolbar", actions: this.actionBarItems, onItemSelected: this.handleActionBarItem })));
    }
    renderLinkMenu() {
        if (!this.isLinkMenuOpen) {
            return;
        }
        return (h("limel-portal", { containerId: this.portalId, visible: this.isLinkMenuOpen, openDirection: "top", inheritParentWidth: true, anchor: this.actionBarElement }, h("limel-text-editor-link-menu", { link: this.link, isOpen: this.isLinkMenuOpen, onLinkChange: this.handleLinkChange, onCancel: this.handleCancelLinkMenu, onSave: this.handleSaveLinkMenu })));
    }
    setupContentConverter() {
        if (this.contentType === 'markdown') {
            this.contentConverter = new MarkdownConverter(this.customElements, this.language);
        }
        else if (this.contentType === 'html') {
            this.contentConverter = new HTMLConverter(this.customElements);
        }
        else {
            throw new Error(`Unsupported content type: ${this.contentType}. Only 'markdown' and 'html' are supported.`);
        }
    }
    async initializeTextEditor() {
        this.schema = this.initializeSchema();
        const initialDoc = await this.parseInitialContent();
        this.menuCommandFactory = new MenuCommandFactory(this.schema);
        this.view = new EditorView(this.host.shadowRoot.querySelector('#editor'), {
            state: this.createEditorState(initialDoc),
            dispatchTransaction: this.handleTransaction,
        });
        this.view.dom.addEventListener('blur', this.handleBlur);
        this.view.dom.addEventListener('mousedown', this.handleMouseDown);
        if (this.value) {
            this.updateView(this.value);
        }
        // Initialize lastEmittedValue to prevent false change events
        this.lastEmittedValue = this.contentConverter.serialize(this.view, this.schema);
    }
    initializeSchema() {
        let nodes = schema.spec.nodes;
        for (const customElement of this.customElements) {
            const newNodeSpec = createNodeSpec(customElement);
            const nodeName = customElement.tagName;
            nodes = nodes.append({ [nodeName]: newNodeSpec });
        }
        nodes = addListNodes(nodes, 'paragraph block*', 'block');
        if (this.contentType === 'html') {
            nodes = nodes.append(getTableNodes());
        }
        nodes = nodes.append(getImageNode(this.language));
        return new Schema({
            nodes: nodes,
            marks: schema.spec.marks.append({
                strikethrough: strikethrough,
                link: linkMarkSpec,
            }),
        });
    }
    async parseInitialContent() {
        const initialContentElement = document.createElement('div');
        if (this.value) {
            initialContentElement.innerHTML =
                await this.contentConverter.parseAsHTML(this.value, this.schema);
        }
        else {
            initialContentElement.innerHTML = '<p></p>';
        }
        return DOMParser.fromSchema(this.schema).parse(initialContentElement);
    }
    createEditorState(initialDoc) {
        return EditorState.create({
            doc: initialDoc,
            plugins: [
                ...exampleSetup({ schema: this.schema, menuBar: false }),
                keymap(this.menuCommandFactory.buildKeymap()),
                createTriggerPlugin(this.triggerCharacters, this.contentConverter),
                createLinkPlugin(this.handleNewLinkSelection),
                createImageInserterPlugin(this.imagePasted.emit),
                createImageViewPlugin(this.language),
                createMenuStateTrackingPlugin(editorMenuTypesArray, this.menuCommandFactory, this.updateActiveActionBarItems),
                createActionBarInteractionPlugin(this.menuCommandFactory),
                ...getTableEditingPlugins(this.contentType === 'html'),
            ],
        });
    }
    async updateView(content) {
        this.suppressChangeEvent = true;
        const html = await this.contentConverter.parseAsHTML(content, this.schema);
        const prosemirrorDOMparser = DOMParser.fromSchema(this.view.state.schema);
        const domParser = new window.DOMParser();
        const doc = domParser.parseFromString(html, 'text/html');
        const prosemirrorDoc = prosemirrorDOMparser.parse(doc.body);
        const tr = this.view.state.tr;
        tr.replaceWith(0, tr.doc.content.size, prosemirrorDoc.content);
        this.view.dispatch(tr);
        const metadata = getMetadataFromDoc(this.view.state.doc);
        this.metadataEmitter(metadata);
        this.suppressChangeEvent = false;
    }
    metadataEmitter(metadata) {
        if (hasMetadataChanged(this.metadata, metadata)) {
            this.removeImagesFromCache(this.metadata, metadata);
            this.metadata = metadata;
            this.metadataChange.emit(metadata);
        }
    }
    removeImagesFromCache(oldMetadata, newMetadata) {
        const removedImages = oldMetadata.images.filter((oldImage) => !newMetadata.images.some((newImage) => newImage.fileInfoId === oldImage.fileInfoId));
        for (const image of removedImages) {
            imageCache.delete(image.fileInfoId);
            this.imageRemoved.emit(image);
        }
    }
    static get is() { return "limel-prosemirror-adapter"; }
    static get encapsulation() { return "shadow"; }
    static get delegatesFocus() { return true; }
    static get originalStyleUrls() {
        return {
            "$": ["prosemirror-adapter.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["prosemirror-adapter.css"]
        };
    }
    static get properties() {
        return {
            "contentType": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "'markdown' | 'html'",
                    "resolved": "\"html\" | \"markdown\"",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The type of content that the editor should handle and emit, defaults to `markdown`\n\nAssumed to be set only once, so not reactive to changes"
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "content-type",
                "defaultValue": "'markdown'"
            },
            "value": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The value of the editor, expected to be markdown"
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "value"
            },
            "language": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "Languages",
                    "resolved": "\"da\" | \"de\" | \"en\" | \"fi\" | \"fr\" | \"nb\" | \"nl\" | \"no\" | \"sv\"",
                    "references": {
                        "Languages": {
                            "location": "import",
                            "path": "../../date-picker/date.types",
                            "id": "src/components/date-picker/date.types.ts::Languages",
                            "referenceLocation": "Languages"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Defines the language for translations."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "language"
            },
            "disabled": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Set to `true` to disable the field.\nUse `disabled` to indicate that the field can normally be interacted\nwith, but is currently disabled. This tells the user that if certain\nrequirements are met, the field may become enabled again."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "disabled",
                "defaultValue": "false"
            },
            "customElements": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "CustomElementDefinition[]",
                    "resolved": "CustomElementDefinition[]",
                    "references": {
                        "CustomElementDefinition": {
                            "location": "import",
                            "path": "../../../global/shared-types/custom-element.types",
                            "id": "src/global/shared-types/custom-element.types.ts::CustomElementDefinition",
                            "referenceLocation": "CustomElementDefinition"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [{
                            "name": "private",
                            "text": undefined
                        }, {
                            "name": "alpha",
                            "text": undefined
                        }],
                    "text": "set to private to avoid usage while under development"
                },
                "getter": false,
                "setter": false,
                "defaultValue": "[]"
            },
            "triggerCharacters": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "TriggerCharacter[]",
                    "resolved": "TriggerCharacter[]",
                    "references": {
                        "TriggerCharacter": {
                            "location": "import",
                            "path": "../text-editor.types",
                            "id": "src/components/text-editor/text-editor.types.ts::TriggerCharacter",
                            "referenceLocation": "TriggerCharacter"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [{
                            "name": "private",
                            "text": undefined
                        }, {
                            "name": "alpha",
                            "text": undefined
                        }],
                    "text": "set to private to avoid usage while under development"
                },
                "getter": false,
                "setter": false,
                "defaultValue": "[]"
            },
            "ui": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "EditorUiType",
                    "resolved": "\"minimal\" | \"no-toolbar\" | \"standard\"",
                    "references": {
                        "EditorUiType": {
                            "location": "import",
                            "path": "../types",
                            "id": "src/components/text-editor/types.ts::EditorUiType",
                            "referenceLocation": "EditorUiType"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Specifies the visual appearance of the editor."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "ui",
                "defaultValue": "'standard'"
            }
        };
    }
    static get states() {
        return {
            "view": {},
            "actionBarItems": {},
            "link": {},
            "isLinkMenuOpen": {}
        };
    }
    static get events() {
        return [{
                "method": "change",
                "name": "change",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Dispatched when a change is made to the editor"
                },
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                }
            }, {
                "method": "imagePasted",
                "name": "imagePasted",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [{
                            "name": "private",
                            "text": undefined
                        }, {
                            "name": "alpha",
                            "text": undefined
                        }],
                    "text": "Dispatched when a image is pasted into the editor"
                },
                "complexType": {
                    "original": "ImageInserter",
                    "resolved": "ImageInserter",
                    "references": {
                        "ImageInserter": {
                            "location": "import",
                            "path": "../text-editor.types",
                            "id": "src/components/text-editor/text-editor.types.ts::ImageInserter",
                            "referenceLocation": "ImageInserter"
                        }
                    }
                }
            }, {
                "method": "imageRemoved",
                "name": "imageRemoved",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [{
                            "name": "private",
                            "text": undefined
                        }, {
                            "name": "alpha",
                            "text": undefined
                        }],
                    "text": "Dispatched when a image is removed from the editor"
                },
                "complexType": {
                    "original": "EditorImage",
                    "resolved": "EditorImage",
                    "references": {
                        "EditorImage": {
                            "location": "import",
                            "path": "../text-editor.types",
                            "id": "src/components/text-editor/text-editor.types.ts::EditorImage",
                            "referenceLocation": "EditorImage"
                        }
                    }
                }
            }, {
                "method": "metadataChange",
                "name": "metadataChange",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [{
                            "name": "private",
                            "text": undefined
                        }, {
                            "name": "alpha",
                            "text": undefined
                        }],
                    "text": "Dispatched when the metadata of the editor changes (images and links)"
                },
                "complexType": {
                    "original": "EditorMetadata",
                    "resolved": "EditorMetadata",
                    "references": {
                        "EditorMetadata": {
                            "location": "import",
                            "path": "../text-editor.types",
                            "id": "src/components/text-editor/text-editor.types.ts::EditorMetadata",
                            "referenceLocation": "EditorMetadata"
                        }
                    }
                }
            }];
    }
    static get elementRef() { return "host"; }
    static get watchers() {
        return [{
                "propName": "value",
                "methodName": "watchValue"
            }];
    }
}
