import { html, nothing, PropertyValues } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import strings from '@/translations/no.json'
import { PktElement } from '@/base-elements/element'
import { repeat } from 'lit/directives/repeat.js'
import { classMap } from 'lit/directives/class-map.js'
import { IPktComboboxOption } from '@/components/combobox/combobox'
import { uuidish } from '@/utils/stringutils'

declare global {
  interface HTMLElementTagNameMap {
    'pkt-listbox': PktListbox
  }
}

export interface IPktListbox {
  options: IPktComboboxOption[]
  isOpen: boolean
  disabled: boolean
  includeSearch: boolean
  isMultiSelect: boolean
  allowUserInput: boolean
  maxIsReached: boolean
  customUserInput: string | null
  searchPlaceholder: string | null
  searchValue: string | null
  maxLength: number
  userMessage: string | null
}

@customElement('pkt-listbox')
export class PktListbox extends PktElement implements IPktListbox {
  @property({ type: String }) id: string = uuidish()
  @property({ type: String }) label: string | null = null
  @property({ type: Array }) options: IPktComboboxOption[] = []
  @property({ type: Boolean, reflect: true }) isOpen: boolean = false
  @property({ type: Boolean }) disabled: boolean = false
  @property({ type: Boolean }) includeSearch: boolean = false
  @property({ type: Boolean }) isMultiSelect: boolean = false
  @property({ type: Boolean }) allowUserInput: boolean = false
  @property({ type: Boolean }) maxIsReached: boolean = false
  @property({ type: String }) customUserInput: string | null = null
  @property({ type: String }) searchPlaceholder: string | null = null
  @property({ type: String }) searchValue: string | null = null
  @property({ type: Number }) maxLength: number = 0
  @property({ type: String }) userMessage: string | null = null

  private _selectedOptions: number = 0

  @state() private _filteredOptions: IPktComboboxOption[] = []

  // Lifecycle methods
  connectedCallback(): void {
    super.connectedCallback()
    if (this.includeSearch && !this.searchValue) {
      this.searchValue = ''
    }
    if (this.options.length > 0) {
      this.filterOptions()
    }
    this.setAttribute('tabindex', '-1')
    this.addEventListener('focus', this.focusFirstOrSelectedOption)
  }

