<!--
@component
Provides user interface for specifying arbitrary scalar `Datum` values, for
example within the `<TableViewer />` or `<ExpressionEditor />`.
-->

<script lang="ts">
  import type { Datum } from "./datum.svelte";
  import {
    datum_from_editor_state,
    editor_state_from_datum,
    type EditorState,
  } from "./editor-state.svelte";
  import {
    type DropdownOption,
    type Presentation,
  } from "./presentation.svelte";

  const BLUR_DEBOUNCE_MS = 100;

  type Props = {
    current_presentation: Presentation;

    on_blur?(ev: FocusEvent): unknown;

    on_cancel_edit?(): unknown;

    on_change?(value?: Datum): unknown;

    on_focus?(ev: FocusEvent): unknown;

    /**
     * In addition to `on_blur()`, this callback is invoked when the component
     * blurs *itself*, for example when a user presses "Enter" or "Escape", as
     * opposed to blurring in response to a user focusing another element. This
     * typically indicates that the parent component should restore focus to a
     * previously focused table cell.
     */
    on_restore_focus?(): unknown;

    /**
     * For use cases in which the user may select between multiple datum types,
     * such as when embedded in an expression editor, this array represents the
     * permissible set of available presentations.
     */
    potential_presentations?: Presentation[];

    value?: Datum;
  };

  let {
    current_presentation = $bindable(),
    on_blur,
    on_cancel_edit,
    on_change,
    on_focus,
    on_restore_focus,
    potential_presentations = [],
    value = $bindable(),
  }: Props = $props();

  let editor_state = $state<EditorState | undefined>();
  let date_input_element = $state<HTMLInputElement | undefined>();
  let text_input_element = $state<HTMLInputElement | undefined>();
  let blur_timeout = $state<number | undefined>();

  $effect(() => {
    if (value) {
      editor_state = editor_state_from_datum(value);
    }
  });

  let filtered_dropdown_options: DropdownOption[] = $state([]);

  export function focus() {
    if (
      current_presentation.t === "Dropdown" ||
      current_presentation.t === "Numeric" ||
      current_presentation.t === "Text" ||
      current_presentation.t === "Uuid"
    ) {
      text_input_element?.focus();
    } else if (current_presentation.t === "Timestamp") {
      date_input_element?.focus();
    }
  }

  function handle_blur(ev: FocusEvent) {
    // Propagating blur events upwards is debounced so that switching focus
    // between elements does not cause spurious `on_blur()` calls.
    if (blur_timeout !== undefined) {
      clearTimeout(blur_timeout);
    }
    blur_timeout = setTimeout(() => {
      on_blur?.(ev);
      filtered_dropdown_options = [];
    }, BLUR_DEBOUNCE_MS);
  }

  function handle_focus(ev: FocusEvent) {
    if (blur_timeout === undefined) {
      on_focus?.(ev);
      update_dropdown_filter();
    } else {
      clearTimeout(blur_timeout);
    }
  }

  function handle_input() {
    if (!editor_state) {
      console.warn("preconditions for handle_input() not met");
      return;
    }
    value = datum_from_editor_state(editor_state, current_presentation);
    on_change?.(value);
  }

  function handle_textinput_input(input_value: string) {
    if (!editor_state) {
      console.warn("text input oninput() preconditions not met");
      return;
    }
    editor_state.text_value = input_value;
    if (input_value !== "") {
      editor_state.is_null = false;
    }
    handle_input();
  }

  function update_dropdown_filter() {
    if (current_presentation.t === "Dropdown") {
      filtered_dropdown_options = current_presentation.c.options.filter(
        (option) =>
          option.value
            .toLocaleLowerCase("en")
            .includes(editor_state?.text_value.toLocaleLowerCase("en") ?? ""),
      );
    }
  }

  function handle_keydown(ev: KeyboardEvent) {
    if (ev.key === "Enter") {
      on_restore_focus?.();
    } else if (ev.key === "Escape") {
      // Cancel edit before blurring, or else the table will try to commit it.
      on_cancel_edit?.();
      on_restore_focus?.();
    } else if (ev.key === "Backspace") {
      if (
        editor_state &&
        ["Dropdown", "Numeric", "Text", "Uuid"].includes(
          current_presentation.t ?? "",
        ) &&
        editor_state.text_value === ""
      ) {
        editor_state.is_null = !editor_state.is_null;
        handle_input();
      }
    } else if (
      current_presentation.t === "Dropdown" &&
      filtered_dropdown_options.length > 0
    ) {
      const current_option_idx = filtered_dropdown_options.findIndex(
        (option) =>
          option.value.toLocaleLowerCase("en") ===
          (editor_state?.text_value.toLocaleLowerCase("en") ?? ""),
      );
      if (ev.key === "ArrowUp" || (ev.key === "Tab" && ev.shiftKey)) {
        // Prevent shifting focus with tab key. User can still use enter or esc.
        ev.preventDefault();
        handle_textinput_input(
          filtered_dropdown_options[Math.max(current_option_idx - 1, 0)].value,
        );
      } else if (ev.key === "ArrowDown" || (ev.key === "Tab" && !ev.shiftKey)) {
        // Prevent shifting focus with tab key. User can still use enter or esc.
        ev.preventDefault();
        handle_textinput_input(
          filtered_dropdown_options[
            Math.min(
              current_option_idx + 1,
              filtered_dropdown_options.length - 1,
            )
          ].value,
        );
      }
    }
  }

  // These handlers must be passed to interactive children to correctly handle
  // focus/blur behavior, particularly when the `<DatumEditor />` is a child of
  // `<TableViewer />`.
  const interactive_handlers = {
    onblur: handle_blur,
    onfocus: handle_focus,
    onkeydown: handle_keydown,
  };
