<script lang="ts">
  import { onDestroy, type Snippet } from 'svelte';
  import OptionListbox, { type ListboxOption } from '../optionlistbox/OptionListbox.svelte';
  import { ChevronDown24, CrossCircleFilled24 } from '@mozaic-ds/icons-svelte';

  /**
   * A combobox is an input field that allows users to select an option from a dropdown list or enter a custom value. It combines the functionality of a text input and a dropdown menu, providing flexibility and ease of use in forms and user interfaces.
   * @slot item - Use this slot to customize the content of each item.
   * @event update:open {CustomEvent<boolean>} - Emitted when the listbox is opened or closed.
   */
  interface Props {
    [key: string]: any;
    /**
     * The current selected value(s) of the combobox.
     */
    value: string | number | null | (string | number)[];
    /**
     * Enable multiple value mode.
     */
    multiple?: boolean;
    /**
     * Size of the combobox: small ('s') or medium ('m').
     */
    size?: 's' | 'm';
    /**
     * Disable the combobox input.
     */
    disabled?: boolean;
    /**
     * Make the combobox read-only.
     */
    readonly?: boolean;
    /**
     * Mark the combobox as invalid.
     */
    invalid?: boolean;
    /**
     * Show a clear value button.
     */
    clearable?: boolean;
    /**
     * Enable the search input inside the listbox.
     */
    search?: boolean;
    /**
     * Show select all / clear buttons when multiple.
     */
    actions?: boolean;
    /**
     * Enable checkable section headers in the listbox.
     */
    checkablesections?: boolean;
    /**
     * Placeholder text when no value is selected.
     */
    placeholder?: string;
    /**
     * Label for the selected items counter.
     */
    counterlabel?: string;
    /**
     * Placeholder text for the search input.
     */
    searchplaceholder?: string;
    /**
     * Label for the "Select all" button.
     */
    selectlabel?: string;
    /**
     * Label for the "Clear value" button.
     */
    clearlabel?: string;
    /**
     * Array of options to display in the listbox.
     */
    options: Array<{
      label: string;
      content?: string;
      value?: string | number;
      disabled?: boolean;
      type?: 'option' | 'section';
    }>;
    /**
     * Callback to trigger when the listbox is opened or closed.
     */
    onopen?: (value: boolean) => void;
    item?: Snippet<[{ option: ListboxOption }]>;
  }

  let {
    value = $bindable(),
    multiple,
    size = 'm',
    disabled,
    readonly,
    invalid,
    clearable,
    search,
    actions,
    checkablesections,
    placeholder = 'Select an option',
    counterlabel,
    searchplaceholder = 'Find an option...',
    selectlabel = 'Select all',
    clearlabel = 'Clear',
    options,
    onopen,
    item,
    class: className,
    ...attrs
  }: Props = $props();

  const id = crypto.randomUUID();

  let combobox: HTMLElement;
  let listbox: {
    toggleValue: (item?: ListboxOption) => void;
    clearSelection: () => void;
    handleKeydown: (event: KeyboardEvent) => void;
  } | null = $state(null);
  let listboxEl: HTMLElement | null | undefined = $state(null);
  let comboboxControl: HTMLElement;

  let isOpen = $state(false);

  let activeIndex = $state(-1);

  const activeDescendantId = $derived.by(() => {
    return activeIndex >= 0 ? `option-${id}-${activeIndex}` : undefined;
  });

  const selectionLabel = $derived.by(() => {
    if (Array.isArray(value)) {
      if (!value.length) return placeholder;
      return (value as (string | number)[]).map((value) => getOptionLabel(value)).join(', ');
    }

    return getOptionLabel(value) || placeholder;
  });

  function getOptionLabel(value: string | number | null): string {
    return options.find((option) => option.value === value)?.label || '';
  }

  const hasValue = $derived.by(() =>
    multiple ? !!(value as (string | number)[]).length : !!value,
  );

  function handleClickOutside(event: MouseEvent) {
    const path = event.composedPath ? event.composedPath() : [];
    if (!path.includes(combobox) && !path.includes(listboxEl!)) {
      close();
    }
  }

  function open() {
    isOpen = true;
    handleOpen();
    document.addEventListener('click', handleClickOutside);
  }

  function close() {
    isOpen = false;
    handleOpen();
    document.removeEventListener('click', handleClickOutside);
  }

  function toggleListbox() {
    return isOpen ? close() : open();
  }

  function handleOpen() {
    onopen?.(isOpen);

    combobox.dispatchEvent(new CustomEvent('update:open', { bubbles: true, composed: true }));
  }

  onDestroy(() => {
    document.removeEventListener('click', handleClickOutside);
  });