  updated(changedProperties: PropertyValues) {
    if (changedProperties.has('options') || changedProperties.has('searchValue')) {
      this.filterOptions()
    }
    super.updated(changedProperties)
  }

  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
    if (name === 'options' || name === 'searchValue' || name === 'search-value') {
      this.filterOptions()
    }
    super.attributeChangedCallback(name, _old, value)
  }

  // Render methods
  render() {
    return html`
      <div
        class=${classMap({
          'pkt-listbox': true,
          'pkt-listbox__open': this.isOpen,
          'pkt-txt-16-light': true,
        })}
        role="listbox"
        aria-label=${ifDefined(this.label)}
      >
        <div class="pkt-listbox__banners">
          ${this.renderMaximumReachedBanner()} ${this.renderUserMessage()}
          ${this.renderNewOptionBanner()} ${this.renderSearch()}
        </div>
        <ul class="pkt-listbox__options" role="presentation">
          ${this.renderList()}
        </ul>
      </div>
    `
  }

  renderCheckboxOrCheckIcon(option: IPktComboboxOption, index: number) {
    return this.isMultiSelect
      ? html`
          <input
            class="pkt-input-check__input-checkbox"
            type="checkbox"
            role="presentation"
            tabindex="-1"
            value=${option.value}
            .checked=${option.selected}
            aria-labelledby=${this.id + '-option-label-' + index}
            ?disabled=${this.disabled || option.disabled || (this.maxIsReached && !option.selected)}
          />
        `
      : option.selected
        ? html`<pkt-icon name="check-big"></pkt-icon>`
        : nothing
  }

  renderList() {
    return html`
      ${repeat(
        this._filteredOptions,
        (option) => option.value,
        (option, index) => html`
          <li
            @click=${() => {
              this.toggleOption(option)
            }}
            aria-selected=${option.selected ? 'true' : 'false'}
            @keydown=${this.handleOptionKeydown}
            class=${classMap({
              'pkt-listbox__option': true,
              'pkt-listbox__option--selected': Boolean(!this.isMultiSelect && option.selected),
              'pkt-listbox__option--checkBox': this.isMultiSelect,
            })}
            tabindex="${this.disabled || option.disabled ? '-1' : '0'}"
            data-index=${index}
            data-value=${option.value}
            data-selected=${option.selected ? 'true' : 'false'}
            ?data-disabled=${this.disabled ||
            option.disabled ||
            (this.maxIsReached && !option.selected)}
            role="option"
            id=${`${this.id}-${index}`}
          >
            ${this.renderCheckboxOrCheckIcon(option, index)}
            <span class="pkt-listbox__option-label" id=${this.id + '-option-label-' + index}>
              ${option.prefix
                ? html`<span class="pkt-listbox__option-prefix">${option.prefix}</span>`
                : nothing}
              ${option.label || option.value}
            </span>
            ${option.description
              ? html`<span class="pkt-listbox__option-description pkt-txt-14-light"
                  >${option.description}</span
                >`
              : nothing}
          </li>
        `,
      )}
    `
  }

  renderNewOptionBanner() {
    return this.allowUserInput && this.customUserInput
      ? html`
          <div
            class="pkt-listbox__banner pkt-listbox__banner--new-option pkt-listbox__option"
            data-type="new-option"
            data-value=${this.customUserInput}
            data-selected="false"
            tabindex="0"
            @click=${() =>
              this.toggleOption({
                value: this.customUserInput || '',
              })}
            @keydown=${this.handleOptionKeydown}
          >
            <pkt-icon class="pkt-listbox__banner-icon" name="plus-sign" size="large"></pkt-icon>
            Legg til “${this.customUserInput}”
          </div>
        `
      : nothing
  }

  renderMaximumReachedBanner() {
    this._selectedOptions = this.options.filter((option) => option.selected).length

    return this.isMultiSelect && this._selectedOptions > 0 && this.maxLength > 0
      ? html`
          <div class="pkt-listbox__banner pkt-listbox__banner--maximum-reached">
            ${this._selectedOptions} av maks ${this.maxLength} mulige er valgt.
          </div>
        `
      : nothing
  }

  renderUserMessage() {
    return this.userMessage
      ? html`<div class="pkt-listbox__banner pkt-listbox__banner--user-message">
          <pkt-icon
            class="pkt-listbox__banner-icon"
            name="exclamation-mark-circle"
            size="large"
          ></pkt-icon>
          ${this.userMessage}
        </div>`
      : nothing
  }

  renderSearch() {
    return this.includeSearch
      ? html`
          <div class="pkt-listbox__search">
            <span class="pkt-listbox__search-icon">
              <pkt-icon name="magnifying-glass-small" size="large"></pkt-icon>
            </span>
            <input
              class="pkt-txt-16-light"
              type="text"
              aria-label="Søk i listen"
              form=""
              placeholder=${this.searchPlaceholder || strings.forms.search.placeholder}
              @input=${this.handleSearchInput}
              @keydown=${this.handleSearchKeydown}
              .value=${this.searchValue}
              data-type="searchbox"
              ?disabled=${this.disabled}
              ?readonly=${this.disabled}
              role="searchbox"
            />
          </div>
        `
      : nothing
  }

  // Event handlers
  handleSearchInput(e: InputEvent) {
    this.searchValue = (e.target as HTMLInputElement).value
    this.dispatchEvent(
      new CustomEvent('search', {
        detail: this.searchValue,
        bubbles: false,
      }),
    )
  }

  handleSearchKeydown(e: KeyboardEvent) {
    switch (e.key) {
      case 'Enter':
        e.preventDefault()
        break
      case 'ArrowUp':
      case 'Escape':
        this.closeOptions()
        e.preventDefault()
        break
      case 'ArrowDown':
      case 'Tab':
        this.focusFirstOrSelectedOption()
        break
    }
  }

  handleOptionKeydown(e: KeyboardEvent) {
    const target = e.currentTarget as HTMLElement
    const value = target.dataset.value
    const itemType = target.dataset.type
    const isValueSelected = target.dataset.selected === 'true'

    if (
      !this.getOptionElements().length &&
      (!this.customUserInput || (!this.allowUserInput && this.customUserInput)) &&
      itemType !== 'new-option' &&
      itemType !== 'searchbox'
    ) {
      return
    }

    switch (e.key) {
      case ' ':
      case 'Enter':
        this.toggleOption(target)
        e.preventDefault()
        break

      case 'Backspace':
        if (value) {
          if (isValueSelected) {
            this.toggleOption(target)
          } else {
            this.closeOptions()
          }
        }
        e.preventDefault()
        break

      case 'Escape':
      case 'Tab':
        this.closeOptions()
        break

      case 'ArrowDown':
        if (e.altKey) {
          this.focusLastOption()
        } else {
          if (itemType === 'searchbox' || itemType === 'new-option') {
            this.focusFirstOption()
          } else {
            this.focusNextOption(target)
          }
        }
        e.preventDefault()
        break

      case 'ArrowUp':
        if (e.altKey) {
          this.focusFirstOption()
        } else {
          if (target.dataset.index === '0' && this.includeSearch) {
            const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
            searchInput && searchInput.focus()
          } else if (target.dataset.index === '0' && this.customUserInput) {
            const newOption = this.querySelector('[data-type="new-option"]') as HTMLElement
            newOption && newOption.focus()
          } else {
            this.focusPreviousOption(target)
          }
        }
        e.preventDefault()
        break

      case 'Home':
        this.focusFirstOption()
        e.preventDefault()
        break

      case 'End':
        this.focusLastOption()
        e.preventDefault()
        break

      default:
        if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
          this.selectAll()
          e.preventDefault()
        }
        if (this.isLetterOrSpace(e.key)) {
          this.handleTypeAhead(e.key)
          e.preventDefault()
        }
        break
    }
  }

  // Focus management methods
  focusAndScrollIntoView(el: HTMLElement) {
    el.scrollIntoView({ block: 'nearest' })
    window.setTimeout(() => el.focus(), 0)
  }

  focusNextOption(target: HTMLElement) {
    const nextOption = target.nextElementSibling as HTMLElement
    nextOption && this.focusAndScrollIntoView(nextOption)
  }

  focusPreviousOption(target: HTMLElement) {
    const previousOption = target.previousElementSibling as HTMLElement
    if (target.dataset.index === '0' && this.includeSearch) {
      const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
      searchInput && this.focusAndScrollIntoView(searchInput)
    } else if (previousOption) {
      this.focusAndScrollIntoView(previousOption)
    }
  }

  focusFirstOption() {
    const firstOption = this.getOptionElements()[0]
    firstOption && this.focusAndScrollIntoView(firstOption)
  }

  focusLastOption() {
    const lastOption = this.getOptionElements().pop()
    lastOption && this.focusAndScrollIntoView(lastOption)
  }

  focusFirstOrSelectedOption() {
    if (this.disabled) return
    const selectedOption = this.getOptionElements().find(
      (option) => option.dataset.selected === 'true',
    )
    if (this.allowUserInput && this.customUserInput) {
      const newOption = this.querySelector('[data-type="new-option"]') as HTMLElement
      this.focusAndScrollIntoView(newOption)
    } else if (selectedOption) {
      this.focusAndScrollIntoView(selectedOption)
    } else if (this.includeSearch && !(document.activeElement instanceof HTMLInputElement)) {
      const searchInput = this.querySelector('[role="searchbox"]') as HTMLElement
      window.setTimeout(() => searchInput.focus(), 0)
    } else {
      this.focusFirstOption()
    }
  }

  // Event dispatching methods
  toggleOption(option: IPktComboboxOption | HTMLElement) {
    const optionDisabled = option instanceof HTMLElement ? option.dataset.disabled : option.disabled
    if (this.disabled || optionDisabled) return
    const value = option instanceof HTMLElement ? option.dataset.value : option.value
    this.dispatchEvent(
      new CustomEvent('option-toggle', {
        detail: value,
        bubbles: false,
      }),
    )
  }

  selectAll() {
    this.dispatchEvent(new CustomEvent('select-all', { bubbles: false }))
  }

  closeOptions() {
    this.dispatchEvent(new CustomEvent('close-options', { bubbles: false }))
  }

  // Filtering and typeahead methods

  filterOptions() {
    if (this.searchValue) {
      this._filteredOptions = this.options.filter((option) => {
        const fulltext = option.label + option.value
        return fulltext.toLowerCase().includes(this.searchValue?.toLowerCase() || '')
      })
    } else {
      this._filteredOptions = [...this.options]
    }
  }

  isLetterOrSpace(char: string): boolean {
    return /^[\p{L} ]$/u.test(char)
  }

  handleTypeAhead(char: string) {
    this.typeAheadString += char.toLowerCase()

    if (this.typeAheadTimeout) {
      clearTimeout(this.typeAheadTimeout)
    }

    this.typeAheadTimeout = window.setTimeout(() => {
      this.typeAheadString = ''
    }, 500)

    const options = this.getOptionElements()
    const match = options.find((option) =>
      option.textContent?.trim().toLowerCase().startsWith(this.typeAheadString),
    )

    match && this.focusAndScrollIntoView(match)
  }

  // DOM helper methods
  getOptionElements() {
    if (!this._filteredOptions.length) {
      return []
    }
    return Array.from(
      this.querySelectorAll('[role="option"]:not([data-disabled])') || [],
    ) as HTMLElement[]
  }
}
