import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { customElement, property, state } from 'lit/decorators.js'
import { formatISODate, fromISOToDate, fromISOtoLocal, newDate } from '@/utils/dateutils'
import { html, nothing, PropertyValues } from 'lit'
import { PktCalendar } from '@/components/calendar/calendar'
import { PktInputElement } from '@/base-elements/input-element'
import { Ref, createRef, ref } from 'lit/directives/ref.js'
import { repeat } from 'lit/directives/repeat.js'
import converters from '@/helpers/converters'
import specs from 'componentSpecs/datepicker.json'
import '@/components/calendar'
import '@/components/icon'
import '@/components/input-wrapper'
import '@/components/tag'
import { PktSlotController } from '@/controllers/pkt-slot-controller'

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
@customElement('pkt-datepicker')
export class PktDatepicker extends PktInputElement {
  /**
   * Element attributes and properties
   */
  @property({ type: String, reflect: true })
  value: string | string[] = ''

  @property({ type: Array })
  _value: string[] = this.value
    ? !Array.isArray(this.value)
      ? this.value.split(',')
      : this.value
    : []

  @property({ type: String, reflect: true })
  label: string = 'Datovelger'

  @property({ type: String })
  dateformat: string = specs.props.dateformat.default

  @property({ type: Boolean, reflect: true })
  multiple: boolean = specs.props.multiple.default

  @property({ type: Number, reflect: true })
  maxlength: number | null = null

  @property({ type: Boolean, reflect: true })
  range: boolean = specs.props.range.default

  @property({ type: Boolean })
  showRangeLabels: boolean = false

  @property({ type: String, reflect: true })
  min: string | null = null

  @property({ type: String, reflect: true })
  max: string | null = null

  @property({ type: Boolean })
  weeknumbers: boolean = specs.props.weeknumbers.default

  @property({ type: Boolean, reflect: true })
  withcontrols: boolean = specs.props.withcontrols.default

  @property({ converter: converters.csvToArray })
  excludedates: string[] = []

  @property({ converter: converters.csvToArray })
  excludeweekdays: string[] = []

  @property({ type: String })
  currentmonth: string | null = null

  @property({ type: Boolean, reflect: true })
  calendarOpen: boolean = false

  @property({ type: String })
  timezone: string = 'Europe/Oslo'

  @state() inputClasses = {}
  @state() buttonClasses = {}

  /**
   * Housekeeping / lifecycle methods
   */

  constructor() {
    super()
    this.slotController = new PktSlotController(this, this.helptextSlot)
  }

  async connectedCallback() {
    super.connectedCallback()

    const ua = navigator.userAgent
    const isIOS = /iP(hone|od|ad)/.test(ua)

    this.inputType = isIOS ? 'text' : 'date'

    document &&
      document.body.addEventListener('click', (e: MouseEvent) => {
        if (
          this.inputRef?.value &&
          this.btnRef?.value &&
          !this.inputRef.value.contains(e.target as Node) &&
          !(this.inputRefTo.value && this.inputRefTo.value.contains(e.target as Node)) &&
          !this.btnRef.value.contains(e.target as Node) &&
          !(e.target as Element).closest('.pkt-calendar-popup') &&
          this.calendarOpen
        ) {
          this.onBlur()
          this.hideCalendar()
        }
      })

    if (this.value.length && this._value.length === 0) {
      this._value = !Array.isArray(this.value) ? this.value.split(',') : this.value
    }
    this.min = this.min || specs.props.min.default
    this.max = this.max || specs.props.max.default

    if (typeof this.excludedates === 'string') {
      this.excludedates = (this.excludedates as unknown as string).split(',')
    }

    if (typeof this.excludeweekdays === 'string') {
      this.excludeweekdays = (this.excludeweekdays as unknown as string).split(',')
    }

    if ((this.multiple || this.range) && this.name && !this.name.endsWith('[]')) {
      this.name = this.name + '[]'
    }

    if (this.calendarOpen) {
      await sleep(20)
      this.handleCalendarPosition()
    }
  }

