import { html, LitElement, nothing, TemplateResult } from 'lit'
import { eventOptions, property, state } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { SplitItem } from './SplitItem.js'
import { SplitPanel } from './SplitPanel.js'
import { SplitDirection, SplitRegion } from './type.js'

export default class SplitView extends LitElement {
  /**
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor layout: SplitDirection = SplitDirection.horizontal

  /**
   * @attribute
   */
  @property({ type: Number, reflect: true }) accessor key: string | undefined

  @property({ type: Array }) accessor dragTypes: string[] | undefined

  /**
   * Whether dragging is occurring over the element
   */
  @state() protected accessor inDrag = false

  /**
   * The region the drag is leaning to.
   */
  @state() protected accessor dragRegion: SplitRegion = SplitRegion.center

  /**
   * A reference to the split panel that owns this view.
   */
  @property({ type: Object }) accessor panel: SplitPanel | undefined

  /**
   * When set it adds the `overflow` hidden on the container that holds the tab contents.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor constrain = false

  constructor() {
    super()
    this.handleDragleave = this.handleDragleave.bind(this)
    this.handleDragEnd = this.handleDragEnd.bind(this)
    this.addEventListener('dragenter', this.handleDragEnter.bind(this))
    this.addEventListener('dragover', this.handleDragOver.bind(this))
    this.addEventListener('drop', this.handleDrop.bind(this))
  }

  override connectedCallback(): void {
    super.connectedCallback()
    document.body.addEventListener('dragleave', this.handleDragleave)
    document.body.addEventListener('dragend', this.handleDragEnd)
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    document.body.removeEventListener('dragleave', this.handleDragleave)
    document.body.removeEventListener('dragend', this.handleDragEnd)
  }

  protected handleDragEnter(e: DragEvent): void {
    const { dataTransfer } = e
    if (!dataTransfer || !this.panelCanDrop(e)) {
      return
    }
    e.preventDefault()
    e.stopPropagation()
    dataTransfer.dropEffect = 'copy'
  }

  protected handleDragOver(e: DragEvent): void {
    const { dataTransfer } = e
    if (!dataTransfer || !this.panelCanDrop(e)) {
      return
    }
    e.preventDefault()
    e.stopPropagation()
    const region = this.getDropRegionFromEvent(e)
    if (!region) {
      this.inDrag = false
      return
    }
    dataTransfer.dropEffect = 'copy'
    this.inDrag = true
    this.dragRegion = region
  }

  protected handleDrop(e: DragEvent): void {
    const { dataTransfer } = e
    const { panel } = this
    if (!dataTransfer || !panel || !this.panelCanDrop(e)) {
      return
    }
    e.preventDefault()
    e.stopPropagation()
    const kind = dataTransfer.getData('text/kind')
    const key = dataTransfer.getData('text/key')
    if (!kind || !key) {
      return
    }
    const layoutKey = dataTransfer.getData('layout/key')
    this.inDrag = false
    // TODO: When the drag originated in another panel then we
    // move the panel between them. Otherwise we add the item to the panel.
    if (layoutKey) {
      // move operation not add.
      panel.manager.moveItem(layoutKey, panel.key, key, { region: this.dragRegion })
    } else {
      panel.addItem({ key, kind, label: 'New tab' }, { region: this.dragRegion })
      panel.manager.requestNameUpdate(key)
    }
    this.dispatchEvent(
      new CustomEvent('datadrop', {
        detail: { kind, key, region: this.dragRegion },
        composed: true,
        bubbles: true,
        cancelable: true,
      })
    )
    this.requestUpdate()
  }

  protected handleDragleave(e: DragEvent): void {
    const elm = this.findLayout(e)
    if (!elm || elm !== this) {
      this.inDrag = false
      this.dragRegion = SplitRegion.center
    }
  }

  protected handleDragEnd(): void {
    this.inDrag = false
    this.dragRegion = SplitRegion.center
  }

  protected getDropRegionFromEvent(e: DragEvent): SplitRegion | undefined {
    const layout = this.findLayout(e)
    if (!layout) {
      return undefined
    }
    return this.getDropRegion(layout, e)
  }

  getDropRegion(element: HTMLElement, e: DragEvent): SplitRegion | undefined {
    const { pageX, pageY } = e
    const rect = element.getBoundingClientRect()
    const quarterWidth = rect.width / 4
    const quarterHeight = rect.height / 4
    if (pageX < rect.left + quarterWidth) {
      return SplitRegion.west
    }
    if (pageX > rect.right - quarterWidth) {
      return SplitRegion.east
    }
    if (pageY < rect.top + quarterHeight) {
      return SplitRegion.north
    }
    if (pageY > rect.bottom - quarterHeight) {
      return SplitRegion.south
    }
    const withingCenterX = pageX >= rect.left + quarterWidth && pageX <= rect.right - quarterWidth
    const withingCenterY = pageY >= rect.top + quarterHeight && pageY <= rect.bottom - quarterHeight
    if (withingCenterX && withingCenterY) {
      return SplitRegion.center
    }
    return undefined
  }

  protected findLayout(e: Event): SplitView | undefined {
    const path = e.composedPath()
    while (path.length) {
      const node = path.shift() as Element
      if (node.nodeType !== Node.ELEMENT_NODE) {
        continue
      }
      if (node.localName === this.localName) {
        return node as SplitView
      }
    }
    return undefined
  }

  protected hasDropTypes(dt: DataTransfer): boolean {
    const { dragTypes } = this
    if (!Array.isArray(dragTypes)) {
      return true
    }
    const eventTypes = [...dt.types]
    const allowedTypes = dragTypes.map((i) => i.toLowerCase())
    return !allowedTypes.some((type) => !eventTypes.includes(type))
  }

  protected panelCanDrop(e: DragEvent): boolean {
    if (e.defaultPrevented) {
      return false
    }
    if (e.dataTransfer && !this.hasDropTypes(e.dataTransfer)) {
      return false
    }
    const { panel } = this
    if (!panel) {
      return true
    }
    return panel.canDrop()
  }

  protected handleTabDragStart(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt || this.key === undefined) {
      return
    }

    dt.effectAllowed = 'copyMove'
    dt.dropEffect = 'move'
    const node = e.target as HTMLElement
    dt.setData('text/kind', node.dataset.kind as string)
    dt.setData('text/key', node.dataset.key as string)
    dt.setData('text/source', this.localName)
    dt.setData('layout/key', this.key)
  }

  protected handleTabsDragEnter(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt || !this.panelCanDrop(e)) {
      return
    }
    e.stopPropagation()
    e.preventDefault()
    dt.effectAllowed = 'move'
    dt.dropEffect = 'move'
  }

  protected handleTabsDragover(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt || !this.panelCanDrop(e)) {
      return
    }
    e.stopPropagation()
    e.preventDefault()
    dt.effectAllowed = 'move'
    dt.dropEffect = 'move'
  }

  protected handleTabDrop(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt || !this.panelCanDrop(e) || this.key === undefined) {
      return
    }
    e.stopPropagation()
    e.preventDefault()
    const src = dt.getData('text/source')
    const key = dt.getData('text/key')
    const kind = dt.getData('text/kind')
    const srcPanelKey = dt.getData('layout/key')
    const movingTab = src === this.localName

    const overTab = this.findTab(e)
    const toIndex = overTab ? Number(overTab.dataset.index) : undefined

    if (movingTab) {
      if (!srcPanelKey) {
        return
      }
      this.moveTab(srcPanelKey, this.key, key, toIndex)
    } else {
      let dispatch = true
      if (this.panel) {
        this.panel.addItem({ key, kind, label: 'New tab' }, { index: toIndex })
        dispatch = true
      }
      if (dispatch) {
        this.dispatchEvent(
          new CustomEvent('datadrop', {
            detail: { kind, key, index: toIndex },
            composed: true,
            bubbles: true,
            cancelable: true,
          })
        )
      }
      this.requestUpdate()
    }
  }

  protected moveTab(fromLayout: string, toLayout: string, key: string, toIndex?: number): void {
    const { panel } = this
    if (panel) {
      panel.manager.moveItem(fromLayout, toLayout, key, { index: toIndex })
    }
  }

  protected findTab(e: Event): HTMLElement | undefined {
    const path = e.composedPath()
    while (path.length) {
      const node = path.shift() as Element
      if (node.nodeType !== Node.ELEMENT_NODE) {
        continue
      }
      if (node.classList.contains('layout-tab')) {
        return node as HTMLElement
      }
    }
    return undefined
  }

  protected async handleTabClick(e: Event): Promise<void> {
    if (!this.panel) {
      return
    }
    const key = (e.currentTarget as HTMLElement).dataset.key as string
    if (!key) {
      return
    }
    if (this.panel.selected === key) {
      return
    }
    this.panel.selected = key
    this.panel.manager.notifyChange()
    this.requestUpdate()
    await this.updateComplete
    const child = this.querySelector(`[data-key="${key}"]`)
    if (child) {
      child.dispatchEvent(new Event('resize'))
    }
  }

  /**
   * Closes a panel with right pointer configuration
   */
  protected handleTabPointerDown(e: PointerEvent): void {
    // the configuration of a middle button click which is
    // equal to 3 fingers click on a track pad.
    if (e.button === 1 && e.buttons === 4) {
      e.preventDefault()
      e.stopPropagation()
      const key = (e.target as HTMLElement).dataset.key as string
      this.closeTab(key)
    }
  }

