import { html, svg, type SVGTemplateResult, type TemplateResult } from 'lit'
import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { property, queryAll } from 'lit/decorators.js'
import { iconWrapper } from '../../../md/icons/Icons.js'
import type { MenuItem } from './MenuItem.js'
import type UiListItem from '../../../md/list/internals/ListItem.js'
import { positionOverlay } from '../../../lib/ElementPositioning.js'
import UiList from '../../../md/list/internals/List.js'

import '../../../md/list/ui-list-item.js'
import '../../../md/divider/ui-divider.js'

export const arrowRight = iconWrapper(svg`<path d="M0 0h24v24H0V0z" fill="none"/><path d="M10 17l5-5-5-5v10z"/>`)
export const check = iconWrapper(
  svg`<path d="M0 0h24v24H0V0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>`
)

interface IMenuFocusOptions extends FocusOptions {
  /**
   * When set it focuses on the first menu item.
   */
  selectFirst?: boolean
}

interface CreateNestedOptions {
  /**
   * When using keyboard navigation.
   * When set it focuses on the first created event.
   */
  keyboard?: boolean
}

/**
 * @fires close - when the menu closes.
 * @fires trigger - when a command is triggered
 */
export default class ContextualMenuElement<S = unknown> extends UiList {
  @queryAll('ui-list-item') accessor _items!: NodeListOf<UiListItem>

  override set items(value: UiListItem[]) {
    //
  }

  override get items(): UiListItem[] {
    return Array.from(this._items)
  }

  /**
   * The list of commands to render.
   */
  @property({ type: Array }) accessor commands: MenuItem<S>[] | undefined

  /**
   * The commands store.
   */
  @property({ type: Object }) accessor store: S | undefined

  /**
   * The menu triggering click target
   */
  @property({ type: Object }) accessor target: HTMLElement | SVGElement | undefined

  /**
   * The current workspace
   */
  @property({ type: Object }) accessor workspace: HTMLElement | undefined

  /**
   * Any arbitrary data to pass to the lifecycle functions.
   */
  @property({ type: Object }) accessor customData: unknown | undefined

  /**
   * The timeout after which the sub menu become visible or is removed from the menu.
   */
  @property({ type: Number }) accessor subMenuTimeout = 300

  /**
   * The id of the parent command, in case when this menu is a sub-menu.
   */
  @property({ type: String }) accessor parentCommand: string | undefined

  protected nestedIdValue?: string

  protected nestedTimeout?: number

  protected removingTimeout?: number

  constructor() {
    super()
    this.subTriggerHandler = this.subTriggerHandler.bind(this)

    this.addEventListener('mousemove', this.mouseoverHandler.bind(this))
  }

  override connectedCallback(): void {
    super.connectedCallback()
    if (!this.hasAttribute('role')) {
      this.setAttribute('role', 'menu')
    }
    if (!this.hasAttribute('aria-label')) {
      this.setAttribute('aria-label', 'Context menu')
    }
  }

  close(): void {
    this.dispatchEvent(new Event('close'))
  }

  /**
   * Triggest nested menu when hovering an item that has a nested menu.
   */
  protected mouseoverHandler(e: MouseEvent): void {
    if (e.defaultPrevented) {
      return
    }
    e.preventDefault()
    const path = e.composedPath()
    const target = path.find((i) => (i as Element).localName === 'ui-list-item')
    if (!target) {
      return
    }
    const { nested, cmd } = (target as HTMLElement).dataset
    if (nested === 'true') {
      this.buildNested(cmd as string)
    } else if (this.nestedIdValue) {
      this.removeNested()
    }
  }

  /**
   * Builds a nested menu for a command.
   * @param id The id of the command
   */
  protected buildNested(id: string): void {
    if (!id || id === this.nestedIdValue) {
      return
    }
    if (this.nestedIdValue) {
      this.clearNested()
    }
    const cmd = this.commands?.find((item) => item.id === id)
    if (!cmd || !cmd.hasChildren) {
      return
    }
    this.nestedIdValue = id
    if (this.nestedTimeout) {
      clearTimeout(this.nestedTimeout)
    }
    this.nestedTimeout = setTimeout(() => {
      this.nestedTimeout = undefined
      this.createNested()
    }, this.subMenuTimeout) as unknown as number
  }