  disconnectedCallback(): void {
    super.disconnectedCallback()
    document &&
      document.body.removeEventListener('click', (e: MouseEvent) => {
        if (
          this.inputRef?.value &&
          this.btnRef?.value &&
          !this.inputRef.value.contains(e.target as Node) &&
          !this.btnRef.value.contains(e.target as Node)
        ) {
          this.hideCalendar()
        }
      })
  }

  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
    if (name === 'value') {
      if (this.range && value?.split(',').length === 1) return
      if (this.value !== _old) this.valueChanged(value, _old)
    }

    if (name === 'excludedates' && typeof this.excludedates === 'string') {
      this.excludedates = value?.split(',') ?? []
    }

    if (name === 'excludeweekdays' && typeof this.excludeweekdays === 'string') {
      this.excludeweekdays = value?.split(',') ?? []
    }
    super.attributeChangedCallback(name, _old, value)
  }

  updated(changedProperties: PropertyValues): void {
    if (changedProperties.has('value')) {
      if (this.range && this.value.length === 1) return
      this.valueChanged(this.value, changedProperties.get('value'))
    }
    super.updated(changedProperties)
  }

  /**
   * Element references
   */

  // When using PktInputElement, we always need to define `inputRef`
  inputRef: Ref<HTMLInputElement> = createRef()
  inputRefTo: Ref<HTMLInputElement> = createRef()
  btnRef: Ref<HTMLButtonElement> = createRef()
  calRef: Ref<PktCalendar> = createRef()
  popupRef: Ref<HTMLDivElement> = createRef()
  helptextSlot: Ref<HTMLElement> = createRef()

  /**
   * Rendering
   */
  renderInput() {
    return html`
      <input
        class="${classMap(this.inputClasses)}"
        .type=${this.inputType}
        id="${this.id}-input"
        .value=${this._value[0] ?? ''}
        min=${ifDefined(this.min)}
        max=${ifDefined(this.max)}
        @click=${(e: MouseEvent) => {
          e.preventDefault()
          this.showCalendar()
        }}
        ?disabled=${this.disabled}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === ',') {
            this.inputRef.value?.blur()
          }
          if (e.key === 'Space' || e.key === ' ') {
            e.preventDefault()
            this.toggleCalendar(e)
          }
          if (e.key === 'Enter') {
            const form = this.internals.form as HTMLFormElement
            if (form) {
              form.requestSubmit()
            } else {
              this.inputRef.value?.blur()
            }
          }
        }}
        @input=${(e: Event) => {
          this.onInput()
          e.stopImmediatePropagation()
        }}
        @focus=${() => {
          this.onFocus()
          if (this.isMobileSafari) {
            this.showCalendar()
          }
        }}
        @blur=${(e: FocusEvent) => {
          if (!this.calRef.value?.contains(e.relatedTarget as Node)) {
            this.onBlur()
          }
          this.manageValidity(e.target as HTMLInputElement)
          this.value = (e.target as HTMLInputElement).value
        }}
        @change=${(e: Event) => {
          e.stopImmediatePropagation()
        }}
        ${ref(this.inputRef)}
      />
    `
  }

  renderRangeInput() {
    const rangeLabelClasses = {
      'pkt-input-prefix': this.showRangeLabels,
      'pkt-hide': !this.showRangeLabels,
    }
    return html`
      ${this.showRangeLabels
        ? html` <div class="pkt-input-prefix">${this.strings.generic.from}</div> `
        : nothing}
      <input
        class=${classMap(this.inputClasses)}
        .type=${this.inputType}
        id="${this.id}-input"
        .value=${this._value[0] ?? ''}
        min=${ifDefined(this.min)}
        max=${ifDefined(this.max)}
        ?disabled=${this.disabled}
        @click=${(e: MouseEvent) => {
          e.preventDefault()
          this.showCalendar()
        }}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === ',') {
            this.inputRef.value?.blur()
          }
          if (e.key === 'Space' || e.key === ' ') {
            e.preventDefault()
            this.toggleCalendar(e)
          }
          if (e.key === 'Enter') {
            const form = this.internals.form as HTMLFormElement
            if (form) {
              form.requestSubmit()
            } else {
              this.inputRefTo.value?.focus()
            }
          }
        }}
        @input=${(e: Event) => {
          this.onInput()
          e.stopImmediatePropagation()
        }}
        @focus=${() => {
          this.onFocus()
          if (this.isMobileSafari) {
            this.showCalendar()
          }
        }}
        @blur=${(e: Event) => {
          if ((e.target as HTMLInputElement).value) {
            this.manageValidity(e.target as HTMLInputElement)
            const date = fromISOToDate((e.target as HTMLInputElement).value)
            if (date) {
              if (this._value[0] !== (e.target as HTMLInputElement).value && this._value[1]) {
                this.clearInputValue()
                this.calRef?.value?.handleDateSelect(date)
              }
            }
          } else if (this._value[0]) {
            this.clearInputValue()
          }
        }}
        @change=${(e: Event) => {
          e.stopImmediatePropagation()
        }}
        ${ref(this.inputRef)}
      />
      <div class="${classMap(rangeLabelClasses)}" id="${this.id}-to-label">
        ${this.strings.generic.to}
      </div>
      ${!this.showRangeLabels ? html` <div class="pkt-input-separator">–</div> ` : nothing}
      <input
        class=${classMap(this.inputClasses)}
        .type=${this.inputType}
        id="${this.id}-to"
        aria-labelledby="${this.id}-to-label"
        .value=${this._value[1] ?? ''}
        min=${ifDefined(this.min)}
        max=${ifDefined(this.max)}
        ?disabled=${this.disabled}
        @click=${(e: MouseEvent) => {
          e.preventDefault()
          this.showCalendar()
        }}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === ',') {
            this.inputRefTo.value?.blur()
          }
          if (e.key === 'Space' || e.key === ' ') {
            e.preventDefault()
            this.toggleCalendar(e)
          }
          if (e.key === 'Enter') {
            const form = this.internals.form as HTMLFormElement
            if (form) {
              form.requestSubmit()
            } else {
              this.inputRefTo.value?.blur()
            }
          }
        }}
        @input=${(e: Event) => {
          this.onInput()
          e.stopImmediatePropagation()
        }}
        @focus=${() => {
          this.onFocus()
          if (this.isMobileSafari) {
            this.showCalendar()
          }
        }}
        @blur=${(e: FocusEvent) => {
          if (!this.calRef.value?.contains(e.relatedTarget as Node)) {
            this.onBlur()
          }
          if ((e.target as HTMLInputElement).value) {
            this.manageValidity(e.target as HTMLInputElement)
            const val = (e.target as HTMLInputElement).value
            if (this.min && this.min > val) {
              this.internals.setValidity(
                { rangeUnderflow: true },
                this.strings.forms.messages.rangeUnderflow,
                e.target as HTMLInputElement,
              )
            } else if (this.max && this.max < val) {
              this.internals.setValidity(
                { rangeOverflow: true },
                this.strings.forms.messages.rangeOverflow,
                e.target as HTMLInputElement,
              )
            }
            const date = fromISOToDate((e.target as HTMLInputElement).value)
            if (date) {
              if (this._value[1] !== formatISODate(date)) {
                this.calRef?.value?.handleDateSelect(date)
              }
            }
          }
        }}
        @change=${(e: Event) => {
          e.stopImmediatePropagation()
        }}
        ${ref(this.inputRefTo)}
      />
    `
  }

  renderMultipleInput() {
    return html`
      <input
        class=${classMap(this.inputClasses)}
        .type=${this.inputType}
        id="${this.id}-input"
        min=${ifDefined(this.min)}
        max=${ifDefined(this.max)}
        ?disabled=${this.disabled || (this.maxlength && this._value.length >= this.maxlength)}
        @click=${(e: MouseEvent) => {
          e.preventDefault()
          this.showCalendar()
        }}
        @blur=${(e: FocusEvent) => {
          if (!this.calRef.value?.contains(e.relatedTarget as Node)) {
            this.onBlur()
          }
          this.addToSelected(e)
        }}
        @input=${(e: Event) => {
          this.onInput()
          e.stopImmediatePropagation()
        }}
        @focus=${() => {
          this.onFocus()
          if (this.isMobileSafari) {
            this.showCalendar()
          }
        }}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === ',') {
            e.preventDefault()
            this.addToSelected(e)
          }
          if (e.key === 'Space' || e.key === ' ') {
            e.preventDefault()
            this.toggleCalendar(e)
          }
          if (e.key === 'Enter') {
            const form = this.internals.form as HTMLFormElement
            if (form) {
              form.requestSubmit()
            } else {
              this.inputRef.value?.blur()
            }
          }
        }}
        @change=${(e: Event) => {
          e.stopImmediatePropagation()
        }}
        ${ref(this.inputRef)}
      />
    `
  }

  renderTags() {
    return html`
      <div class="pkt-datepicker__tags" aria-live="polite">
        ${!!this._value[0]
          ? repeat(
              this._value ?? [],
              (date) => date,
              (date) => html`
                <pkt-tag
                  .id="${this.id + date + '-tag'}"
                  closeTag
                  ariaLabel="${this.strings.calendar.deleteDate} ${fromISOtoLocal(
                    date,
                    this.dateformat,
                  )}"
                  @close=${() => this.calRef.value?.handleDateSelect(fromISOToDate(date))}
                  ><time datetime="${date}">${fromISOtoLocal(date, this.dateformat)}</time></pkt-tag
                >
              `,
            )
          : nothing}
      </div>
    `
  }

  renderCalendar() {
    return html`<div
      class="pkt-calendar-popup pkt-${this.calendarOpen ? 'show' : 'hide'}"
      @focusout=${(e: FocusEvent) => {
        if (this.calendarOpen) this.handleFocusOut(e)
      }}
      id="${this.id}-popup"
      ${ref(this.popupRef)}
    >
      <pkt-calendar
        id="${this.id}-calendar"
        ?multiple=${this.multiple}
        ?range=${this.range}
        ?weeknumbers=${this.weeknumbers}
        ?withcontrols=${this.withcontrols}
        .maxMultiple=${this.maxlength}
        .selected=${this._value}
        .earliest=${this.min}
        .latest=${this.max}
        .excludedates=${Array.isArray(this.excludedates)
          ? this.excludedates
          : (this.excludedates as string).split(',')}
        .excludeweekdays=${this.excludeweekdays}
        .currentmonth=${this.currentmonth ? newDate(this.currentmonth) : null}
        @date-selected=${(e: CustomEvent) => {
          this.value = !this.multiple && !this.range ? e.detail[0] : e.detail
          this._value = e.detail
          if (this.inputRef.value) {
            if (this.range && this.inputRefTo.value) {
              this.inputRef.value.value = this._value[0] ?? ''
              this.inputRefTo.value.value = this._value[1] ?? ''
            } else if (!this.multiple) {
              this.inputRef.value.value = this._value.length ? this._value[0] : ''
            }
          }
        }}
        @close=${() => {
          this.onBlur()
          this.hideCalendar()
        }}
        ${ref(this.calRef)}
      ></pkt-calendar>
    </div>`
  }

  render() {
    this.inputClasses = {
      'pkt-input': true,
      'pkt-datepicker__input': true,
      'pkt-input--fullwidth': this.fullwidth,
      'pkt-datepicker--hasrangelabels': this.showRangeLabels,
      'pkt-datepicker--multiple': this.multiple,
      'pkt-datepicker--range': this.range,
    }

    this.buttonClasses = {
      'pkt-input-icon': true,
      'pkt-btn': true,
      'pkt-btn--icon-only': true,
      'pkt-btn--tertiary': true,
    }

    return html`
      <pkt-input-wrapper
        label="${this.label}"
        forId="${this.id}-input"
        ?counter=${this.multiple && !!this.maxlength}
        .counterCurrent=${this.value ? this._value.length : 0}
        .counterMaxLength=${this.maxlength}
        ?disabled=${this.disabled}
        ?hasError=${this.hasError}
        ?hasFieldset=${this.hasFieldset}
        ?inline=${this.inline}
        ?required=${this.required}
        ?optionalTag=${this.optionalTag}
        ?requiredTag=${this.requiredTag}
        ?useWrapper=${this.useWrapper}
        .optionalText=${this.optionalText}
        .requiredText=${this.requiredText}
        .tagText=${this.tagText}
        .errorMessage=${this.errorMessage}
        .helptext=${this.helptext}
        .helptextDropdown=${this.helptextDropdown}
        .helptextDropdownButton=${this.helptextDropdownButton}
        .ariaDescribedBy=${this.ariaDescribedBy}
        class="pkt-datepicker"
      >
        <div class="pkt-contents" ${ref(this.helptextSlot)} name="helptext" slot="helptext"></div>
        ${this.multiple ? this.renderTags() : nothing}
        <div
          class="pkt-datepicker__inputs ${this.range && this.showRangeLabels
            ? 'pkt-input__range-inputs'
            : ''}"
        >
          <div class="pkt-input__container">
            ${this.range
              ? this.renderRangeInput()
              : this.multiple
                ? this.renderMultipleInput()
                : this.renderInput()}
            <button
              class="${classMap(this.buttonClasses)}"
              type="button"
              @click=${this.toggleCalendar}
              ?disabled=${this.disabled}
              ${ref(this.btnRef)}
            >
              <pkt-icon name="calendar"></pkt-icon>
              <span class="pkt-btn__text">${this.strings.calendar.buttonAltText}</span>
            </button>
          </div>
        </div>
      </pkt-input-wrapper>
      ${this.renderCalendar()}
    `
  }

  /**
   * Handlers
   */

  handleCalendarPosition() {
    if (this.popupRef.value && this.inputRef.value) {
      const counter = this.multiple && !!this.maxlength

      const inputRect =
        this.inputRef.value.parentElement?.getBoundingClientRect() ||
        this.inputRef.value.getBoundingClientRect()

      const inputHeight = counter ? inputRect.height + 30 : inputRect.height
      const popupHeight = this.popupRef.value.getBoundingClientRect().height

      let top = counter ? 'calc(100% - 30px)' : '100%'
      if (
        inputRect &&
        inputRect.top + popupHeight > window.innerHeight &&
        inputRect.top - popupHeight > 0
      ) {
        top = `calc(100% - ${inputHeight}px - ${popupHeight}px)`
      }
      this.popupRef.value.style.top = top
    }
  }

  addToSelected = (e: Event | KeyboardEvent) => {
    const target = e.target as HTMLInputElement
    if (!target.value) return
    const minAsDate = this.min ? newDate(this.min as string) : null
    const maxAsDate = this.max ? newDate(this.max as string) : null
    const date = newDate(target.value.split(',')[0])
    if (
      date &&
      !isNaN(date.getTime()) &&
      (!minAsDate || date >= minAsDate) &&
      (!maxAsDate || date <= maxAsDate) &&
      this.calRef.value
    ) {
      this.calRef.value.handleDateSelect(date)
    }
    target.value = ''
  }

  private handleFocusOut(e: FocusEvent) {
    if (!this.contains(e.target as Node)) {
      this.onBlur()
      this.hideCalendar()
    }
  }

  public async showCalendar() {
    this.calendarOpen = true
    await sleep(20)
    this.handleCalendarPosition()
    if (this.isMobileSafari) {
      this.calRef.value?.focusOnCurrentDate()
    }
  }

  public hideCalendar() {
    this.calendarOpen = false
  }

  public async toggleCalendar(e: Event) {
    e.preventDefault()
    this.calendarOpen ? this.hideCalendar() : this.showCalendar()
  }
}
