import { classMap } from 'lit/directives/class-map.js'
import { customElement, property, state } from 'lit/decorators.js'
import {
  formatISODate,
  newDate,
  newDateYMD,
  formatReadableDate,
  parseISODateString,
  todayInTz,
} from '@/utils/dateutils'
import { getWeek, eachDayOfInterval, getISODay, addDays } from 'date-fns'
import { html, nothing, PropertyValues } from 'lit'
import { PktElement } from '@/base-elements/element'
import converters from '../../helpers/converters'
import specs from 'componentSpecs/calendar.json'
import '@/components/icon'

type DatesInRange = {
  [key: string]: boolean
}

@customElement('pkt-calendar')
export class PktCalendar extends PktElement {
  /**
   * Element attributes
   */
  @property({ type: Boolean })
  multiple: boolean = specs.props.multiple.default

  @property({ type: Number })
  maxMultiple: number = specs.props.maxMultiple.default

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

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

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

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

  @property({ type: String })
  earliest: string | null = specs.props.earliest.default

  @property({ type: String })
  latest: string | null = specs.props.latest.default

  @property({ converter: converters.stringsToDate })
  excludedates: Date[] = []

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

  @property({ converter: converters.stringToDate })
  currentmonth: Date | null = null

  /**
   * Strings
   */
  @property({ type: Array }) dayStrings: string[] = this.strings.dates.daysShort
  @property({ type: Array }) dayStringsLong: string[] = this.strings.dates.days
  @property({ type: Array }) monthStrings: string[] = this.strings.dates.months
  @property({ type: String }) weekString: string = this.strings.dates.week
  @property({ type: String }) prevMonthString: string = this.strings.dates.prevMonth
  @property({ type: String }) nextMonthString: string = this.strings.dates.nextMonth

  /**
   * Private properties
   */
  @property({ type: Array }) private _selected: Date[] = []
  @property({ type: Number }) private year: number = 0
  @property({ type: Number }) private month: number = 0
  @property({ type: Number }) private week: number = 0
  @property({ type: Date }) private rangeHovered: Date | null = null

  @state() private inRange: DatesInRange = {}
  @state() private focusedDate: string | null = null
  @state() private selectableDates: {
    currentDateISO: string
    isDisabled: boolean
    tabindex: string
  }[] = []
  @state() private currentmonthtouched: boolean = false
  @state() private tabIndexSet: number = 0
  /**
   * Runs on mount, used to set up various values and whatever you need to run
   */
  connectedCallback() {
    super.connectedCallback()
  }

  disconnectedCallback(): void {
    this.removeEventListener('keydown', this.handleKeydown)
    super.disconnectedCallback()
  }

  attributeChangedCallback(name: string, _old: string | null, value: string | null): void {
    if (name === 'selected' && value) {
      this.convertSelected()
    }
    super.attributeChangedCallback(name, _old, value)
  }

  updated(changedProperties: PropertyValues): void {
    super.updated(changedProperties)
    if (changedProperties.has('selected')) {
      this.convertSelected()
    }
  }

  protected firstUpdated(_changedProperties: PropertyValues): void {
    this.addEventListener('keydown', this.handleKeydown)
  }

  convertSelected() {
    if (typeof this.selected === 'string') {
      this.selected = this.selected.split(',')
    }
    if (this.selected.length === 1 && this.selected[0] === '') {
      this.selected = []
    }
    this._selected = this.selected.map((d: string) => parseISODateString(d))
    if (this.range && this.selected.length === 2) {
      const days = eachDayOfInterval({
        start: this._selected[0],
        end: this._selected[1],
      })

      this.inRange = {}
      if (Array.isArray(days) && days.length) {
        const inRange: DatesInRange = {}
        for (let i = 0; i < days.length; i++) {
          inRange[formatISODate(days[i])] = this.isInRange(days[i])
        }
        this.inRange = inRange
      }
    }
    this.setCurrentMonth()
  }

  setCurrentMonth() {
    if (this.currentmonth === null && !this.currentmonthtouched) {
      this.currentmonthtouched = true
      return
    }
    if (this.selected.length && this.selected[0] !== '') {
      const d = parseISODateString(this.selected[this.selected.length - 1])
      this.currentmonth = isNaN(d.getTime()) ? new Date() : d
    } else if (this.currentmonth === null) {
      this.currentmonth = new Date()
    }
    // fallback to today if invalid
    if (!this.currentmonth || isNaN(this.currentmonth.getTime())) {
      this.currentmonth = new Date()
    }
    this.year = this.currentmonth.getFullYear()
    this.month = this.currentmonth.getMonth()
  }

