/* ===================== AUTOGENERATED SOURCE IMPORT ===================== */

import Template from "/emcJS/util/html/template/HTMLTemplate.js";

const TPL = new Template(`
    <div class="header">
        <button id="expand-button" tabindex="-1">⛶</button>
    </div>
    <div id="container">
        <div id="line-numbers">
            <div class="caret">1</div>
        </div>
        <div id="input-wrapper">
            <textarea id="input" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off"></textarea>
            <div id="output-wrapper">
                <code id="input-mirror"></code>
                <div id="line-marker"></div>
            </div>
        </div>
    </div>
    <div class="footer">
        <span id="line-info">Ln 1, Col 1</span>
    </div>
    
`);

import GlobalStyle from "/emcJS/util/html/template/CSSTemplate.js";

const STYLE = new GlobalStyle(`
    #field {
        display: flex;
        flex-direction: column;
        min-height: 10em;
        padding: 0px;
        color: var(--input-text-color, #000000);
        background-color: var(--input-back-color, #ffffff);
        background-image: var(--input-back-image, none);
        border-style: solid;
        border-width: var(--input-border-width, 2px);
        border-color: var(--input-border-color, #222222);
        border-radius: var(--input-border-radius, 6px);
        overflow: hidden;
    }
    
    #field.expanded {
        position: fixed;
        top: 0px;
        left: 0px;
        bottom: 0px;
        right: 0px;
        min-width: 100%;
        min-height: 100%;
        border-style: none;
        border-radius: 0px;
        z-index: var(--z-index-input-expanded, 100000000);
    }
    
    #field:not(.expanded):focus-within {
        box-shadow: 0 0 2px 3px var(--input-focus-color, #06b5ff);
        z-index: 1;
    }
    
    :host {
        font-size: 14px;
    }
    
    :host(:invalid) #field:not(.expanded):focus-within {
        box-shadow: 0 0 2px 3px var(--input-invalid-focus-color, #ff2500);
    }
    
    /* --- */
    #container {
        --num-digits: 1;
        position: relative;
        box-sizing: border-box;
        display: flex;
        flex: 1;
        width: 100%;
        margin: 0;
        padding: 0;
        color: #ffffff;
        background-color: #232323;
        font-family: monospace;
        font-size: 1em;
        overflow-y: scroll;
        overflow-x: hidden;
    }
    
    #line-numbers {
        box-sizing: border-box;
        width: calc(var(--num-digits, 1) * 0.55em + 1.8em);
        min-height: 100%;
        height: max-content;
        padding-top: 5px;
        padding-bottom: calc(var(--scrollbar-size, 0) + 5px);
        border-right-style: solid;
        border-right-width: 1px;
        border-color: #ffffff1a;
        user-select: none;
    }
    
    #line-numbers > div {
        position: relative;
        box-sizing: border-box;
        display: inline-flex;
        justify-content: end;
        height: 1.5em;
        left: 0;
        width: calc(var(--num-digits, 1) * 0.55em + 2em);
        padding: 0 1em 0 1em;
        color: #89a3d9;
        font-family: monospace;
        font-size: 1em;
        font-weight: bold;
        text-align: end;
        line-height: 1.5em;
        z-index: 1;
        content: counter(step-counter);
    }
    
    #line-numbers > div.caret {
        color: #ffffff;
    }
    
    #input-wrapper {
        position: sticky;
        display: flex;
        top: 0;
        width: 100%;
        padding: 5px 0px;
        overflow-y: hidden;
        overflow-x: scroll;
    }
    
    #input-wrapper > #output-wrapper {
        position: absolute;
        display: flex;
        top: 0;
        min-width: 100%;
        height: 100%;
        padding: 5px 0;
        overflow: hidden;
        pointer-events: none;
    }
    
    #input-wrapper > #input,
    #input-wrapper > #output-wrapper > #input-mirror {
        position: relative;
        box-sizing: border-box;
        min-width: 100%;
        min-height: 100%;
        margin: 0;
        padding: 0 5px;
        color: #ffffff;
        border: none;
        font-family: monospace;
        font-size: 1em;
        line-height: 1.5em;
        white-space: pre;
        tab-size: 4;
    }
    
    #input-wrapper > #input {
        color: transparent;
        caret-color: #ffffff;
        background-color: transparent;
        resize: none;
        overflow: hidden;
    }
    
    #input-wrapper > #input::selection {
        background: #445155;
        text-shadow: none;
    }
    
    #input-wrapper > #input::-moz-selection {
        background: #445155;
        text-shadow: none;
    }
    
    #input-wrapper > #input:focus {
        outline: none;
    }
    
    #input-wrapper > #output-wrapper > #input-mirror {
        height: fit-content;
        padding-bottom: 5px;
    }
    
    #input-wrapper > #output-wrapper > #line-marker {
        position: absolute;
        box-sizing: border-box;
        display: inline-flex;
        top: calc(1.5em * var(--current-line, 0) + 5px);
        left: 0;
        height: 1.5em;
        width: 100%;
        border-top-style: solid;
        border-bottom-style: solid;
        border-top-width: 1px;
        border-bottom-width: 1px;
        border-color: #ffffff1a;
    }
    
    #input-wrapper > #output-wrapper > #line-marker,
    #input-wrapper > #output-wrapper > #input-mirror {
        pointer-events: none;
        z-index: 1;
    }
    
    /* --- */
    .header {
        display: flex;
        justify-content: flex-end;
        border-bottom-style: solid;
        border-bottom-width: var(--input-border-width, 2px);
        border-bottom-color: var(--input-border-color, #222222);
    }
    
    :host(:invalid) .header {
        border-bottom-color: var(--input-invalid-border-color, #ff4444);
    }
    
    :host(:disabled) .header {
        border-bottom-color: var(--input-disabled-border-color, #6a6a6a) !important;
    }
    
    .footer {
        display: flex;
        justify-content: flex-end;
        padding: 2px 20px;
        border-top-style: solid;
        border-top-width: var(--input-border-width, 2px);
        border-top-color: var(--input-border-color, #222222);
    }
    
    :host(:invalid) .footer {
        border-top-color: var(--input-invalid-border-color, #ff4444);
    }
    
    :host(:disabled) .footer {
        border-top-color: var(--input-disabled-border-color, #6a6a6a) !important;
    }
    
    #line-info {
        font-size: 0.7em;
        user-select: none;
    }
    
    #expand-button {
        display: flex;
        width: 1.5em;
        height: 1.5em;
        border: none;
        align-items: center;
        justify-content: center;
        color: var(--button-text-color, #000000);
        background-color: var(--button-back-color, #eaeaea);
        background-image: var(--button-back-image, none);
        cursor: pointer;
        user-select: none;
        appearance: none;
        font-size: 1em;
        line-height: 1em;
    }
    
    #expand-button:focus {
        outline: none;
    }
    
    #expand-button:hover {
        color: var(--button-hover-text-color, #ffffff);
        background-color: var(--button-hover-back-color, #353935);
        background-image: var(--button-hover-back-image, none);
    }
    
`);