  /**
   * Adds the nested menu to the local DOM.
   */
  protected createNested(opts: CreateNestedOptions = {}): void {
    const id = this.nestedIdValue
    if (!id) {
      return
    }
    const ref = (this.shadowRoot as ShadowRoot).querySelector(`[data-cmd="${id}"]`) as HTMLElement
    // const box = ref.getBoundingClientRect();
    const cmd = this.commands?.find((item) => item.id === id)
    if (!cmd) {
      return
    }
    const { children } = cmd
    const menu = document.createElement('contextual-menu') as ContextualMenuElement<S>
    // TODO: create a type definition for processed internally menu items
    menu.commands = children as MenuItem<S>[]
    menu.store = this.store
    menu.target = this.target
    menu.workspace = this.workspace
    menu.customData = this.customData
    const styles = positionOverlay(menu, ref, {
      horizontal: 'left',
      vertical: 'top',
      noOverlap: true,
      verticalOffset: -56,
      horizontalOffset: -4,
    })
    if (styles.top) {
      menu.style.top = styles.top as string
    }
    if (styles.left) {
      menu.style.left = styles.left as string
    }

    menu.parentCommand = id
    menu.addEventListener('close', this.handleCloseNested.bind(this))
    menu.addEventListener('trigger', this.subTriggerHandler as EventListener)
    this.shadowRoot?.append(menu)
    if (opts.keyboard) {
      menu.focus({ selectFirst: true })
    } else {
      menu.focus()
    }
  }

  protected handleCloseNested(): void {
    this.clearNested()
  }

  /**
   * Triggers the remove nested menu action.
   */
  protected removeNested(): void {
    if (this.removingTimeout) {
      return
    }
    this.removingTimeout = setTimeout(() => {
      this.removingTimeout = undefined
      this.clearNested()
    }, this.subMenuTimeout) as unknown as number
  }

  override focus(options?: IMenuFocusOptions): void {
    const { activeListItem } = this
    if (activeListItem) {
      activeListItem.focus(options)
    } else if (options && options.selectFirst) {
      this.focusAfterUpdate()
    } else {
      super.focus(options)
    }
  }

  /**
   * Waits for the update to complete and focuses on the first item
   * or self when there are no items.
   */
  protected async focusAfterUpdate(options?: IMenuFocusOptions): Promise<void> {
    await this.updateComplete
    if (this.items.length) {
      this.activeListItem = this.items[0] as UiListItem
      this.activateListItem(this.activeListItem)
    } else {
      super.focus(options)
    }
  }

  /**
   * Removes the nested menu from the DOM.
   */
  protected clearNested(): void {
    if (!this.nestedIdValue) {
      return
    }
    this.nestedIdValue = undefined
    const menu = this.shadowRoot?.querySelector('contextual-menu') as ContextualMenuElement<S> | null
    if (menu) {
      this.shadowRoot?.removeChild(menu)
    }
    this.focus()
  }

  /**
   * Opens the menu when the current focused item has a sub menu.
   */
  protected moveRight(): void {
    const { activeListItem } = this
    if (!activeListItem) {
      return
    }
    const { nested, cmd } = activeListItem.dataset
    if (nested === 'true') {
      this.clearNested()
      this.nestedIdValue = cmd
      this.createNested({ keyboard: true })
    }
  }

  /**
   * Closes the menu when this is a sub menu.
   */
  protected moveLeft(): void {
    if (!this.parentCommand) {
      return
    }
    this.close()
  }

  /**
   * A handler for the sub-menu `trigger` event.
   * Retargets the event to the parent.
   * It adds `parent` to the detail, when the parent (currently selected command) has
   * the execute function.
   */
  protected subTriggerHandler(e: CustomEvent): void {
    if (!e.detail.parent) {
      const id = this.nestedIdValue
      const cmd = this.commands?.find((item) => item.id === id)
      if (!cmd) {
        return
      }
      if (typeof cmd.execute === 'function') {
        e.detail.parent = cmd
      }
    }
    this.dispatchEvent(
      new CustomEvent('trigger', {
        detail: e.detail,
      })
    )
  }

  override handleKeydown(e: KeyboardEvent): void {
    e.stopPropagation()
    e.stopImmediatePropagation()
    super.handleKeydown(e)
    if (e.code === 'Escape' || e.code === 'Tab') {
      this.close()
    } else if (['Enter', 'Space'].includes(e.code)) {
      e.preventDefault()
      this.activateFromEvent(e)
    } else if (e.code === 'ArrowRight') {
      e.preventDefault()
      this.moveRight()
    } else if (e.code === 'ArrowLeft') {
      e.preventDefault()
      this.moveLeft()
    }
  }

  protected override activateFromEvent(e: Event): void {
    const path = e.composedPath()
    const item = path.find((i) => (i as Element).localName === 'ui-list-item')
    if (!item) {
      return
    }
    this.activate((item as HTMLElement).dataset.cmd)
  }