</script>

<div
  class="datum-editor__container"
  class:datum-editor__container--incomplete={!value}
  onblur={handle_blur}
  onfocus={handle_focus}
>
  {#if editor_state}
    {#if potential_presentations?.length > 0}
      <div class="datum-editor__type-selector">
        <select
          {...interactive_handlers}
          oninput={(ev) => {
            current_presentation =
              potential_presentations[parseInt(ev.currentTarget.value)];
          }}
        >
          {#each potential_presentations as presentation, i}
            <option value={`${i}`}>{presentation.t}</option>
          {/each}
        </select>
      </div>
    {/if}
    <button
      {...interactive_handlers}
      type="button"
      class="datum-editor__null-control"
      class:datum-editor__null-control--disabled={editor_state.text_value !==
        ""}
      disabled={editor_state.text_value !== ""}
      onclick={() => {
        if (!editor_state) {
          console.warn("null control onclick() preconditions not met");
          return;
        }
        editor_state.is_null = !editor_state.is_null;
        handle_input();
      }}
    >
      {#if editor_state.is_null}
        <i class="ti ti-cube-3d-sphere-off"></i>
      {:else}
        <i class="ti ti-cube"></i>
      {/if}
    </button>
    {#if ["Dropdown", "Numeric", "Text", "Uuid"].includes(current_presentation.t)}
      <input
        {...interactive_handlers}
        bind:this={text_input_element}
        value={editor_state.text_value}
        oninput={({ currentTarget }) => {
          handle_textinput_input(currentTarget.value);
          update_dropdown_filter();
        }}
        class="datum-editor__text-input"
        type="text"
      />
    {:else if current_presentation.t === "Timestamp"}
      <div class="datum-editor__timestamp-inputs">
        <input
          {...interactive_handlers}
          bind:this={date_input_element}
          oninput={({ currentTarget }) => {
            if (!editor_state) {
              console.warn("date input oninput() preconditions not met");
              return;
            }
            editor_state.date_value = currentTarget.value;
            editor_state.is_null = false;
            handle_input();
          }}
          value={editor_state.date_value}
          type="date"
        />
        <input
          {...interactive_handlers}
          oninput={({ currentTarget }) => {
            if (!editor_state) {
              console.warn("time input oninput() preconditions not met");
              return;
            }
            editor_state.time_value = currentTarget.value;
            editor_state.is_null = false;
            handle_input();
          }}
          value={editor_state.time_value}
          step="1"
          type="time"
        />
      </div>
    {/if}
    <div class="datum-editor__helpers">
      {#if current_presentation.t === "Dropdown"}
        <menu class="datum-editor__dropdown-options">
          {#each filtered_dropdown_options as dropdown_option}
            <!-- FIXME: validate or escape dropdown_option.color -->
            <li
              class={[
                "dropdown-option-badge",
                `dropdown-option-badge--${dropdown_option.color.toLocaleLowerCase("en-US")}`,
              ]}
              role="option"
              aria-selected={dropdown_option.value === value?.c}
            >
              <button
                {...interactive_handlers}
                class="datum-editor__dropdown-option-button"
                onclick={() => {
                  if (!editor_state || !text_input_element) {
                    console.warn(
                      "dropdown option onclick() preconditions not met",
                    );
                    return;
                  }
                  editor_state.text_value = dropdown_option.value;
                  editor_state.is_null = false;
                  text_input_element.focus();
                  handle_input();
                  text_input_element.blur();
                  on_restore_focus?.();
                }}
                tabindex="-1"
                type="button"
              >
                {dropdown_option.value}
              </button>
            </li>
          {/each}
        </menu>
      {/if}
    </div>
  {/if}
</div>
