<script lang="ts">
  import { tick } from "svelte";
  import { Check, ChevronDown } from "@lucide/svelte";

  export interface SelectMenuOption {
    value: string;
    label: string;
    group?: string;
    disabled?: boolean;
  }

  interface Props {
    value: string;
    options: SelectMenuOption[];
    placeholder?: string;
    disabled?: boolean;
    ariaLabel?: string;
    class?: string;
    showSelectedGroup?: boolean;
    onChange: (value: string) => void;
  }

  let {
    value,
    options = [],
    placeholder = "Select an option",
    disabled = false,
    ariaLabel = "",
    class: className = "",
    showSelectedGroup = false,
    onChange,
  }: Props = $props();

  let root = $state<HTMLDivElement>();
  let trigger = $state<HTMLButtonElement>();
  let open = $state(false);
  let activeIndex = $state(-1);
  let menuStyle = $state("");
  const menuId = `select-menu-${Math.random().toString(36).slice(2)}`;

  let selectedOption = $derived(options.find((option) => option.value === value));
  let enabledIndices = $derived(options.map((option, index) => option.disabled ? -1 : index).filter((index) => index >= 0));

  function groupCount(group: string): number {
    return options.filter((option) => option.group === group).length;
  }

  function positionMenu() {
    if (!trigger) return;
    const rect = trigger.getBoundingClientRect();
    const viewportGap = 8;
    const menuGap = 5;
    const desiredHeight = Math.min(300, options.length * 34 + 24);
    const spaceBelow = window.innerHeight - rect.bottom - viewportGap;
    const spaceAbove = rect.top - viewportGap;
    const openAbove = spaceBelow < Math.min(desiredHeight, 180) && spaceAbove > spaceBelow;
    const maxHeight = Math.max(96, Math.min(desiredHeight, openAbove ? spaceAbove - menuGap : spaceBelow - menuGap));
    const width = Math.max(rect.width, 180);
    const left = Math.min(rect.left, window.innerWidth - width - viewportGap);
    const top = openAbove ? rect.top - menuGap : rect.bottom + menuGap;
    menuStyle = `left:${Math.max(viewportGap, left)}px;${openAbove ? `bottom:${window.innerHeight - top}px;` : `top:${top}px;`}min-width:${width}px;max-width:${Math.max(180, window.innerWidth - viewportGap * 2)}px;max-height:${maxHeight}px;`;
  }

  async function show() {
    if (disabled || enabledIndices.length === 0) return;
    const selectedIndex = options.findIndex((option) => option.value === value && !option.disabled);
    activeIndex = selectedIndex >= 0 ? selectedIndex : enabledIndices[0];
    open = true;
    await tick();
    positionMenu();
    document.getElementById(`${menuId}-option-${activeIndex}`)?.scrollIntoView({ block: "nearest" });
  }

  function close() {
    open = false;
  }

  function choose(index: number) {
    const option = options[index];
    if (!option || option.disabled) return;
    onChange(option.value);
    close();
    trigger?.focus();
  }

  function moveActive(direction: -1 | 1) {
    if (enabledIndices.length === 0) return;
    const current = enabledIndices.indexOf(activeIndex);
    const next = current < 0
      ? (direction > 0 ? 0 : enabledIndices.length - 1)
      : (current + direction + enabledIndices.length) % enabledIndices.length;
    activeIndex = enabledIndices[next];
    document.getElementById(`${menuId}-option-${activeIndex}`)?.scrollIntoView({ block: "nearest" });
  }

  function handleKeydown(event: KeyboardEvent) {
    if (disabled) return;
    if (!open && ["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) {
      event.preventDefault();
      void show();
      return;
    }
    if (!open) return;
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault();
      moveActive(event.key === "ArrowDown" ? 1 : -1);
    } else if (event.key === "Home" || event.key === "End") {
      event.preventDefault();
      activeIndex = event.key === "Home" ? enabledIndices[0] : enabledIndices[enabledIndices.length - 1];
    } else if (event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      choose(activeIndex);
    } else if (event.key === "Escape" || event.key === "Tab") {
      if (event.key === "Escape") {
        event.preventDefault();
        event.stopPropagation();
      }
      close();
    }
  }

  function handleWindowPointerDown(event: PointerEvent) {
    if (open && root && !root.contains(event.target as Node)) close();
  }
</script>

<svelte:window onpointerdown={handleWindowPointerDown} onresize={positionMenu} onscroll={positionMenu} />

<div class={`select-menu ${className}`} class:open bind:this={root}>
  <button
    bind:this={trigger}
    class="select-menu-trigger"
    type="button"
    {disabled}
    aria-label={ariaLabel || undefined}
    aria-haspopup="listbox"
    aria-expanded={open}
    aria-controls={open ? menuId : undefined}
    onclick={() => open ? close() : void show()}
    onkeydown={handleKeydown}
  >
    <span class="select-menu-value" class:placeholder={!selectedOption} title={selectedOption?.label}>
      {#if showSelectedGroup && selectedOption?.group}<small>{selectedOption.group}</small>{/if}
      <span>{selectedOption?.label ?? placeholder}</span>
    </span>
    <ChevronDown size={14} aria-hidden="true" />
  </button>

  {#if open}
    <div id={menuId} class="select-menu-popup" style={menuStyle} role="listbox" aria-label={ariaLabel || undefined}>
      {#each options as option, index (`${option.value}:${index}`)}
        {#if option.group && (index === 0 || options[index - 1]?.group !== option.group)}
          <div class="select-menu-group" role="presentation">
            <span>{option.group}</span>
            <small>{groupCount(option.group)}</small>
          </div>
        {/if}
        <button
          id={`${menuId}-option-${index}`}
          class="select-menu-option"
          class:active={index === activeIndex}
          class:selected={option.value === value}
          type="button"
          role="option"
          aria-selected={option.value === value}
          disabled={option.disabled}
          onmouseenter={() => { if (!option.disabled) activeIndex = index; }}
          onclick={() => choose(index)}
        >
          <span>{option.label}</span>
          {#if option.value === value}<Check size={14} aria-hidden="true" />{/if}
        </button>
      {/each}
    </div>
  {/if}
</div>
