export class CommandLine extends HTMLElement {
    static commands = new Map();

    constructor() {
        super();
    }

    connectedCallback() {
        this.suggestions = this.appendChild(document.createElement("ul"));
        this.suggestions.classList.add("suggestions");

        let inputWrapper = this.appendChild(document.createElement("div"));
        inputWrapper.classList.add("input-wrapper");
        this.input = inputWrapper.appendChild(document.createElement("input"));
        this.input.type = "text";

        window.addEventListener("keydown", (event) => {
            if (event.key == ":" && !this.visible) {
                event.stopPropagation();
                event.preventDefault();
                this.show();
            }

            if (event.key == "Escape") {
                this.hide();
            }
        });

        window.addEventListener("click", (event) => {
            if (!this.contains(event.target)) {
                this.hide();
            }
        });

        this.input.addEventListener("keydown", (event) => {
            if (event.key == "Enter") {
                event.preventDefault();
                this.hide();
                this.runCommand(this.input.value);
            }

            if (event.key == "Tab") {
                event.preventDefault();
                if (event.shiftKey) this.tabToPreviousSuggestion();
                else this.tabToNextSuggestion();
            }
        });

        this.input.addEventListener("input", () => {
            this.updateSuggestions();
        });
    }

    get visible() {
        return this.classList.contains("visible");
    }

    show() {
        this.classList.add("visible");
        this.input.focus();
        this.input.value = "";
        this.updateSuggestions();
    }

    hide() {
        this.classList.remove("visible");
    }

    tab(current, next) {
        current?.classList?.remove("tabbed");
        next.classList.add("tabbed");
        next.scrollIntoView();

        this.input.value = next.name;
        // NOTE: Do NOT update suggestions here.
        // This would cause the tabbing to break.
    }

    tabToNextSuggestion() {
        let current = this.suggestions.querySelector(".tabbed");
        let next = current?.nextSibling ?? this.suggestions.childNodes[0];
        this.tab(current, next);
    }

    tabToPreviousSuggestion() {
        let current = this.suggestions.querySelector(".tabbed");
        let previous =
            current?.previousSibling ??
            this.suggestions.childNodes[this.suggestions.childNodes.length - 1];
        this.tab(current, previous);
    }

    updateSuggestions() {
        let search = parseCommand(this.input.value)?.command ?? "";
        let suggestions = Array.from(CommandLine.commands.entries()).filter(
            ([name, def]) =>
                !def.isAlias &&
                (def.showInSuggestions?.(this.input.value) ?? true) &&
                fuzzyMatch(search, name),
        );
        suggestions.sort();

        this.suggestions.replaceChildren();
        for (let [name, def] of suggestions) {
            let suggestion = this.suggestions.appendChild(document.createElement("li"));
            if (def.immediate) {
                suggestion.classList.add("immediate");
            }

            let commandName = suggestion.appendChild(document.createElement("dfn"));
            commandName.textContent = name;
            let commandDescription = suggestion.appendChild(document.createElement("span"));
            commandDescription.classList.add("description");
            commandDescription.textContent = def.description;

            suggestion.name = name;
            suggestion.def = def;

            suggestion.addEventListener("click", (event) => {
                event.stopPropagation();

                if (def.immediate) {
                    this.hide();
                    this.runCommand(name);
                } else {
                    this.input.value = name + " ";
                    this.updateSuggestions();
                    this.input.focus();
                }
            });
        }
    }

    runCommand(commandLine) {
        let { command, args } = parseCommand(commandLine);
        let commandDef = CommandLine.commands.get(command);
        if (CommandLine.commands.has(command)) {
            commandDef.run(args);
        } else {
            console.log(`unknown command`);
        }
    }

    static registerCommand({ aliases, description, immediate, showInSuggestions, run }) {
        for (let i = 0; i < aliases.length; ++i) {
            CommandLine.commands.set(aliases[i], {
                isAlias: i != 0,
                description,
                immediate,
                showInSuggestions,
                run,
            });
        }
    }
}

customElements.define("th-command-line", CommandLine);

function parseCommand(commandLine) {
    let result = /^([^ ]+) *(.*)$/.exec(commandLine);
    if (result == null) return null;

    let [_, command, args] = result;
    return { command, args };
}

// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/
function fuzzyMatch(pattern, string) {
    let iPattern = 0;
    let iString = 0;

    while (iPattern < pattern.length && iString < string.length) {
        if (pattern.charAt(iPattern).toLowerCase() == string.charAt(iString).toLowerCase()) {
            iPattern += 1;
        }

        iString += 1;
    }

    return iPattern == pattern.length;
}

CommandLine.registerCommand({
    aliases: ["help", "h"],
    description: '"OwO, what is this?"',
    immediate: true,

    run() {
        window.location = `${TREEHOUSE_SITE}/treehouse/cmd`;
    },
});

CommandLine.registerCommand({
    aliases: ["new", "n"],
    description: "go to news feed",
    immediate: true,

    run() {
        window.location = `${TREEHOUSE_SITE}/treehouse/new`;
    },
});

CommandLine.registerCommand({
    aliases: ["index", "i", "-w-"],
    description: "go home",
    immediate: true,

    run() {
        window.location = `${TREEHOUSE_SITE}/`;
    },
});

CommandLine.registerCommand({
    aliases: ["quit", "exit", "q", "q!", "wq", "wq!", "wqa", "wqa!", "bc", "bc!", "bca", "bca!"],
    description: "quit riki's treehouse (congration!)",
    // non-immediate because this is a destructive action

    showInSuggestions(commandLine) {
        return commandLine.length >= 1;
    },

    run() {
        window.location = `${TREEHOUSE_SITE}/treehouse/quit`;
    },
});