import { html, PropertyValues } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { Ref, createRef, ref } from 'lit/directives/ref.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { PktInputElement } from '@/base-elements/input-element'
import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
import { PktSlotController } from '@/controllers/pkt-slot-controller'
import '@/components/input-wrapper'

export type TSelectOption = {
  value: string
  label: string
  selected?: boolean
  disabled?: boolean
  hidden?: boolean
}

export interface IPktSelect {
  options: TSelectOption[]
  value: string
}

/**
 * Pkt Select is a wrapper for the native select element using the pkt-input-wrapper component.
 *
 * The component will prioritize options passed as a prop over options passed as children if both are provided.
 * This is to allow for dynamic options that might change in the case of both children/slot and props are provided.
 *
 * @slot (default) - Options to be rendered as children
 * @prop {TSelectOption[]} options - Options to be rendered as children
 *
 *
 */

declare global {
  interface HTMLElementTagNameMap {
    'pkt-select': PktSelect & HTMLSelectElement
  }
}

@customElement('pkt-select')
export class PktSelect extends PktInputElement implements IPktSelect {
  private inputRef: Ref<HTMLSelectElement> = createRef()
  private helptextSlot: Ref<HTMLElement> = createRef()

  @property({ type: Array }) options: TSelectOption[] = []
  @property({ type: String }) value: string = ''

  @state() private _options: TSelectOption[] = []

  public selectedIndex: number | null = -1
  public selectedOptions: HTMLCollectionOf<HTMLOptionElement> | undefined = undefined

  constructor() {
    super()
    this.optionsController = new PktOptionsSlotController(this)
    this.slotController = new PktSlotController(this, this.helptextSlot)
    this.slotController.skipOptions = true
  }

  // Used for initilization
  connectedCallback(): void {
    super.connectedCallback()

    const optionsReceivedFromProps = this.options.length > 0
    const checkIfOptionNodesInSlot =
      this.optionsController.nodes.length && this.optionsController.nodes.length > 0

    if (!optionsReceivedFromProps && checkIfOptionNodesInSlot) {
      this.optionsController.nodes.forEach((node: Element) => {
        const option: TSelectOption = {
          value: node.hasAttribute('value')
            ? (node.getAttribute('value') ?? '')
            : (node.textContent ?? ''),
          label: node.textContent || node.getAttribute('value') || '',
          disabled: node.hasAttribute('disabled'),
          hidden: node.hasAttribute('data-hidden'),
        }
        if (node.getAttribute('selected') && !this.value) {
          this.value = option.value
        }
        this._options.push(option)
      })
    } else {
      this._options = this.options
      this._options.forEach((option) => {
        if (option.selected && !this.value) {
          this.value = option.value
        }
      })
    }
  }

  // Support native Select method `add`
  public add(item: HTMLOptionElement, before?: HTMLOptionElement | number) {
    this.inputRef.value?.add(item, before)
    this._options.push({
      value: item.value || item.text,
      label: item.text || item.value,
      selected: item.selected,
      disabled: item.disabled,
    })
    if (item.selected) {
      this.value = item.value || item.text
      this.selectedIndex = this.returnNumberOrNull(this.inputRef.value?.selectedIndex)
      this.selectedOptions = this.inputRef.value?.selectedOptions
    }
    this.requestUpdate()
  }

  // Support native Select method `remove`
  public remove(item?: number) {
    if (typeof item === 'number') {
      if (this.selectedIndex === item) {
        this.value = this._options[0]?.value || ''
      }
      this.inputRef.value?.remove(item)
    }
  }

  // Support native Select method `item`
  public item(index: number) {
    return this.inputRef.value?.item(index)
  }

  // Support native Select method `namedItem`
  public namedItem(name: string) {
    return this.inputRef.value?.namedItem(name)
  }

  // Support native Select method `showPicker`
  public showPicker() {
    this.inputRef.value?.showPicker()
  }