  protected handleTabClose(e: Event): void {
    const icon = e.target as HTMLElement
    const button = icon.parentElement as HTMLElement
    const key = button.dataset.key as string
    if (key) {
      e.preventDefault()
      e.stopPropagation()
      this.closeTab(key)
    }
  }

  @eventOptions({ passive: true })
  protected handleTabTouchStart(e: TouchEvent): void {
    if (e.targetTouches.length === 3) {
      e.preventDefault()
      e.stopPropagation()
      const key = (e.target as HTMLElement).dataset.key as string
      this.closeTab(key)
    }
  }

  protected closeTab(key: string): void {
    const { panel } = this
    if (!panel) {
      return
    }
    const item = panel.manager.findItem(key)
    if (!item) {
      return
    }
    if (item.persistent && item.pinned) {
      return
    }
    panel.removeItem(key)
    this.requestUpdate()
    this.dispatchEvent(
      new CustomEvent('closetab', {
        bubbles: true,
        cancelable: true,
        composed: true,
        detail: key,
      })
    )
  }

  protected handleTabKeyDown(e: KeyboardEvent): void {
    if (e.key === 'Enter') {
      this.handleTabClick(e)
    }
  }

  override render(): TemplateResult {
    return html`
      ${this.dragRegionTemplate()} ${this.tabsTemplate()}
      <div class="content">
        <slot></slot>
      </div>
    `
  }

