<script lang="ts">
    // @ts-ignore
    import {go, highlight} from "fuzzysort";
    import {element_has_focus, random_string} from "$shared/lib/helpers";
    import Button from "$shared/components/button.svelte";
    import Chip from "$shared/components/chip.svelte";

    export let name;
    export let id;
    export let maxlength;
    export let placeholder = "Search";
    export let entries = [];
    export let createable = false;
    export let loading = false;
    export let multiple = false;
    export let noResultsText;
    export let errorText;
    export let label;
    export let on_create_async = ({name: string}) => {
    };

    export const reset = () => methods.reset();
    export const select = (id: string) => methods.select_entry(id);
    export const deselect = (id: string) => methods.deselect_entry(id);

    const INTERNAL_ID = "__dropdown-" + random_string(5);

    let searchInputNode;
    let searchResults = [];
    let searchValue = "";
    let showCreationHint = false;
    let showDropdown = false;
    let lastKeydownCode = "";
    let mouseIsOverDropdown = false;
    let mouseIsOverComponent = false;

    $: hasSelection = entries.some((c) => c.selected === true);
    $: if (searchValue.trim()) {
        showCreationHint = createable && entries.every((c) => search.normalise_value(c.name) !== search.normalise_value(searchValue));
    } else {
        showCreationHint = false;
        entries = methods.get_sorted_array(entries);
    }

    const search = {
        normalise_value(value: string): string {
            if (!value) {
                return "";
            }
            return value.toString().trim().toLowerCase();
        },
        do() {
            const query = search.normalise_value(searchValue);
            if (!query.trim()) {
                searchResults = [];
                return;
            }

            const options = {
                limit: 10,
                allowTypo: true,
                threshold: -10000,
                key: "name",
            };
            searchResults = go(query, entries, options);
            showDropdown = true;
        },
        on_input_focusout() {
            if (lastKeydownCode !== "Tab" && (mouseIsOverDropdown || lastKeydownCode === "ArrowDown")) {
                return;
            }
            const selected = entries.find((c) => c.selected === true);
            if (selected && !multiple) {
                searchValue = selected.name;
            }
            document.querySelector("#" + INTERNAL_ID + " ul li.focus")?.classList.remove("focus");
            showDropdown = false;
        }
    };

    const methods = {
        reset(focus_input = false) {
            searchValue = "";
            const copy = entries;
            for (const entry of copy) {
                entry.selected = false;
            }
            entries = methods.get_sorted_array(copy);
            if (focus_input) {
                searchInputNode?.focus();
                showDropdown = true;
            } else {
                showDropdown = false;
            }
        },
        async create_entry(name) {
            if (!name || !createable || loading) {
                console.log("Not sending creation event due to failed preconditions", {name, createable, loading});
                return;
            }
            try {
                await on_create_async({name});
                searchValue = "";
                loading = false;
            } catch (e) {
                console.error(e);
            }
        },
        select_entry(entry_id) {
            if (!entry_id || loading) {
                console.log("Not selecting entry due to failed preconditions", {
                    entry_id,
                    loading,
                });
                return;
            }

            const copy = entries;
            let selected;
            for (const entry of entries) {
                if (entry.id === entry_id) {
                    entry.selected = true;
                    selected = entry;
                    if (multiple) {
                        searchValue = "";
                    } else {
                        searchValue = entry.name;
                    }
                } else if (!multiple) {
                    entry.selected = false;
                }
            }
            entries = methods.get_sorted_array(copy);
            searchInputNode?.focus();
            searchResults = [];
        },
        deselect_entry(entry_id) {
            if (!entry_id || loading) {
                console.log("Not deselecting entry due to failed preconditions", {
                    entry_id,
                    loading,
                });
                return;
            }
            console.log("Deselecting entry", entry_id);

            const copy = entries;
            let deselected;

            for (const entry of copy) {
                if (entry.id === entry_id) {
                    entry.selected = false;
                    deselected = entry;
                }
            }

            entries = methods.get_sorted_array(copy);
            searchInputNode?.focus();
        },
        get_sorted_array(entries: Array<DropdownEntry>): Array<DropdownEntry> {
            if (!entries) {
                return;
            }
            if (entries.length < 1) {
                return [];
            }
            if (searchValue) {
                return entries;
            }
            return (entries as any).sort((a, b) => {
                search.normalise_value(a.name).localeCompare(search.normalise_value(b.name));
            });
        },
    };

    const windowEvents = {
        on_mousemove(event) {
            mouseIsOverDropdown = (event.target?.closest("#" + INTERNAL_ID + " .autocomplete__results") != null ?? false);
            mouseIsOverComponent = (event.target?.closest("#" + INTERNAL_ID) != null ?? false);
        },
        on_click(event) {
            if (showDropdown && !mouseIsOverDropdown && !mouseIsOverComponent && event.target.id !== id && event.target?.htmlFor !== id) {
                showDropdown = false;
            }
        },
        on_keydown(event) {
            lastKeydownCode = event.code;
            const enterPressed = event.code === "Enter";
            const backspacePressed = event.code === "Backspace";
            const arrowUpPressed = event.code === "ArrowUp";
            const spacePressed = event.code === "Space";
            const arrowDownPressed = event.code === "ArrowDown";
            const searchInputHasFocus = element_has_focus(searchInputNode);
            const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul    .focus");

            if (showDropdown && (enterPressed || arrowDownPressed)) {
                event.preventDefault();
                event.stopPropagation();
            }

            if (searchInputHasFocus && backspacePressed && !searchValue && entries.length > 0) {
                if (entries.filter(c => c.selected === true).at(-1)?.id ?? false) {
                    methods.deselect_entry(entries.filter(c => c.selected === true).at(-1)?.id ?? "");
                }
                return;
            }
            if (searchInputHasFocus && enterPressed && showCreationHint) {
                methods.create_entry(searchValue.trim());
                return;
            }

            if (searchInputHasFocus && !focusedEntry && arrowDownPressed) {
                const firstEntry = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type");
                if (firstEntry) {
                    firstEntry.classList.add("focus");
                    return;
                }
            }

            if (focusedEntry && (arrowUpPressed || arrowDownPressed)) {
                if (arrowDownPressed) {
                    if (focusedEntry.nextElementSibling) {
                        focusedEntry.nextElementSibling.classList.add("focus");
                        focusedEntry.nextElementSibling.scrollIntoView();
                    } else {
                        document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type").classList.add("focus");
                        document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type").scrollIntoView();
                    }
                } else if (arrowUpPressed) {
                    if (focusedEntry.previousElementSibling) {
                        focusedEntry.previousElementSibling.classList.add("focus");
                        focusedEntry.previousElementSibling.scrollIntoView();
                    } else {
                        document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type").classList.add("focus");
                        document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type").scrollIntoView();
                    }
                }
                focusedEntry.classList.remove("focus");
                return;
            }

            if (focusedEntry && (spacePressed || enterPressed)) {
                methods.select_entry(focusedEntry.dataset.id);
                return;
            }

            if (lastKeydownCode === "Tab" && !searchInputHasFocus) {
                showDropdown = false;
            }
        },
        on_touchend(event) {
            windowEvents.on_mousemove(event);
        }
    };

    interface DropdownEntry {
        name: string,
        id: string,
    }
