import { html, PropertyValues } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { Ref, createRef, ref } from 'lit/directives/ref.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { PktOptionsInputElement } from '@/base-elements/options-input-element'
import { PktOptionsSlotController } from '@/controllers/pkt-options-controller'
import { slotContent } from '@/directives/slot-content'
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
  }
}

export class PktSelect extends PktOptionsInputElement<{}, TSelectOption> implements IPktSelect {
  inputRef: Ref<HTMLSelectElement> = createRef()

  @property({ type: String }) value: string = ''

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

  constructor() {
    super()
    this.optionsController = new PktOptionsSlotController(this)
  }

  connectedCallback(): void {
    super.connectedCallback()

    // Parse options from props or slots
    this.parseOptions()

    // Set initial value from selected option
    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) {
    const newOption = {
      value: item.value || item.text,
      label: item.text || item.value,
      selected: item.selected,
      disabled: item.disabled,
    }

    // Add to our internal options array at the correct position
    if (before === undefined) {
      this._options.push(newOption)
    } else if (typeof before === 'number') {
      this._options.splice(before, 0, newOption)
    } else {
      // If before is an HTMLOptionElement, find its index in our options
      const beforeValue = before.value || before.text
      const beforeIndex = this._options.findIndex((opt) => opt.value === beforeValue)
      if (beforeIndex >= 0) {
        this._options.splice(beforeIndex, 0, newOption)
      } else {
        this._options.push(newOption)
      }
    }

    if (item.selected) {
      this.value = item.value || item.text
      this.selectedIndex = this._options.findIndex((opt) => opt.value === this.value)
    }
    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._options.splice(item, 1)
      this.requestUpdate()
    }
  }

  // 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() {
    if (this.inputRef.value && 'showPicker' in this.inputRef.value) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any -- showPicker is not in TypeScript's HTMLSelectElement type yet
      ;(this.inputRef.value as any).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('_optionsProp') && this._optionsProp.length > 0) {
      this._options = this._optionsProp
      this.requestUpdate('_options')

      // If no value is set and we have options, set to first option
      if (!this.value && this._options.length > 0) {
        // eslint-disable-next-line lit/no-property-change-update -- Initial value setup is intentional
        this.value = this._options[0].value
        this.selectedIndex = 0
      }
    }
    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._optionsProp.length) {
      this._options = this._optionsProp
    }
    // 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" slot="helptext">${slotContent(this, 'helptext')}</div>
      </pkt-input-wrapper>
    `
  }

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

try {
  customElement('pkt-select')(PktSelect)
} catch (e) {
  console.warn('Forsøker å definere <pkt-select>, men den er allerede definert')
}