  handleKeydown(e: KeyboardEvent) {
    switch (e.key) {
      case 'ArrowLeft':
        this.handleArrowKey(e, -1)
        break
      case 'ArrowRight':
        this.handleArrowKey(e, 1)
        break
      case 'ArrowUp':
        this.handleArrowKey(e, -7)
        break
      case 'ArrowDown':
        this.handleArrowKey(e, 7)
        break
      default:
        break
    }
  }

  handleArrowKey(e: KeyboardEvent, direction: number) {
    if ((e.target as HTMLElement)?.nodeName === 'INPUT') return
    if ((e.target as HTMLElement)?.nodeName === 'SELECT') return
    if ((e.target as HTMLElement)?.nodeName === 'BUTTON') return
    e.preventDefault()
    if (!this.focusedDate) {
      this.focusOnCurrentDate()
    }
    const date = this.focusedDate ? newDate(this.focusedDate) : newDateYMD(this.year, this.month, 1)
    let nextDate = addDays(date, direction)
    if (nextDate) {
      let el = this.querySelector(`div[data-date="${formatISODate(nextDate)}"]`)
      if (el instanceof HTMLDivElement) {
        if (el.dataset.disabled) {
          nextDate = addDays(nextDate, direction)
          let nextElement = this.querySelector(`div[data-date="${formatISODate(nextDate)}"]`)
          while (
            nextElement &&
            nextElement instanceof HTMLDivElement &&
            nextElement.dataset.disabled
          ) {
            nextDate = addDays(nextDate, direction)
            nextElement = this.querySelector(`div[data-date="${formatISODate(nextDate)}"]`)
          }
          el = nextElement
        }
        if (el instanceof HTMLDivElement && !el.dataset.disabled) {
          this.focusedDate = formatISODate(nextDate)
          el.focus()
        }
      }
    }
  }
  /**
   * Component functionality and render
   */
  render() {
    return html`
      <div
        class="pkt-calendar ${this.weeknumbers ? 'pkt-cal-weeknumbers' : nothing}"
        @focusout=${this.closeEvent}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === 'Escape') {
            e.preventDefault()
            this.close()
          }
        }}
      >
        <nav class="pkt-cal-month-nav">
          <div>
            <button
              type="button"
              @click=${this.isPrevMonthAllowed() && this.prevMonth}
              @keydown=${(e: KeyboardEvent) => {
                if (e.key === 'Enter' || e.key === ' ') {
                  e.preventDefault()
                  this.isNextMonthAllowed() && this.prevMonth()
                }
              }}
              class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only ${this.isPrevMonthAllowed()
                ? ''
                : 'pkt-hide'}"
              .data-disabled=${!this.isPrevMonthAllowed() ? 'disabled' : nothing}
              ?aria-disabled=${!this.isPrevMonthAllowed()}
              tabindex=${this.isPrevMonthAllowed() ? '0' : '-1'}
            >
              <pkt-icon class="pkt-btn__icon" name="chevron-thin-left"></pkt-icon>
              <span class="pkt-btn__text">${this.prevMonthString}</span>
            </button>
          </div>
          ${this.renderMonthNav()}
          <div>
            <button
              type="button"
              @click=${this.isNextMonthAllowed() && this.nextMonth}
              @keydown=${(e: KeyboardEvent) => {
                if (e.key === 'Enter' || e.key === ' ') {
                  e.preventDefault()
                  this.isNextMonthAllowed() && this.nextMonth()
                }
              }}
              class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only ${this.isNextMonthAllowed()
                ? ''
                : 'pkt-hide'}"
              .data-disabled=${!this.isNextMonthAllowed() ? 'disabled' : nothing}
              ?aria-disabled=${!this.isNextMonthAllowed()}
              tabindex=${this.isNextMonthAllowed() ? '0' : '-1'}
            >
              <pkt-icon class="pkt-btn__icon" name="chevron-thin-right"></pkt-icon>
              <span class="pkt-btn__text">${this.nextMonthString}</span>
            </button>
          </div>
        </nav>
        <table
          class="pkt-cal-days pkt-txt-12-medium"
          role="grid"
          ?aria-multiselectable=${this.range || this.multiple}
        >
          <thead>
            ${this.renderDayNames()}
          </thead>
          <tbody>
            ${this.renderCalendarBody()}
          </tbody>
        </table>
      </div>
    `
  }

