import { html, LitElement, nothing, PropertyValues, TemplateResult } from 'lit'
import { property, query, queryAssignedElements, state } from 'lit/decorators.js'
import { ClassInfo, classMap } from 'lit/directives/class-map.js'
import { StyleInfo, styleMap } from 'lit/directives/style-map.js'
import { Easing } from '../../motion/animation.js'
import UiTab from './Tab.js'
import '../../icon-button/ui-icon-button.js'
import '../../divider/ui-divider.js'
import '../../icons/ui-icon.js'

export type TabsPriority = 'primary' | 'secondary'

export interface TabSelectionDetail {
  item: UiTab
  index: number
  select: string | null
}

/**
 * Calculates value in percentages
 * @param w Element width
 * @param w0 Parent width
 * @return The percentage of element's width relative to parent.
 */
export function calcPercent(w: number, w0: number): number {
  return (100 * w) / w0
}

/**
 * A container for tabs.
 *
 * @fires select - A non bubbling event when selection change through user interaction.
 *                The `event.detail` object contains the `item` and `index` properties.
 *                It also has the `select` property with the value of the `selectedAttribute` on the tab.
 */
export default class UiTabs extends LitElement {
  items: UiTab[] = []

  activeItem: UiTab | null = null

  previousItem: UiTab | null = null

  @queryAssignedElements({ flatten: true })
  protected accessor assignedElements!: HTMLElement[]

  @query('.pointer') protected accessor pointer!: HTMLElement

  @query('.content') protected accessor content!: HTMLElement

  /**
   * The priority of the tabs.
   *
   * @default primary
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor priority: TabsPriority = 'primary'

  /**
   * If true, tabs are scrollable and the tab width is based on the label width.
   * @attribute
   */
  @property({ type: Boolean }) accessor scrollable: boolean | undefined

  /**
   * The value of the selected tab.
   * This is matched with the `aria-controls` of the tab.
   * @attribute
   */
  @property({ type: String }) accessor selected: string | undefined

  /**
   * The attribute on the `ui-tab` that indicates which value for `selected`
   * corresponds to which tab.
   *
   * @default aria-controls
   * @attribute
   */
  @property({ type: String }) accessor selectedAttribute = `aria-controls`

  @state() protected accessor pointerStyles: StyleInfo | undefined

  protected contentScroll?: number

  @state() protected accessor indicated = false

  protected observer: IntersectionObserver

  /**
   * This is set by the intersection observer. Once the tabs are in the view it turns to `true`.
   * This we can properly visualize selection.
   */
  @state() accessor isVisible: boolean | undefined