</script>

<svelte:window
        on:keydown={windowEvents.on_keydown}
        on:mousemove={windowEvents.on_mousemove}
        on:touchend={windowEvents.on_touchend}
        on:click={windowEvents.on_click}
/>

{#if label}
    <label for="{id}"
           class="form-label margin-bottom-xxs">{label}</label>
{/if}

<div class="autocomplete position-relative select-auto"
     class:cursor-wait={loading}
     class:autocomplete--results-visible={showDropdown}
     class:select-auto--selection-done={searchValue}
     id={INTERNAL_ID}
>
    <!-- input -->
    <div class="select-auto__input-wrapper form-control"
         style="cursor: text"
         on:click={() => {
			 if (!element_has_focus(searchInputNode)) searchInputNode.focus();
			 showDropdown = true;
         }}
         class:multiple={multiple === true}
         class:has-selection={hasSelection}>
        {#if multiple === true && hasSelection}
            {#each entries.filter((c) => c.selected === true) as entry}
                <Chip id={entry.id}
                      removable={true}
                      tabindex="-1"
                      on:remove={() => methods.deselect_entry(entry.id)}
                      text={entry.name}/>
            {/each}
        {/if}
        <input
                class="reset width-100%"
                style="outline:none;"
                type="text"
                {name}
                {id}
                {maxlength}
                {placeholder}
                bind:value={searchValue}
                bind:this={searchInputNode}
                on:input={() => search.do()}
                on:click={() => (showDropdown = true)}
                on:focus={() => (showDropdown = true)}
                on:blur={search.on_input_focusout}
                autocomplete="off"
        />
        <div class="select-auto__input-icon-wrapper">
            <!-- arrow icon -->
            <svg class="icon"
                 viewBox="0 0 16 16">
                <title>Open selection</title>
                <polyline points="1 5 8 12 15 5"
                          fill="none"
                          stroke="currentColor"
                          stroke-linecap="round"
                          stroke-linejoin="round"
                          stroke-width="2"/>
            </svg>

            <!-- close X icon -->
            <button class="reset select-auto__input-btn"
                    type="button"
                    on:click={() => reset(true)}>
                <svg class="icon"
                     viewBox="0 0 16 16">
                    <title>Reset selection</title>
                    <path
                            d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0Zm3.707,10.293a1,1,0,1,1-1.414,1.414L8,9.414,5.707,11.707a1,1,0,0,1-1.414-1.414L6.586,8,4.293,5.707A1,1,0,0,1,5.707,4.293L8,6.586l2.293-2.293a1,1,0,1,1,1.414,1.414L9.414,8Z"
                    />
                </svg>
            </button>
        </div>
    </div>

    {#if errorText}
        <small class="color-error">{errorText}</small>
    {/if}

    <!-- dropdown -->
    <div class="autocomplete__results select-auto__results">
        <ul on:keydown={(event) => event.code.startsWith("Arrow") && event.preventDefault()}
            tabindex="-1"
            class="autocomplete__list">
            {#if searchResults.length > 0}
                {#each searchResults.filter((c) => !c.selected) as result}
                    <li class="select-auto__option padding-y-xs padding-x-sm"
                        data-id={result.obj.id}
                        on:click={(e) => methods.select_entry(e.target.dataset.id)}
                        tabindex="-1">
                        {@html highlight(result, (open = '<span class="font-semibold">'), (close = "</span>"))}
                    </li>
                {/each}
            {:else if entries.length > 0}
                {#each entries.filter((c) => !c.selected) as entry}
                    <li class="select-auto__option padding-y-xs padding-x-sm"
                        data-id={entry.id}
                        on:click={(e) => methods.select_entry(e.target.dataset.id)}
                        tabindex="-1">
                        {entry.name}
                    </li>
                {/each}
            {:else}
                <li class="select-auto__option text-center padding-y-xs padding-x-sm pointer-events-none"
                    tabindex="-1">
                    {noResultsText}
                </li>
            {/if}
        </ul>
        {#if showCreationHint}
            <div class="width-100% border-top border-bg-lighter padding-xxxs">
                <Button variant="reset"
                        type="button"
                        class="width-100%"
                        text="Press enter or click to create {searchValue.trim()}"
                        title="Press enter or click here to create {searchValue.trim()}"
                        loading={loading}
                        on:click={() => methods.create_entry(searchValue.trim())}/>
            </div>
        {/if}
    </div>
</div>
