<script lang="ts">
  import { debounce } from "lodash";
  import { createEventDispatcher } from "svelte";
  import type { UpListing } from "upend";
  import type { IValue, VALUE_TYPE } from "upend/types";
  import { fetchAllAttributes } from "../../lib/api";
  import { baseSearchOnce, createLabelled } from "../../util/search";
  import UpObject from "../display/UpObject.svelte";
  import IconButton from "./IconButton.svelte";
  import Input from "./Input.svelte";
  const dispatch = createEventDispatcher();
  import { matchSorter } from "match-sorter";

  const MAX_OPTIONS = 25;

  export let type: "attribute" | "value";
  export let attribute: string | undefined = undefined;
  export let value: IValue | undefined = undefined;
  export let valueTypes: VALUE_TYPE[] | undefined = undefined;
  export let placeholder = "";
  export let disabled = false;

  let inputValue = "";
  if (type == "attribute") {
    inputValue = attribute || "";
  } else {
    inputValue = String(value?.c || "");
  }
  $: if (value === undefined) inputValue = undefined;

  function onInput(ev: CustomEvent<string>) {
    if (type == "attribute") {
      attribute = ev.detail;
    } else {
      value = {
        t: "String",
        c: ev.detail,
      };
    }
  }

  interface SelectorOption {
    attribute?: string;
    value?: IValue;
    labelToCreate?: string;
  }

  let options: SelectorOption[] = [];
  let searchResult: UpListing;
  const updateOptions = debounce(async (query: string, doSearch: boolean) => {
    if (query.length == 0) {
      options = [];
      return;
    }

    switch (type) {
      case "attribute": {
        const allAttributes = await fetchAllAttributes();
        options = allAttributes
          .map((attr) => attr.name)
          .filter((attr) => attr.toLowerCase().includes(query.toLowerCase()))
          .map((attribute) => {
            return {
              attribute,
            };
          });
        break;
      }
      case "value": {
        options = [];

        if (valueTypes === undefined || valueTypes.includes("Number")) {
          const number = parseFloat(query);
          if (!Number.isNaN(number)) {
            options.push({
              value: {
                t: "Number",
                c: number,
              },
            });
          }
        }

        if (valueTypes === undefined || valueTypes.includes("String")) {
          options.push({
            value: {
              t: "String",
              c: query,
            },
          });
        }

        if (valueTypes === undefined || valueTypes.includes("Address")) {
          if (doSearch) {
            searchResult = await baseSearchOnce(query);
          }

          let exactHits = Object.entries(addressToLabels)
            .filter(([_, labels]) =>
              labels
                .map((l) => l.toLowerCase())
                .includes(inputValue.toLowerCase())
            )
            .map(([addr, _]) => addr);

          if (exactHits.length) {
            exactHits.forEach((addr) =>
              options.push({
                value: {
                  t: "Address",
                  c: addr,
                },
              })
            );
          } else {
            options.push({
              labelToCreate: inputValue,
            });
          }

          const validOptions = searchResult.entries
            .filter((e) => e.attribute === "LBL")
            .filter((e) => !exactHits.includes(e.entity));

          const sortedOptions = matchSorter(validOptions, inputValue, {
            keys: ["value.c"],
          });

          options.push(
            ...sortedOptions.map((e) => {
              return {
                value: {
                  t: "Address",
                  c: e.entity,
                },
              } as SelectorOption;
            })
          );
        }

        options = options;
        break;
      }
    }
  }, 200);

  $: {
    if (inputFocused) {
      updateOptions(inputValue, true);
      addressToLabels = {};
    }
  }

  let addressToLabels: { [key: string]: string[] } = {};
  function onAddressResolved(address: string, ev: CustomEvent<string[]>) {
    addressToLabels[address] = ev.detail;
    updateOptions(inputValue, false);
  }

  async function set(option: SelectorOption) {
    switch (type) {
      case "attribute":
        attribute = option.attribute;
        inputValue = option.attribute;
        break;
      case "value":
        if (!option.labelToCreate) {
          value = option.value;
          inputValue = String(option.value.c);
        } else {
          const addr = await createLabelled(option.labelToCreate);
          value = {
            t: "Address",
            c: addr,
          };
          inputValue = addr;
        }
        break;
    }
    dispatch("input", value);
    visible = false;
  }

  let inputFocused = false;
  let hover = false;
  $: visible = (inputFocused || hover) && Boolean(options.length);

  let input: Input;
  export function focus() {
    input.focus();
  }
</script>

<div class="selector">
  {#if value?.t == "Address" && inputValue}
    <div class="input">
      <div class="label">
        <UpObject link address={String(value.c)} />
      </div>
      <IconButton name="x" on:click={() => (inputValue = "")} />
    </div>
  {:else}
    <Input
      bind:this={input}
      bind:value={inputValue}
      on:input={onInput}
      on:focusChange={(ev) => (inputFocused = ev.detail)}
      {disabled}
      {placeholder}
    />
  {/if}
  <ul
    class="options"
    class:visible
    on:mouseenter={() => (hover = true)}
    on:mouseleave={() => (hover = false)}
  >
    {#each options.slice(0, MAX_OPTIONS) as option}
      <li on:click={() => set(option)}>
        {#if option.attribute}
          {option.attribute}
        {:else if option.value}
          {#if option.value.t == "Address"}
            <UpObject
              address={String(option.value.c)}
              on:resolved={(ev) =>
                onAddressResolved(String(option.value.c), ev)}
            />
          {:else}
            <div class="type">{option.value.t}</div>
            <div class="content">{option.value.c}</div>
          {/if}
        {:else if option.labelToCreate}
          <div class="type">Create object</div>
          <div class="content">{option.labelToCreate}</div>
        {/if}
      </li>
    {/each}
  </ul>
</div>

<style lang="scss">
  .selector {
    position: relative;
  }

  .input {
    display: flex;
    min-width: 0;
    .label {
      flex: 1;
      min-width: 0;
    }
  }

  .options {
    position: absolute;
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--foreground-lighter);
    width: 100%;
    border-radius: 4px;
    margin-top: 2px;
    background: var(--background);
    font-size: smaller;

    visibility: hidden;
    opacity: 0;

    transition: opacity 0.2s;

    z-index: 99;

    &.visible {
      visibility: visible;
      opacity: 1;
    }

    li {
      cursor: pointer;
      padding: 0.5em;

      transition: background-color 0.2s;
      &:hover {
        background-color: var(--background-lighter);
      }

      .type,
      .content {
        display: inline-block;
      }

      .type {
        opacity: 0.8;
        font-size: smaller;
      }
    }
  }
</style>