  protected activate(id?: string): void {
    const { commands } = this
    if (!id || !commands) {
      return
    }
    const index = commands.findIndex((item) => item.id === id)
    const cmd = commands[index]
    if (!cmd) {
      return
    }
    if (cmd.hasChildren) {
      this.buildNested(id)
    } else {
      const detail = {
        command: cmd,
        item: index,
      }
      this.dispatchEvent(
        new CustomEvent('trigger', {
          detail,
        })
      )
    }
  }

  override render(): TemplateResult {
    const { commands } = this
    if (!Array.isArray(commands) || !commands.length) {
      return html``
    }
    return html` ${commands.map((item) => this.renderMenuItem(item))} `
  }

  protected renderMenuItem(item: MenuItem<S>): TemplateResult | string {
    switch (item.type) {
      case 'normal':
        return this.renderMenuEntry(item)
      case 'separator':
        return this.renderSeparator(item)
      case 'label':
        return this.renderLabel(item)
      case 'radio':
        return this.renderRadioEntry(item)
      default:
        return ''
    }
  }

  protected renderSeparator(item: MenuItem<S>): TemplateResult {
    const { store, target, workspace, customData } = this
    if (!store || !target || !workspace) {
      return html``
    }
    const visible = item.isVisible(store, target, workspace, customData)
    const classes = {
      'menu-divider': true,
      'hidden': !visible,
    }
    return html` <ui-divider type="inset" class="${classMap(classes)}" data-cmd="${item.id}"></ui-divider> `
  }

  protected renderLabel(item: MenuItem<S>): TemplateResult {
    const { store, target, workspace, customData } = this
    if (!store || !target || !workspace) {
      return html``
    }
    const visible = item.isVisible(store, target, workspace, customData)
    const classes = {
      'menu-section-label': true,
      'hidden': !visible,
    }
    return html` <div class="${classMap(classes)}" data-cmd="${item.id}">${item.label}</div> `
  }

  protected renderMenuEntry(item: MenuItem<S>): TemplateResult {
    const { store, target, workspace, customData } = this
    if (!store || !target || !workspace) {
      return html``
    }
    item.beforeRenderCallback(store, target, workspace, customData)
    const visible = item.isVisible(store, target, workspace, customData)
    const enabled = item.isEnabled(store, target, workspace, customData)
    const classes = {
      item: true,
      disabled: !enabled,
      hidden: !visible,
    }
    const { title, icon, label, id, hasChildren } = item
    return html`
      <ui-list-item
        role="menuitem"
        class="${classMap(classes)}"
        ?disabled="${!visible || !enabled}"
        aria-hidden="${!visible}"
        title="${ifDefined(title)}"
        data-cmd="${id}"
        data-nested="${hasChildren}"
        aria-haspopup="${hasChildren}"
      >
        ${this.renderMenuEntryIcon(icon)}
        <span class="menu-label">${label}</span>
        ${hasChildren ? this.renderChildIcon() : ''}
      </ui-list-item>
    `
  }

  protected renderRadioEntry(item: MenuItem<S>): TemplateResult {
    const { store, target, workspace, customData } = this
    if (!store || !target || !workspace) {
      return html``
    }
    item.beforeRenderCallback(store, target, workspace, customData)
    const visible = item.isVisible(store, target, workspace, customData)
    const enabled = item.isEnabled(store, target, workspace, customData)
    const checked = item.isChecked(store, target, workspace, customData)
    const classes = {
      item: true,
      disabled: !enabled,
      hidden: !visible,
    }
    const { title, label, id, hasChildren } = item
    return html`
      <ui-list-item
        role="menuitem"
        class="${classMap(classes)}"
        ?disabled="${!visible || !enabled}"
        aria-hidden="${!visible}"
        title="${ifDefined(title)}"
        data-cmd="${id}"
        data-nested="${hasChildren}"
        data-type="radio"
        aria-haspopup="${hasChildren}"
      >
        ${this.renderCheckMark(checked)}
        <span class="menu-label">${label}</span>
        ${hasChildren ? this.renderChildIcon() : ''}
      </ui-list-item>
    `
  }

  /**
   * @param icon The icon is not required.
   */
  protected renderMenuEntryIcon(icon?: SVGTemplateResult): TemplateResult {
    if (!icon) {
      return html``
    }
    return html` <div class="menu-icon" slot="start">${icon}</div> `
  }

  /**
   * @return The template for sub-menu icon
   */
  protected renderChildIcon(): TemplateResult {
    return html` <div class="sub-menu-icon" slot="end">${arrowRight}</div> `
  }

  /**
   * @return The template for the menu entry checked mark.
   */
  protected renderCheckMark(checked: boolean): TemplateResult | string {
    if (!checked) {
      return html`<div class="menu-icon" data-state="unchecked" slot="start"></div>`
    }
    return html` <div class="menu-icon" data-state="checked" slot="start">${check}</div> `
  }
}