  private renderDayNames() {
    const days = []
    if (this.weeknumbers) {
      days.push(html`<th><div>${this.weekString}</div></th>`)
    }
    for (let i = 0; i < this.dayStrings.length; i++) {
      days.push(
        html`<th><div aria-label="${this.dayStringsLong[i]}">${this.dayStrings[i]}</div></th>`,
      )
    }
    return html`<tr class="pkt-cal-week-row">
      ${days}
    </tr>`
  }

  private renderMonthNav() {
    let monthView = []
    if (this.withcontrols) {
      monthView.push(
        html`<div class="pkt-cal-month-picker">
          <label for="${this.id}-monthnav" class="pkt-hide">${this.strings.dates.month}</label>
          <select
            aria-label="${this.strings.dates.month}"
            class="pkt-input pkt-input-compact"
            id="${this.id}-monthnav"
            @change=${(e: any) => {
              e.stopImmediatePropagation()
              this.changeMonth(this.year, e.target.value)
            }}
          >
            ${this.monthStrings.map(
              (month, index) =>
                html`<option value=${index} ?selected=${this.month === index}>${month}</option>`,
            )}
          </select>
          <label for="${this.id}-yearnav" class="pkt-hide">${this.strings.dates.year}</label>
          <input
            aria-label="${this.strings.dates.year}"
            class="pkt-input pkt-cal-input-year pkt-input-compact"
            id="${this.id}-yearnav"
            type="number"
            size="4"
            placeholder="0000"
            @change=${(e: any) => {
              e.stopImmediatePropagation()
              this.changeMonth(e.target.value, this.month)
            }}
            .value=${this.year}
          />
        </div> `,
      )
    } else {
      monthView.push(
        html`<div class="pkt-txt-16-medium" aria-live="polite">
          ${this.monthStrings[this.month]} ${this.year}
        </div>`,
      )
    }
    return monthView
  }

  private renderDayView(dayCounter: number, today: Date, j: number) {
    const currentDate = newDateYMD(this.year, this.month, dayCounter)
    const currentDateISO = formatISODate(currentDate)
    const isToday = currentDateISO === formatISODate(today)
    const isSelected = this.selected.includes(currentDateISO)
    const ariaLabel = formatReadableDate(currentDate)
    const isDisabled =
      this.isExcluded(j, currentDate) ||
      (!isSelected &&
        this.multiple &&
        this.maxMultiple > 0 &&
        this.selected.length >= this.maxMultiple)
    const tabindex = this.focusedDate
      ? this.focusedDate === currentDateISO && !isDisabled
        ? '0'
        : '-1'
      : !isDisabled && this.tabIndexSet === 0
        ? '0'
        : this.tabIndexSet === dayCounter
          ? '0'
          : '-1'

    if (tabindex === '0') {
      this.tabIndexSet = dayCounter
    }

    this.selectableDates.push({ currentDateISO, isDisabled, tabindex })

    const classes = {
      'pkt-cal-today': isToday,
      'pkt-cal-selected': isSelected,
      'pkt-cal-in-range': this.inRange[currentDateISO],
      'pkt-cal-excluded': this.isExcluded(j, currentDate),
      'pkt-cal-in-range-first':
        this.range &&
        (this.selected.length === 2 || this.rangeHovered !== null) &&
        currentDateISO === this.selected[0],
      'pkt-cal-in-range-last':
        this.range && this.selected.length === 2 && currentDateISO === this.selected[1],
      'pkt-cal-range-hover':
        this.rangeHovered !== null && currentDateISO === formatISODate(this.rangeHovered),
    }
    return html`<td class=${classMap(classes)}>
      <div
        ?aria-selected=${isSelected}
        role="gridcell"
        class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only"
        @mouseover=${() =>
          this.range && !this.isExcluded(j, currentDate) && this.handleRangeHover(currentDate)}
        @focus=${() => {
          this.range && !this.isExcluded(j, currentDate) && this.handleRangeHover(currentDate)
          this.focusedDate = currentDateISO
        }}
        aria-label="${ariaLabel}"
        tabindex=${this.selectableDates.find((x) => x.currentDateISO === currentDateISO)?.tabindex}
        data-disabled=${isDisabled ? 'disabled' : nothing}
        data-date=${currentDateISO}
        @keydown=${(e: KeyboardEvent) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault()
            this.handleDateSelect(currentDate)
          }
        }}
        @click=${(e: MouseEvent) => {
          if (!isDisabled) {
            e.preventDefault()
            this.handleDateSelect(currentDate)
          }
        }}
      >
        <span class="pkt-btn__text pkt-txt-14-light">${dayCounter}</span>
      </div>
    </td>`
  }

