import { html, nothing, TemplateResult } from 'lit'
import { property, queryAsync, state } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { UiElement } from '../../UiElement.js'
import { BeginPressConfig, EndPressConfig } from '../../../controllers/ActionController.js'
import UiRipple from '../../ripple/internals/ripple.js'
import { isDisabled, setDisabled } from '../../../lib/disabled.js'

import '../../ripple/ui-ripple.js'
import '@material/web/focus/md-focus-ring.js'

/**
 * A material design button.
 *
 * Note, this control is form associated.
 *
 * @slot icon - A slot for the icon element
 * @slot - The default slot for the label
 * @attribute {string} form - The form associated with this element when the element is outside the form.
 *                   It should be the id of the form element within the same document.
 */
export default class UiButton extends UiElement {
  static readonly formAssociated = true

  #internals = this.attachInternals()

  /**
   * The form associated with this element
   * @attribute {string} form - The form associated with this element when the element is outside the form.
   *                   It should be the id of the form element within the same document.
   */
  get form(): HTMLFormElement | null {
    return this.#internals.form
  }

  get validity() {
    return this.#internals.validity
  }

  get validationMessage() {
    return this.#internals.validationMessage
  }

  get willValidate() {
    return this.#internals.willValidate
  }

  /**
   * Determines when the element has an icon in the "icon" slot.
   */
  @state() protected accessor hasIcon: boolean

  /**
   * The name of the button, submitted as a pair with the button's value as part of the form data,
   * when that button is used to submit the form.
   * @attribute
   */
  @property({ type: String }) accessor name: string | undefined

  #value?: string

  get value(): string | undefined {
    return this.#value
  }

  /**
   * Defines the value associated with the button's name when it's submitted with the form data.
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#value}
   * @attribute
   */
  @property({ type: String })
  set value(value: string | undefined) {
    if (this.#value === value) {
      return
    }
    this.#value = value
    this.#internals?.setFormValue(value || null)
  }

  /**
   * Whether to render the icon at the inline end of the label rather than the
   * inline start.
   * @attribute
   */
  @property({ type: Boolean, attribute: 'trailingicon', reflect: true }) accessor trailingIcon = false

  @queryAsync('ui-ripple') protected accessor ripple!: Promise<UiRipple | null>

  /**
   * The default behavior of the button.
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#type}
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor type: 'submit' | 'reset' | 'button' = 'button'

  get disabled(): boolean {
    return isDisabled(this)
  }

  /**
   * When set, the button is a disabled state.
   * @attribute
   */
  @property({ reflect: true, type: Boolean })
  set disabled(value: boolean) {
    const old = isDisabled(this)
    setDisabled(this, value)
    this.requestUpdate('disabled', old)
  }

  constructor() {
    super()

    this.hasIcon = false
    this.actionController.cancelKeyboardEvents = true
    this.addEventListener('keydown', this.handleKeyDown.bind(this))
    this.addEventListener('keyup', this.handleKeyUp.bind(this))
    this.addEventListener('click', this.handleClick.bind(this))
    this.addEventListener('pointerdown', this.handlePointerDown.bind(this))
    this.addEventListener('pointerup', this.handlePointerUp.bind(this))
    this.addEventListener('pointercancel', this.handlePointerCancel.bind(this))
    this.addEventListener('pointerleave', this.handlePointerLeave.bind(this))
    this.addEventListener('contextmenu', this.handleContextMenu.bind(this))
  }

  override connectedCallback(): void {
    super.connectedCallback()
    if (!this.hasAttribute('role')) {
      this.setAttribute('role', 'button')
    }
    if (!this.hasAttribute('tabindex') && !this.disabled) {
      this.setAttribute('tabindex', '0')
    }
  }

  checkValidity() {
    return this.#internals.checkValidity()
  }

  reportValidity() {
    return this.#internals.reportValidity()
  }

  protected override firstUpdated(): void {
    this.#internals?.setFormValue(this.value || null)
  }

  protected async pressRipple(): Promise<void> {
    const element = await this.ripple
    if (element && !element.isPressed) {
      element.beginPress()
    }
  }

  protected async endRipple(): Promise<void> {
    const element = await this.ripple
    element?.endPress()
  }

  override beginPress(options: BeginPressConfig): void {
    super.beginPress(options)
    this.pressRipple()
  }

  override endPress(config: EndPressConfig): void {
    super.endPress(config)
    this.endRipple()
    const { cancelled, reason } = config
    if (cancelled) {
      return
    }
    const allowedReasons = reason === 'enter' || reason === 'space'
    if (this.type === 'submit' && (!reason || allowedReasons)) {
      this.handleSubmit()
    } else if (allowedReasons) {
      this.click()
    }
  }

  protected handleSubmit(): void {
    const { name, value, type, disabled, form } = this
    if (!form || !type || disabled) {
      return
    }
    let button: HTMLButtonElement | undefined
    if (name || value) {
      button = document.createElement('button')
      if (name) {
        button.name = name
      }
      if (value) {
        button.value = value
      }
      button.type = type
      button.hidden = true
      form.append(button)
    }
    try {
      form.requestSubmit(button)
    } catch {
      // ...
    }
    if (button) {
      form.removeChild(button)
    }
  }

  /**
   * Sets the `_hasIcon` state property when the "icon" slot change event is dispatched.
   */
  protected handleSlotChange(e: Event): void {
    const slot = e.target as HTMLSlotElement
    this.hasIcon = !!slot.assignedNodes().length
  }

  protected override render(): TemplateResult {
    const { pressed = false, trailingIcon = false } = this
    const containerClasses = classMap({
      surface: true,
      withIcon: this.hasIcon,
      pressed,
    })
    const icon = this.renderIcon()
    return html`
      <md-focus-ring part="focus-ring" .control="${this as HTMLElement}"></md-focus-ring>
      <ui-ripple class="ripple" ?disabled="${this.disabled}"></ui-ripple>
      <div class="${containerClasses}" id="container">
        <div class="container"></div>
        <div class="state"></div>
        <div class="content">
          ${trailingIcon ? nothing : icon}
          <slot></slot>
          ${trailingIcon ? icon : nothing}
        </div>
      </div>
    `
  }

  protected renderIcon(): TemplateResult {
    return html`<slot name="icon" @slotchange="${this.handleSlotChange}"></slot>`
  }
}