const CONFIG_FIELDS = [
    {
        "type": "SwitchInput",
        "name": "newlineonshift",
        "label": "Hold shift to add newline",
        "value": false
    }
]
;

/* ======================================================================= */

import AbstractFormElement from "../../AbstractFormElement.js";
import {debounce} from "../../../../../util/Debouncer.js";
import {registerFocusable} from "../../../../../util/helper/html/ElementFocusHelper.js";
import {safeSetAttribute} from "../../../../../util/helper/ui/NodeAttributes.js";
import {deepClone} from "../../../../../util/helper/DeepClone.js";
import FormElementRegistry from "../../../../../data/registry/form/FormElementRegistry.js";

const LAST_CHARACTER_NEWLINE = /\n$/;

// TODO add enter and escape action to not trap keyboard
// TODO add css variables for code editor colors
// TODO customize scrollbars to have sharp corners and transparent backgrounds
export default class CodeInput extends AbstractFormElement {

    static get formConfigurationFields() {
        return [...super.formConfigurationFields, ...deepClone(CONFIG_FIELDS)];
    }

    #fieldEl;

    #containerEl;

    #lineNumbersEl;

    #lineMarkerEl;

    #inputEl;

    #inputMirrorEl;

    #outputWrapperEl;

    #lineInfoEl;

    #expandButtonEl;

    #lineCount = 1;

    #currentLine = 0;