  private renderCalendarBody() {
    const today = todayInTz()
    const firstDayOfMonth = newDateYMD(this.year, this.month, 1)
    const lastDayOfMonth = newDateYMD(this.year, this.month + 1, 0)
    const startingDay = (firstDayOfMonth.getDay() + 6) % 7
    const numDays = lastDayOfMonth.getDate()
    const numRows = Math.ceil((numDays + startingDay) / 7)
    const lastDayOfPrevMonth = newDateYMD(this.year, this.month, 0)
    const numDaysPrevMonth = lastDayOfPrevMonth.getDate()

    let dayCounter = 1
    this.week = getWeek(newDateYMD(this.year, this.month, 1))

    const rows = []

    for (let i = 0; i < numRows; i++) {
      const cells = []

      this.weeknumbers && cells.push(html`<td class="pkt-cal-week">${this.week}</td>`)
      this.week++

      for (let j = 1; j < 8; j++) {
        if (i === 0 && j < startingDay + 1) {
          const dayFromPrevMonth = numDaysPrevMonth - (startingDay - j)
          cells.push(
            html`<td class="pkt-cal-other">
              <div
                class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only"
                data-disabled="disabled"
              >
                <span class="pkt-btn__text pkt-txt-14-light">${dayFromPrevMonth}</span>
              </div>
            </td>`,
          )
        } else if (dayCounter > numDays) {
          cells.push(
            html`<td class="pkt-cal-other">
              <div
                class="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only"
                data-disabled="disabled"
              >
                <span class="pkt-btn__text pkt-txt-14-light">${dayCounter - numDays}</span>
              </div>
            </td>`,
          )
          dayCounter++
        } else {
          cells.push(this.renderDayView(dayCounter, today, j))
          dayCounter++
        }
      }

      rows.push(
        html`<tr class="pkt-cal-week-row" role="row">
          ${cells}
        </tr>`,
      )
    }

    return rows
  }

  private isExcluded(weekday: number, date: Date) {
    if (this.excludeweekdays.includes(weekday.toString())) return true
    if (this.earliest && newDate(date, 'end') < newDate(this.earliest, 'start')) return true
    if (this.latest && newDate(date, 'start') > newDate(this.latest, 'end')) return true
    return this.excludedates.some((x: Date | string) => {
      if (typeof x === 'string') {
        return x === formatISODate(date)
      } else {
        return x.toDateString() === date.toDateString()
      }
    })
  }

  isPrevMonthAllowed() {
    const prevMonth = newDateYMD(this.year, this.month, 0)
    if (this.earliest && newDate(this.earliest) > prevMonth) return false
    return true
  }

  private prevMonth() {
    const newMonth = this.month === 0 ? 11 : this.month - 1
    const newYear = this.month === 0 ? this.year - 1 : this.year
    this.changeMonth(newYear, newMonth)
  }

  isNextMonthAllowed() {
    const nextMonth = newDateYMD(
      this.month === 11 ? this.year + 1 : this.year,
      this.month === 11 ? 0 : this.month + 1,
      1,
    )
    if (this.latest && newDate(this.latest) < nextMonth) return false
    return true
  }

  private nextMonth() {
    const newMonth = this.month === 11 ? 0 : this.month + 1
    const newYear = this.month === 11 ? this.year + 1 : this.year
    this.changeMonth(newYear, newMonth)
  }

  private changeMonth(year: number, month: number) {
    this.year = typeof year === 'string' ? parseInt(year) : year
    this.month = typeof month === 'string' ? parseInt(month) : month
    this.tabIndexSet = 0
    this.focusedDate = null
    this.selectableDates = []
  }

  private isInRange(date: Date) {
    if (this.range && this.selected.length === 2) {
      if (date > newDate(this.selected[0]) && date < newDate(this.selected[1])) return true
    } else if (this.range && this.selected.length === 1 && this.rangeHovered) {
      if (date > newDate(this.selected[0]) && date < this.rangeHovered) return true
    }
    return false
  }

  private isRangeAllowed(date: Date) {
    let allowed = true
    if (this._selected.length === 1) {
      const days = eachDayOfInterval({
        start: this._selected[0],
        end: date,
      })

      if (Array.isArray(days) && days.length) {
        for (let i = 0; i < days.length; i++) {
          this.excludedates.forEach((d: Date) => {
            if (d > this._selected[0] && d < date) {
              allowed = false
            }
          })
          if (this.excludeweekdays.includes(getISODay(days[i]).toString())) {
            allowed = false
          }
        }
      }
    }
    return allowed
  }

