import { type CSSResultOrNative, html, nothing, type PropertyValues, type TemplateResult, adoptStyles } from 'lit'
import { property, query, queryAssignedElements, queryAssignedNodes, state } from 'lit/decorators.js'
import { type ClassInfo, classMap } from 'lit/directives/class-map.js'
import { UiElement } from '../../UiElement.js'
import { isDisabled, setDisabled } from '../../../lib/disabled.js'
import type UiButton from '../../button/internals/button.js'
import type { TypedEvents } from '../../../core/types.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { SyntheticSubmitEvent } from '../../../events/SyntheticSubmitEvent.js'
import '../../button/ui-button.js'

export interface UiDialogClosingReason {
  /**
   * Whether the dialog was cancelled by either activating the `dismiss` button
   * or by pressing escape.
   */
  cancelled: boolean
  /**
   * This is used in cases when the dialog has more complex purpose.
   * This is the value expected from the dialog.
   */
  value?: unknown
}

interface DialogEventMap {
  close: CustomEvent<UiDialogClosingReason>
}

export type RenderFunction = () => TemplateResult

/**
 * Styled dialog using a native `<dialog>` element under the hood.
 * Note, since native dialog renders in the top layer it is not necessary
 * to place the dialog in the `<body>`.
 *
 * **Using Buttons**
 *
 * The dialog automatically recognizes buttons with values `confirm` and `dismiss`
 * to close the dialog and dispatch the `close` event. The event has additional
 * closing reason detail.
 *
 * ```javascript
 * <ui-button color="text" value="dismiss">Cancel</ui-button>
 * <ui-button color="text" value="confirm">Take action</ui-button>
 * ```
 *
 * ```javascript
 * <button value="dismiss">Cancel</button>
 * <button value="confirm">Take action</button>
 * ```
 *
 * The detail object of the `close` event has the following properties:
 * - cancelled - Whether the dialog was cancelled by either activating the `dismiss` button or by pressing escape.
 *
 * The `close` event is only dispatched when the user interact with the dialog. Imperative control of the
 * dialog won't trigger the close button.
 *
 * ** Full example**
 *
 * ```javascript
 * <ui-dialog modal>
 *  <ui-icon slot="icon" icon="delete"></ui-icon>
 *
 *  <span slot="title">Delete photos?</span>
 *  <p>This action will permanently remove the selected pictures from your account.</p>
 *
 *  <ui-button color="text" slot="button" value="dismiss" type="text">Cancel</ui-button>
 *  <ui-button color="text" slot="button" value="confirm" type="text">Confirm</ui-button>
 * </ui-dialog>
 * ```
 *
 * @slot - The slot for the content of the dialog.
 * @slot icon - The slot to place the dialog icon
 * @slot title - The slot to place the dialog title. Do not put elements here, just the text.
 * @slot button - The slot to place the dialog buttons. Use the `confirm` or `dismiss`
 *  buttons to automatically close the dialog.
 * @fires close - A non-bubbling, non-cancellable event with the `UiDialogClosingReason` as the detail.
 */