  public attributeChangedCallback(name: string, _old: string, value: string): void {
    if (name === 'options') {
      this._options = value ? JSON.parse(value) : []
    }
    if (name === 'value' && this.value !== _old) {
      this.selectedIndex = this.touched
        ? this.returnNumberOrNull(this.inputRef.value?.selectedIndex)
        : this.options.findIndex((option) => option.value === value)
      this.selectedOptions = this.inputRef.value?.selectedOptions
      this.valueChanged(value, _old)
    }
    super.attributeChangedCallback(name, _old, value)
  }

  update(changedProperties: PropertyValues) {
    super.update(changedProperties)
    if (changedProperties.has('value') && this.value !== changedProperties.get('value')) {
      this.selectedIndex = this.touched
        ? this.returnNumberOrNull(this.inputRef.value?.selectedIndex)
        : this.options.findIndex((option) => option.value === this.value)
      this.selectedOptions = this.inputRef.value?.selectedOptions
      this.valueChanged(this.value, changedProperties.get('value'))
    }
    if (changedProperties.has('id')) {
      !this.name && this.id && (this.name = this.id)
    }
  }

  protected firstUpdated(_changedProperties: PropertyValues): void {
    super.firstUpdated(_changedProperties)
    if (this.options.length) {
      this._options = this.options
    }
    // If no options are selected, set value and selectedIndex to the first option
    if (!this.value && this._options.length > 0) {
      this.value = this._options[0].value
      this.selectedIndex = 0
    } else {
      this.selectedIndex = this._options.findIndex((option) => option.value === this.value)
    }
    this.selectedOptions = this.inputRef.value?.selectedOptions
  }

  render() {
    const inputClass = `pkt-input ${this.fullwidth ? 'pkt-input--fullwidth' : ''}`

    return html`
      <pkt-input-wrapper
        ?counter=${this.counter}
        ?disabled=${this.disabled}
        ?hasError=${this.hasError}
        ?hasFieldset=${this.hasFieldset}
        ?inline=${this.inline}
        ?optionalTag=${this.optionalTag}
        ?requiredTag=${this.requiredTag}
        ?useWrapper=${this.useWrapper}
        ariaDescribedBy=${ifDefined(this.ariaDescribedBy)}
        class="pkt-select"
        errorMessage=${ifDefined(this.errorMessage)}
        forId=${this.id + '-input'}
        helptext=${ifDefined(this.helptext)}
        helptextDropdown=${ifDefined(this.helptextDropdown)}
        helptextDropdownButton=${ifDefined(this.helptextDropdownButton)}
        label=${ifDefined(this.label)}
        optionalText=${ifDefined(this.optionalText)}
        requiredText=${ifDefined(this.requiredText)}
        tagText=${ifDefined(this.tagText)}
      >
        <select
          class=${inputClass}
          aria-invalid=${this.hasError}
          aria-errormessage=${`${this.id}-error`}
          aria-labelledby=${ifDefined(this.ariaLabelledby)}
          ?disabled=${this.disabled}
          id=${this.id + '-input'}
          name=${(this.name || this.id) + '-input'}
          value=${this.value}
          @change=${(e: Event) => {
            this.touched = true
            this.value = (e.target as HTMLSelectElement).value
            e.stopImmediatePropagation()
          }}
          @input=${(e: Event) => {
            this.onInput()
            e.stopImmediatePropagation()
          }}
          @focus=${(e: FocusEvent) => {
            this.onFocus()
            e.stopImmediatePropagation()
          }}
          @blur=${(e: FocusEvent) => {
            this.onBlur()
            e.stopImmediatePropagation()
          }}
          ${ref(this.inputRef)}
        >
          ${this._options.length > 0 &&
          this._options.map(
            (option) =>
              html`<option
                value=${option.value}
                ?selected=${this.value == option.value || option.selected}
                ?disabled=${option.disabled}
                ?hidden=${option.hidden}
              >
                ${option.label}
              </option>`,
          )}
        </select>
        <div class="pkt-contents" ${ref(this.helptextSlot)} name="helptext" slot="helptext"></div>
      </pkt-input-wrapper>
    `
  }

  private returnNumberOrNull(value: number | null | undefined): number | null {
    if (value === undefined || value === null || isNaN(value)) {
      return null
    }
    return value
  }
}