  private emptySelected() {
    this.selected = []
    this._selected = []
    this.inRange = {}
  }

  public addToSelected(selectedDate: Date) {
    if (this.selected.includes(formatISODate(selectedDate))) return
    this.selected = [...this.selected, formatISODate(selectedDate)]
    this._selected = [...this._selected, selectedDate]
    if (this.range && this.selected.length === 2) {
      this.close()
    }
  }

  public removeFromSelected(selectedDate: Date) {
    if (this.selected.length === 1) {
      this.emptySelected()
    } else {
      const selectedDateIndex = this.selected.indexOf(formatISODate(selectedDate))
      const selectedCopy = [...this.selected]
      const _selectedCopy = [...this._selected]
      selectedCopy.splice(selectedDateIndex, 1)
      _selectedCopy.splice(selectedDateIndex, 1)
      this.selected = selectedCopy
      this._selected = _selectedCopy
    }
  }

  public toggleSelected(selectedDate: Date) {
    const selectedDateISO = formatISODate(selectedDate)
    this.selected.includes(selectedDateISO)
      ? this.removeFromSelected(selectedDate)
      : !(this.maxMultiple && this.selected.length >= this.maxMultiple)
        ? this.addToSelected(selectedDate)
        : null
  }

  private handleRangeSelect(selectedDate: Date) {
    const selectedDateISO = formatISODate(selectedDate)
    if (this.selected.includes(selectedDateISO)) {
      if (this.selected.indexOf(selectedDateISO) === 0) {
        this.emptySelected()
      } else {
        this.removeFromSelected(selectedDate)
      }
    } else {
      if (this.selected.length > 1) {
        this.emptySelected()
        this.addToSelected(selectedDate)
      } else {
        if (this.selected.length === 1 && !this.isRangeAllowed(selectedDate)) {
          this.emptySelected()
        }
        if (this.selected.length === 1 && this._selected[0] > selectedDate) {
          this.emptySelected()
        }
        this.addToSelected(selectedDate)
      }
    }
    return Promise.resolve()
  }

  private handleRangeHover(date: Date) {
    if (
      this.range &&
      this._selected.length === 1 &&
      this.isRangeAllowed(date) &&
      this._selected[0] < date
    ) {
      this.rangeHovered = date

      this.inRange = {}
      const days = eachDayOfInterval({
        start: this._selected[0],
        end: date,
      })

      if (Array.isArray(days) && days.length) {
        for (let i = 0; i < days.length; i++) {
          this.inRange[formatISODate(days[i])] = this.isInRange(days[i])
        }
      }
    } else {
      this.rangeHovered = null
    }
  }

  public handleDateSelect(selectedDate: Date | null) {
    if (!selectedDate) return
    if (this.range) {
      this.handleRangeSelect(selectedDate)
    } else if (this.multiple) {
      this.toggleSelected(selectedDate)
    } else {
      if (this.selected.includes(formatISODate(selectedDate))) {
        this.emptySelected()
      } else {
        this.emptySelected()
        this.addToSelected(selectedDate)
      }
      this.close()
    }
    this.dispatchEvent(
      new CustomEvent('date-selected', {
        detail: this.selected,
        bubbles: true,
        composed: true,
      }),
    )
    return Promise.resolve()
  }

  public focusOnCurrentDate() {
    const currentDateISO = formatISODate(newDate())
    const el = this.querySelector(`div[data-date="${currentDateISO}"]`)
    if (el instanceof HTMLDivElement) {
      this.focusedDate = currentDateISO
      el.focus()
    } else {
      const firstSelectable = this.selectableDates.find((x) => !x.isDisabled)
      if (firstSelectable) {
        const firstSelectableEl = this.querySelector(
          `div[data-date="${firstSelectable.currentDateISO}"]`,
        )
        if (firstSelectableEl instanceof HTMLDivElement) {
          this.focusedDate = firstSelectable.currentDateISO
          firstSelectableEl.focus()
        }
      }
    }
  }

  public closeEvent(e: FocusEvent) {
    if (
      !this.contains(e.relatedTarget as Node) &&
      !(e.target as Element).classList.contains('pkt-hide')
    ) {
      this.close()
    }
  }

  public close() {
    this.dispatchEvent(
      new CustomEvent('close', {
        detail: true,
        bubbles: true,
        composed: true,
      }),
    )
  }
}