</script>

<div
  bind:this={combobox}
  class={[
    'mc-combobox',
    isOpen && 'mc-combobox--open',
    multiple && 'mc-combobox--multiple',
    invalid && 'mc-combobox--invalid',
    size === 's' && 'mc-combobox--s',
    disabled && 'mc-combobox--disabled',
    readonly && 'mc-combobox--readonly',
    className,
  ]}
  {...attrs}
>
  <div class="mc-combobox__input">
    <button
      bind:this={comboboxControl}
      type="button"
      aria-expanded={isOpen}
      aria-haspopup="listbox"
      aria-controls={`listbox-${id}`}
      {disabled}
      class="mc-combobox__control"
      aria-label="Combobox input"
      onclick={toggleListbox}
      {...search
        ? {}
        : {
            role: 'combobox',
            'aria-activedescendant': activeDescendantId,
            onkeydown: listbox?.handleKeydown,
          }}
    >
      {selectionLabel}
    </button>
    {#if multiple && counterlabel}
      <span class="mc-combobox__counter">
        {counterlabel}
      </span>
    {/if}

    {#if clearable && hasValue}
      <button
        type="button"
        class="mc-combobox__clear"
        aria-label="Clear value"
        onclick={() => listbox?.clearSelection()}
      >
        <CrossCircleFilled24 />
      </button>
    {/if}

    <button
      type="button"
      tabindex={-1}
      class="mc-combobox__icon"
      aria-label="Expand options list"
      onclick={toggleListbox}
    >
      <ChevronDown24 />
    </button>
  </div>

  <div class="mc-combobox__listbox">
    <OptionListbox
      bind:this={listbox}
      bind:element={listboxEl}
      bind:value
      {id}
      open={isOpen}
      {multiple}
      {search}
      {actions}
      {checkablesections}
      {searchplaceholder}
      {selectlabel}
      {clearlabel}
      {options}
      {item}
      activeindex={activeIndex}
      onopen={open}
      onclose={() => {
        close();
        comboboxControl?.focus();
      }}
    />
  </div>
</div>

<style>/**
 * Do not edit directly, this file was auto-generated.
 */
.mc-combobox {
  position: relative;
  width: 100%;
}
.mc-combobox__input {
  transition: box-shadow 200ms ease;
  background-color: var(--forms-color-background-default, #ffffff);
  border: var(--border-width-s, 0.0625rem) solid var(--forms-color-border-default, #666666);
  border-radius: var(--forms-border-radius, 0.25rem);
  transition: all ease 200ms;
  color: var(--forms-color-text-default, #000000);
  display: block;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  box-sizing: border-box;
}
.mc-combobox__input:focus-within {
  box-shadow: 0 0 0 0.125rem var(--focus-color-mid, var(--focus-color-outline-mid, #ffffff)), 0 0 0 0.25rem var(--focus-color-outer, var(--focus-color-outline-outer, #000000));
  outline: 0.125rem solid transparent;
  outline-offset: 0.125rem;
}
.mc-combobox__input:hover:not(:focus-within) {
  border-color: var(--forms-color-border-hover, #4d4d4d);
  box-shadow: 0 0 0 var(--border-width-s, 0.0625rem) var(--forms-color-border-hover, #4d4d4d);
}
.mc-combobox__input:has(.mc-combobox__control:disabled) {
  background-color: var(--forms-color-background-disabled, #d9d9d9);
  cursor: not-allowed;
  border-color: transparent;
  box-shadow: none;
  color: var(--forms-color-text-disabled, #737373);
  pointer-events: none;
}
.mc-combobox__input .mc-combobox__control {
  text-align: left;
  font-size: var(--font-size-150, 1rem);
  font-weight: var(--font-weight-regular, 400);
  color: var(--forms-color-text-default, #000000);
  background-color: transparent;
  border: none;
  padding-left: 0.75rem;
  padding-right: 0;
  flex-grow: 1;
  height: 3rem;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mc-combobox__input .mc-combobox__control:focus-within {
  outline: none;
}
.mc-combobox__icon {
  border: none;
  display: flex;
  align-items: center;
  cursor: pointer;
  padding-right: 0.75rem;
  background: none;
  color: var(--forms-color-icon-interactive, #000000);
  fill: currentcolor;
}
.mc-combobox__clear {
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--forms-color-icon-clear, #666666);
  fill: currentcolor;
}
.mc-combobox__counter {
  height: 1.5rem;
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  padding: 0 0.5rem;
  border-radius: var(--border-radius-l, 1rem);
  background-color: var(--forms-color-selection-counter-background, #464e63);
  font-weight: var(--font-weight-regular, 400);
  font-size: var(--font-size-50, 0.75rem);
  line-height: var(--line-height-s, 1.3);
  color: var(--forms-color-selection-counter-text, #ffffff);
}
.mc-combobox__listbox {
  position: absolute;
  left: 0;
  top: calc(100% + 0.25rem);
  visibility: hidden;
  opacity: 0;
  box-shadow: var(--shadow-bottom-s-x, 0px) var(--shadow-bottom-s-y, 5px) var(--shadow-bottom-s-blur, 10px) var(--shadow-bottom-s-spread, -2px) rgba(0, 0, 0, var(--shadow-bottom-s-opacity, 20%));
  border: var(--border-width-s, 0.0625rem) solid var(--color-border-primary, #cccccc);
  border-radius: var(--border-radius-m, 0.5rem);
  width: 100%;
  z-index: 10;
  background-color: var(--color-background-primary, #ffffff);
  pointer-events: all;
}
.mc-combobox--open .mc-combobox__listbox {
  visibility: visible;
  opacity: 1;
}
.mc-combobox--s .mc-combobox__control {
  height: 2rem;
  font-size: var(--font-size-100, 0.875rem);
  padding-left: 0.5rem;
}
.mc-combobox--s .mc-combobox__icon {
  padding-right: 0.5rem;
}
.mc-combobox--s .mc-combobox__clear {
  width: 1.25rem;
  height: 1.25rem;
}
.mc-combobox--invalid .mc-combobox__input {
  border-color: var(--forms-color-border-invalid, #ea302d);
  box-shadow: 0 0 0 var(--border-width-s, 0.0625rem) var(--forms-color-border-invalid, #ea302d);
}
.mc-combobox--invalid .mc-combobox__input:hover:not(:focus-within) {
  border-color: var(--forms-color-border-invalid-hover, #c61112);
  box-shadow: 0 0 0 var(--border-width-s, 0.0625rem) var(--forms-color-border-invalid-hover, #c61112);
}
.mc-combobox--disabled .mc-combobox__input {
  background-color: var(--forms-color-background-disabled, #d9d9d9);
  cursor: not-allowed;
  border-color: transparent;
  box-shadow: none;
  color: var(--forms-color-text-disabled, #737373);
  pointer-events: none;
}
.mc-combobox--disabled .mc-combobox__control {
  color: var(--forms-color-text-disabled, #737373);
}
.mc-combobox--disabled .mc-combobox__icon {
  color: var(--forms-color-icon-disabled, #737373);
}
.mc-combobox--readonly .mc-combobox__input {
  border-color: var(--forms-color-border-read-only, #cccccc);
  pointer-events: none;
}</style>