  protected dragRegionTemplate(): TemplateResult | typeof nothing {
    const { inDrag, dragRegion } = this
    if (!inDrag) {
      return nothing
    }
    return html` <div class="drag-region ${dragRegion}"></div> `
  }

  protected tabsTemplate(): TemplateResult | string {
    const { panel } = this
    if (!panel) {
      return ''
    }
    const items = panel.sortedItems()
    if (!items) {
      return ''
    }
    const size = items.length
    return html`
      <div
        class="layout-tabs"
        role="tablist"
        @dragenter="${this.handleTabsDragEnter}"
        @dragover="${this.handleTabsDragover}"
        @drop="${this.handleTabDrop}"
      >
        ${items.map((tab, i) => this.tabTemplate(tab, i + 1 === size, items[i + 1]?.key))}
      </div>
    `
  }

  protected tabTemplate(item: SplitItem, last: boolean, nextKey?: string): TemplateResult {
    const { key, kind, label = '', index = 0, icon, isDirty = false, parent } = item
    const { panel } = this
    const selected = !!panel && panel.selected === key
    const nextSelected = !!nextKey && !!panel && panel.selected === nextKey
    const closable = !item.persistent && !item.pinned
    const classes = {
      'layout-tab': true,
      'is-dirty': isDirty,
      selected,
      'surface1': selected,
    }
    let title = label
    if (isDirty) {
      title = 'Unsaved changes'
    }

    const dividerHidden = last || selected || nextSelected
    return html`
      <div
        data-key="${key}"
        data-kind="${kind}"
        data-index="${index}"
        data-parent="${ifDefined(parent)}"
        data-dirty="${isDirty}"
        data-panel="${ifDefined(panel?.key)}"
        role="tab"
        class="${classMap(classes)}"
        draggable="true"
        tabindex="0"
        @dragstart="${this.handleTabDragStart}"
        @click="${this.handleTabClick}"
        @pointerdown="${this.handleTabPointerDown}"
        @touchstart="${this.handleTabTouchStart}"
        @keydown="${this.handleTabKeyDown}"
      >
        ${icon ? html`<ui-icon icon="${icon}" class="tab-favicon"></ui-icon>` : ''}
        <span class="tab-label" title="${title}">${label}</span>
        ${closable ? html`<ui-icon icon="close" class="close-icon" @click="${this.handleTabClose}"></ui-icon>` : ''}
      </div>
      <div class="tab-divider ${dividerHidden ? 'hidden' : ''}"></div>
    `
  }
}