    constructor() {
        super();
        this.#fieldEl = this.shadowRoot.getElementById("field");
        this.#fieldEl.append(TPL.generate());
        STYLE.apply(this.shadowRoot);
        /* --- */
        this.#containerEl = this.shadowRoot.getElementById("container");
        this.#lineNumbersEl = this.shadowRoot.getElementById("line-numbers");
        this.#lineMarkerEl = this.shadowRoot.getElementById("line-marker");
        this.#lineInfoEl = this.shadowRoot.getElementById("line-info");
        this.#expandButtonEl = this.shadowRoot.getElementById("expand-button");
        this.#outputWrapperEl = this.shadowRoot.getElementById("output-wrapper");
        this.#inputEl = this.shadowRoot.getElementById("input");
        this.#inputMirrorEl = this.shadowRoot.getElementById("input-mirror");
        this.#inputEl.addEventListener("input", () => {
            const value = this.#inputEl.value;
            this.#updateText(value);
            this.value = value;
        });
        this.#inputEl.addEventListener("keydown", (event) => {
            if (event.key === "Enter") {
                if (!event.shiftKey === this.newlineOnShift) {
                    if (this.form != null) {
                        event.preventDefault();
                        this.form.requestSubmit();
                    }
                }
                event.stopPropagation();
                return false;
            }
        });
        /* --- */
        this.#containerEl.addEventListener("scroll", () => {
            const scrollTop = this.#containerEl.scrollTop;
            this.#inputEl.scrollTop = scrollTop;
            this.#outputWrapperEl.scrollTop = scrollTop;
            this.#updateCaretPosition(true);
        }, {passive: false});
        this.#inputEl.addEventListener("scroll", () => {
            const scrollTop = this.#inputEl.scrollTop;
            this.#containerEl.scrollTop = scrollTop;
            this.#outputWrapperEl.scrollTop = scrollTop;
            this.#updateCaretPosition(true);
        }, {passive: false});
        /* --- */
        this.#inputEl.addEventListener("focus", (event) => {
            event.stopPropagation();
            const selection = this.shadowRoot.getSelection();
            if (selection.focusNode != null) {
                this.#updateCaretPosition();
            }
        });
        document.addEventListener("selectionchange", () => {
            const selection = this.shadowRoot.getSelection();
            if (selection.focusNode != null) {
                this.#updateCaretPosition();
            }
        }, {passive: true});
        /* --- */
        this.#expandButtonEl.addEventListener("click", () => {
            if (this.#fieldEl.classList.contains("expanded")) {
                this.#fieldEl.classList.remove("expanded");
                this.#expandButtonEl.innerText = "⛶";
            } else {
                this.#fieldEl.classList.add("expanded");
                this.#expandButtonEl.innerText = "🗙";
            }
        });
    }

    formDisabledCallback(disabled) {
        super.formDisabledCallback(disabled);
        this.#inputEl.disabled = disabled;
    }

    focus(options) {
        this.#inputEl.focus(options);
    }

    set newlineOnShift(value) {
        this.setBooleanAttribute("newlineonshift", value);
    }

    get newlineOnShift() {
        return this.getBooleanAttribute("newlineonshift");
    }

    static get observedAttributes() {
        return [...super.observedAttributes, "readonly"];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        super.attributeChangedCallback(name, oldValue, newValue);
        switch (name) {
            case "readonly": {
                if (oldValue != newValue) {
                    safeSetAttribute(this.#inputEl, "readonly", this.readonly);
                }
            } break;
        }
    }

    renderValue(value) {
        this.#inputEl.value = value ?? "";
        this.#updateText(value);
    }

    #updateText(value) {
        const lineCount = value.split("\n").length;
        this.#printLineNumbers(lineCount);
        if (LAST_CHARACTER_NEWLINE.test(value)) {
            this.#inputMirrorEl.innerText = `${value} `;
        } else {
            this.#inputMirrorEl.innerText = value;
        }
    }

    #updateCaretPosition = debounce((force = false) => {
        const selectionStart = this.#inputEl.selectionStart;
        const selectionEnd = this.#inputEl.selectionEnd;
        const selectionForward = this.#inputEl.selectionDirection === "forward";
        const value = this.#inputEl.value;
        const textBeforeCursor = value.slice(0, selectionForward ? selectionEnd : selectionStart);
        const lines = textBeforeCursor.split("\n");
        const currentLine = lines.length - 1;
        const focusOffset = lines.at(-1)?.length ?? 0;

        if (this.#currentLine != currentLine || force) {
            this.#currentLine = currentLine;

            const oldNumbersNodes = this.#lineNumbersEl.querySelectorAll(".caret");
            for (const oldNode of oldNumbersNodes) {
                oldNode.classList.remove("caret");
            }

            const numberEl = this.#lineNumbersEl.children[currentLine];
            if (numberEl != null) {
                numberEl.classList.add("caret");
            }

            this.#lineMarkerEl.style.setProperty("--current-line", currentLine);
        }

        const length = Math.abs(selectionEnd - selectionStart);
        if (length > 0) {
            this.#lineInfoEl.innerText = `Ln ${currentLine + 1}, Col ${focusOffset + 1} (${length} selected)`;
        } else {
            this.#lineInfoEl.innerText = `Ln ${currentLine + 1}, Col ${focusOffset + 1}`;
        }
    });

    #printLineNumbers(lineCount) {
        if (this.#lineCount != lineCount) {
            this.#lineCount = lineCount;
            this.#lineNumbersEl.innerHTML = "";
            for (let i = 0; i < lineCount; ++i) {
                const numberEl = document.createElement("div");
                numberEl.innerText = i + 1;
                this.#lineNumbersEl.append(numberEl);
            }
            this.#containerEl.style.setProperty("--num-digits", `${lineCount}`.length);
        }
    }

}

FormElementRegistry.register("CodeInput", CodeInput);
customElements.define("emc-input-code", CodeInput);
registerFocusable("emc-input-code");