  constructor() {
    super()

    this.observer = new IntersectionObserver(this.intersectionCallback.bind(this), {
      threshold: 1.0,
      rootMargin: '0px',
    })
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('isVisible')) {
      this.handleVisibility()
    }
    super.willUpdate(cp)
  }

  override connectedCallback(): void {
    super.connectedCallback()
    if (!this.hasAttribute('role')) {
      this.setAttribute('role', 'tablist')
    }
    this.observer.observe(this)
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    this.observer.unobserve(this)
  }

  protected intersectionCallback(entries: IntersectionObserverEntry[]): void {
    const [entry] = entries
    this.isVisible = entry.isIntersecting
  }

  protected async updateItems(): Promise<void> {
    const elements = this.assignedElements || []
    const items = elements.filter(this.isTabItem, this)
    this.items = items
    if (this.activeItem && !items.includes(this.activeItem)) {
      this.activeItem = null
    }
    await this.updateComplete
    this.ensureSelection()
  }

  /**
   * @return Whether the given element is a list item element.
   */
  protected isTabItem(element: Element): element is UiTab {
    if (element.nodeType !== Node.ELEMENT_NODE) {
      return false
    }
    return element.localName === 'ui-tab'
  }

  protected isSelectable(element: UiTab): boolean {
    if (element.disabled) {
      return false
    }
    if (element.hidden && element.hasAttribute('hidden')) {
      return false
    }
    return true
  }

  protected ensureSelection(): void {
    const { selected, selectedAttribute, activeItem, items } = this
    if (!selectedAttribute) {
      return
    }
    const item = items.find((i) => i.getAttribute(selectedAttribute) === selected)
    if (item && item === activeItem) {
      return
    }
    this.makeSelection(item)
  }

  protected makeSelection(tab?: UiTab, focus = false): void {
    const { activeItem } = this
    if (activeItem === tab) {
      tab.highlight()
      return
    }
    this.previousItem = activeItem
    if (activeItem) {
      this.deselectItem(activeItem)
    }
    if (tab) {
      this.selectItem(tab, focus)
      this.positionPointer(tab, activeItem)
      if (this.activeItem) {
        // we set this here so we won't notify selection when initializing.
        this.notifySelect(tab)
      }
    }
  }

  notifySelect(item: UiTab): void {
    const index = this.items.indexOf(item)
    if (index === -1) {
      return
    }
    const attrValue = item.getAttribute(this.selectedAttribute)
    this.dispatchEvent(
      new CustomEvent<TabSelectionDetail>('select', {
        detail: {
          item,
          index,
          select: attrValue,
        },
      })
    )
  }

  protected selectItem(tab: UiTab, focus = false): void {
    this.activeItem = tab
    if (this.hasAttribute('tabindex')) {
      this.removeAttribute('tabindex')
    }
    tab.setAttribute('aria-selected', 'true')
    tab.setAttribute('tabindex', '0')
    tab.selected = true
    tab.priority = this.priority
    tab.scrollIntoView({ inline: 'nearest', behavior: 'auto', block: 'nearest' })
    if (focus) {
      tab.focus()
    }
  }

  protected deselectItem(tab: UiTab): void {
    tab.setAttribute('aria-selected', 'false')
    tab.setAttribute('tabindex', '-1')
    tab.selected = false
    tab.indicated = false
  }

  protected handleVisibility(): void {
    const { previousItem, activeItem } = this
    if (!activeItem) {
      return
    }
    this.positionPointer(activeItem, previousItem)
  }

  protected async positionPointer(tab: UiTab, old?: UiTab | null): Promise<void> {
    const { items, pointer, isVisible } = this
    if (!isVisible) {
      return
    }
    const index = items.indexOf(tab)
    if (index < 0 || !pointer) {
      this.pointerStyles = undefined
      return
    }

    if (!old) {
      // no start animation, just show indicator.
      tab.indicated = true
      return
    }
    const isPrimary = this.priority === 'primary'
    const final = isPrimary ? this.getPrimaryLeft(tab) : this.getSecondaryLeft(tab)
    if (this.pointerStyles && this.pointerStyles.left === `${final}px`) {
      return
    }

    // first position this indicator in the place of the old one.
    // update the view and then run the animation.
    this.indicated = true
    const left = isPrimary ? this.getPrimaryLeft(old) : this.getSecondaryLeft(old)
    this.pointerStyles = { left }
    await this.updateComplete
    const frames = isPrimary ? this.getPrimaryKeyframes(left, final) : this.getSecondaryKeyframes(left, final)

    if (this.moveAnimation) {
      this.moveAnimation.cancel()
    }
    const moveAnimation = pointer.animate(frames, {
      duration: 360,
      iterations: 1,
      easing: Easing.DECELERATION,
    })
    const finalStyles: StyleInfo = {
      left: `${final}px`,
    }
    moveAnimation.addEventListener('finish', () => {
      this.pointerStyles = finalStyles
      this.moveAnimation = undefined
      tab.indicated = true
      this.indicated = false
    })
    moveAnimation.addEventListener('cancel', () => {
      this.pointerStyles = finalStyles
    })
    this.moveAnimation = moveAnimation
  }

  protected moveAnimation?: Animation

  protected getPrimaryLeft(tab: UiTab): string {
    const contentBox = this.content.getBoundingClientRect()
    const tabRect = tab.getBoundingClientRect()
    const leftFromParent = tabRect.x - contentBox.x
    const offset = this.scrollable ? 48 : 0 // left button
    const left = leftFromParent + tabRect.width / 2 - 20 + offset
    return `${left}px`
  }

  protected getSecondaryLeft(tab: UiTab): string {
    const contentBox = this.content.getBoundingClientRect()
    const tabRect = tab.getBoundingClientRect()
    const offset = this.scrollable ? 48 : 0 // left button
    const leftFromParent = tabRect.x - contentBox.x + offset
    return `${leftFromParent}px`
  }

  protected getPrimaryKeyframes(start: string, final: string): Keyframe[] {
    return [
      {
        left: start,
        width: `40px`,
      },
      {
        width: `80px`,
      },
      {
        left: final,
        width: `40px`,
      },
    ]
  }

  protected getSecondaryKeyframes(start: string, final: string): Keyframe[] {
    return [
      {
        left: start,
      },
      {
        left: final,
      },
    ]
  }

  protected contentClickHandler(e: PointerEvent): void {
    this.activateFromEvent(e)
  }

  protected contentKeyDownHandler(e: KeyboardEvent): void {
    if (['Enter', 'Space'].includes(e.code)) {
      this.activateFromEvent(e)
    }
  }

  protected activateFromEvent(e: Event): void {
    const { items } = this
    const path = e.composedPath()
    let item: UiTab | undefined
    while (!item) {
      const next = path.shift() as UiTab
      if (!next) {
        break
      }
      if (items.includes(next)) {
        item = next
      }
    }
    if (!item) {
      return
    }
    this.makeSelection(item, true)
  }

  protected handleScrollLeft(): void {
    const { contentScroll = 0, content } = this
    if (contentScroll === 0) {
      return
    }
    let left = contentScroll - 80
    if (left < 0) {
      left = 0
    }
    this.contentScroll = left
    content.scrollTo({
      behavior: 'smooth',
      left,
    })
  }

  protected handleScrollRight(): void {
    const { contentScroll = 0, content } = this
    let left = contentScroll + 80
    if (left + content.clientWidth > content.scrollWidth) {
      left = content.scrollWidth - content.clientWidth
    }
    this.contentScroll = left
    content.scrollTo({
      behavior: 'smooth',
      left,
    })
  }

  protected handleScroll(e: Event): void {
    const div = e.target as HTMLElement
    this.contentScroll = div.scrollLeft
  }

  protected handleKeyDown(e: KeyboardEvent): void {
    if (e.code === 'ArrowRight') {
      e.preventDefault()
      const tab = this.activeItem ? this.getNextItem(this.activeItem) : this.items[0]
      this.makeSelection(tab, true)
    } else if (e.code === 'ArrowLeft') {
      e.preventDefault()
      const tab = this.activeItem ? this.getPreviousItem(this.activeItem) : this.items[0]
      this.makeSelection(tab, true)
    }
  }

  getPreviousItem(item: UiTab): UiTab {
    const { items } = this
    const curIndex = items.indexOf(item)
    if (curIndex < 0) {
      return item
    }
    let i = curIndex
    let result: HTMLElement | undefined
    do {
      i--
      if (i === curIndex) {
        // looped back from the end, no active element to find.
        return item
      }
      const tmp = items[i]
      if (!tmp) {
        i = items.length
        continue
      }
      if (this.isSelectable(tmp)) {
        result = tmp
      }
    } while (!result)
    return (result as UiTab) || item
  }

  getNextItem(item: UiTab): UiTab {
    const { items } = this
    const curIndex = items.indexOf(item)
    if (curIndex < 0) {
      return item
    }
    let i = curIndex
    let next: HTMLElement | undefined
    do {
      i++
      if (i === curIndex) {
        // looped back from the start, no active element to find.
        return item
      }
      const tmp = items[i]
      if (!tmp) {
        i = -1
        continue
      }
      if (this.isSelectable(tmp)) {
        next = tmp
      }
    } while (!next)
    return (next as UiTab) || item
  }

  override render(): TemplateResult {
    const classes: ClassInfo = {
      surface: true,
      scrollable: !!this.scrollable,
    }
    return html`
      <div class="${classMap(classes)}">
        ${this.renderLeftScrollControl()}
        <div class="content" @scroll="${this.handleScroll}" @keydown="${this.handleKeyDown}">${this.renderSlot()}</div>
        ${this.rightScrollControl()} ${this.renderIndicator()}
        <div class="divider"></div>
      </div>
    `
  }

  protected renderSlot(): TemplateResult {
    return html`<slot
      @slotchange="${this.updateItems}"
      @click="${this.contentClickHandler}"
      @keydown="${this.contentKeyDownHandler}"
    ></slot>`
  }

  protected renderIndicator(): TemplateResult {
    const classes: ClassInfo = {
      indicator: true,
      indicated: this.indicated,
      primary: this.priority === 'primary',
      secondary: this.priority === 'secondary',
    }
    return html`
      <div class="${classMap(classes)}">
        <span role="presentation" class="pointer" style="${styleMap(this.pointerStyles || {})}"></span>
      </div>
    `
  }

  protected renderLeftScrollControl(): TemplateResult | typeof nothing {
    if (!this.scrollable) {
      return nothing
    }
    return html` <div class="scroll-control left">
      <ui-icon-button aria-label="Scroll tabs left" title="Scroll tabs left" @click="${this.handleScrollLeft}">
        <ui-icon icon="chevronLeft"></ui-icon>
      </ui-icon-button>
    </div>`
  }

  protected rightScrollControl(): TemplateResult | typeof nothing {
    if (!this.scrollable) {
      return nothing
    }
    return html`<div class="scroll-control right">
      <ui-icon-button aria-label="Scroll tabs right" title="Scroll tabs right" @click="${this.handleScrollRight}">
        <ui-icon icon="chevronRight"></ui-icon>
      </ui-icon-button>
    </div>`
  }
}