export default class UiDialog extends UiElement implements TypedEvents<DialogEventMap> {
  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)
  }

  /**
   * Opens the dialog as modal when toggling dialog's open state.
   *
   * Setting this value after the dialog was opened has no effect.
   *
   * @attribute
   */
  @property({ type: Boolean }) accessor modal = false

  /**
   * Toggles visibility of the dialog.
   * Note, the dialog is opened asynchronously after the update is performed.
   * @attribute
   */
  @property({ type: Boolean }) accessor open = false

  /**
   * Imperative access to create a dismiss button.
   * When set this will render a dismiss button at the end of the buttons line, before the `confirmLabel` button.
   * @attribute
   */
  @property({ type: String }) accessor dismissLabel: string | undefined

  /**
   * Imperative access to create a confirm button.
   * When set this will render a confirm button at the end of the buttons line, after the `dismissLabel` button.
   * @attribute
   */
  @property({ type: String }) accessor confirmLabel: string | undefined

  /**
   * Part of the imperative access to the element.
   * When set, it wraps the content in a `<form>` element.
   * When this is enabled the following will happen:
   * - The `confirm` button will get `type="submit"`
   * - The form `method` attribute is set to `dialog`
   * - The form `id` attribute is set to a random string. You can get it from the `formId` getter.
   * - The dialog will dispatch the same `submit` event as the form.
   * - When the `submit` event dispatched by the dialog gets cancelled, then:
   *  - The original submit event also gets cancelled.
   *  - The default confirm action is not invoked
   *  - The dialog stays opened
   * - When the submit event is not cancelled, then:
   *  - The default confirm action is invoked.
   *  - The dialog is closed.
   */
  @property({ type: Boolean }) accessor useForm: boolean | undefined

  /**
   * Only when `confirmLabel` is set.
   * 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 }) accessor confirmValue: string | undefined

  /**
   * A reference to the underlying dialog element.
   */
  @query('dialog') accessor dialog!: HTMLDialogElement

  @state() protected accessor hasIcon = false

  @state() protected accessor hasTitle = false

  @state() protected accessor hasButton = false

  @queryAssignedElements({ flatten: true, slot: 'icon' }) protected accessor icons!: HTMLElement[]

  @queryAssignedNodes({ flatten: true, slot: 'title' }) protected accessor titles!: HTMLElement[]

  @queryAssignedElements({ flatten: true, slot: 'button' }) protected accessor buttons!: HTMLElement[]

  /**
   * To be set by a child class when closing the dialog.
   * This is passed to the close event.
   */
  dialogValue?: unknown

  #formId?: string

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

  @state() accessor #inject: TemplateResult | RenderFunction | undefined

  #pendingStyles?: CSSResultOrNative[]

  constructor() {
    super()

    this.addEventListener('click', this.handleClick)
    this.addEventListener('keydown', this.handleKeyDown)
  }

  /**
   * Allows to inject a template into the internals of the element.
   * This is helpful when working with imperative dialogs.
   * @param content The content to inject into the body.
   */
  inject(content?: TemplateResult | RenderFunction, styles?: CSSResultOrNative[]): void {
    this.#inject = content
    if (styles) {
      if (this.shadowRoot) {
        adoptStyles(this.shadowRoot, styles)
      } else {
        if (!this.#pendingStyles) {
          this.#pendingStyles = []
        }
        this.#pendingStyles.push(...styles)
      }
    }
    if (this.shadowRoot && styles) {
      adoptStyles(this.shadowRoot, styles)
    }
  }

  protected override firstUpdated(): void {
    const styles = this.#pendingStyles
    if (styles) {
      const root = this.shadowRoot as ShadowRoot
      const all = [...root.adoptedStyleSheets, ...styles]
      adoptStyles(this.shadowRoot as ShadowRoot, all)
    }
  }

  override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('useForm') && this.useForm && !this.#formId) {
      const r = (Math.random() + 1).toString(36).substring(7)
      this.#formId = `form-${r}`
    }
  }

  override handleClick(e: MouseEvent): void {
    super.handleClick(e)
    const path = e.composedPath()
    const { buttons } = this
    const button = path.find((i) => buttons.includes(i as HTMLElement)) as HTMLButtonElement | UiButton | undefined
    if (!button) {
      return
    }
    if (button.type === 'submit') {
      // Adds support for forms.
      // When a form's submit button is clicked we yield the flow control to the form.
      // This way the form can handle the submit event.
      e.preventDefault()
      return
    }
    const { value = '' } = button
    this.handleInteraction(value as 'dismiss' | 'confirm')
  }

  override handleKeyDown(e: KeyboardEvent): void {
    super.handleKeyDown(e)
    if (e.key === 'Escape') {
      this.handleInteraction('dismiss')
    }
  }

  override updated(changedProperties: PropertyValues<this>): void {
    if (changedProperties.has('open')) {
      this.#controlVisibility()
    }
    super.updated(changedProperties)
  }

  #controlVisibility(): void {
    const { dialog, modal, open } = this
    if (!dialog) {
      return
    }
    if (open) {
      if (modal) {
        dialog.showModal()
      } else {
        dialog.show()
      }
    } else {
      dialog.close()
    }
  }

  #handleSlotChange(): void {
    const { icons, titles, buttons } = this
    this.hasIcon = !!icons && !!icons.length
    this.hasTitle = !!titles && !!titles.length
    this.hasButton = !!buttons && !!buttons.length
  }

  protected handleInteraction(value: 'dismiss' | 'confirm'): void {
    if (!['dismiss', 'confirm'].includes(value)) {
      return
    }
    this.open = false
    const detail: UiDialogClosingReason = {
      cancelled: value === 'dismiss',
    }
    if (this.dialogValue !== undefined) {
      detail.value = this.dialogValue
    }
    this.dispatchEvent(
      new CustomEvent<UiDialogClosingReason>('close', {
        composed: true,
        detail,
      })
    )
  }

  protected handleDialogClose(): void {
    if (!this.open) {
      return
    }
    this.open = false
    this.handleInteraction('dismiss')
  }

  protected handleDismiss(): void {
    this.handleInteraction('dismiss')
  }

  protected handleConfirm(): void {
    if (!this.useForm) {
      this.handleInteraction('confirm')
    }
  }

  #handleSubmit(event: SubmitEvent): void {
    const form = event.target as HTMLFormElement
    const copy = new SyntheticSubmitEvent(event.type, form, {
      submitter: event.submitter,
      cancelable: true,
      bubbles: false,
      composed: false,
    })
    const dispatched = this.dispatchEvent(copy)
    if (dispatched) {
      this.handleInteraction('confirm')
    } else {
      event.preventDefault()
    }
  }

  override render(): TemplateResult {
    const { useForm } = this
    return html`
      <dialog @close="${this.handleDialogClose}">
        <div class="container">${useForm ? this.#renderFormContent() : this.renderContent()}</div>
      </dialog>
    `
  }

  protected renderContent(): TemplateResult[] | TemplateResult {
    return [this.renderIcon(), this.renderTitle(), this.renderBody(), this.renderButtons()]
  }

  #renderFormContent(): TemplateResult {
    return html`
      <form id="${ifDefined(this.formId)}" method="dialog" @submit="${this.#handleSubmit}">
        ${this.renderContent()}
      </form>
    `
  }

  protected renderIcon(): TemplateResult {
    const classes: ClassInfo = {
      'icon': true,
      'with-icon': this.hasIcon,
    }
    return html`
      <div class="${classMap(classes)}" part="icon">
        <slot name="icon" @slotchange="${this.#handleSlotChange}"></slot>
      </div>
    `
  }

  protected renderTitle(): TemplateResult {
    const classes: ClassInfo = {
      'title': true,
      'with-title': this.hasTitle,
    }
    return html`
      <div class="${classMap(classes)}" part="title">
        <slot name="title" @slotchange="${this.#handleSlotChange}"></slot>
      </div>
    `
  }

  protected renderBody(): TemplateResult {
    let content = this.#inject
    if (typeof content === 'function') {
      content = content()
    }
    const injected = content || nothing
    return html` <div class="content"><slot></slot>${injected}</div> `
  }

  protected renderButtons(): TemplateResult {
    const classes: ClassInfo = {
      'buttons': true,
      'with-buttons': this.hasButton || !!this.confirmLabel || !!this.dismissLabel,
    }
    return html`
      <div class="${classMap(classes)}" part="button">
        <slot name="button" @slotchange="${this.#handleSlotChange}"></slot>
        ${this.#renderDismissButton()} ${this.#renderConfirmButton()}
      </div>
    `
  }

  #renderDismissButton(): TemplateResult | typeof nothing {
    const { dismissLabel } = this
    if (!dismissLabel) {
      return nothing
    }
    return html`
      <ui-button color="text" value="dismiss" class="internal-button" @click="${this.handleDismiss}"
        >${dismissLabel}</ui-button
      >
    `
  }

  #renderConfirmButton(): TemplateResult | typeof nothing {
    const { confirmLabel, confirmValue = 'confirm', useForm } = this
    if (!confirmLabel) {
      return nothing
    }
    const type = useForm ? 'submit' : 'button'
    return html`
      <ui-button
        color="text"
        type="${type}"
        value="${confirmValue}"
        class="internal-button"
        @click="${this.handleConfirm}"
        >${confirmLabel}</ui-button
      >
    `
  }
}
