import { type UIWidget, type UIWidgetStyle, WidgetElement } from "./UIWidget"
import { buildUI, type BaseStyle, type ChildrenFn, type Color, type DrawableStyle, type UIChildArg, type UINodeChild } from "./UINode"

export type UIModalStyle = UIWidgetStyle

/**
 * The modal's hidden pose: `show()` animates from it, `hide()` animates to it. `duration` (ms)
 * applies to both directions.
 */
export type UIModalTransition = DrawableStyle & BaseStyle & {
  overlayColor?: Color | null,
  duration?: number,
}

const MODAL_ANIM_MS = 200

/**
 * A dialog: a `UIWidget` with the modal boilerplate built in. Comes with a scrim
 * (`overlayColor: "rgba(0, 0, 0, 0.5)"` unless overridden), animates on `show()`/`hide()`
 * (a 200 ms fade by default — swap the pose via `transition()`; the exit plays with
 * `commit: false`, so the style is intact for the next show), and closes itself on a scrim tap
 * or the Android back button (disable via `dismissible(false)`). Everything else is a plain
 * widget — same styling, children, touch and `attachTo()` surface; create it **once** at module
 * scope and reuse it.
 */
export interface UIModal extends UIWidget {
  /** Mount as an overlay and play the entrance transition. No-op while already open. */
  show(): void,
  /** Play the exit transition, then unmount. No-op while closed or already closing. */
  hide(): void,
  /** True from `show()` until `hide()` starts (already false during the exit animation). */
  readonly isOpen: boolean,

  /**
   * Replace the show/hide animation. `hidden` is the off-screen pose — `show()` animates from
   * it, `hide()` animates to it. Default `{ opacity: 0, duration: 200 }` (a fade); the pose
   * replaces it wholesale, so a slide without a fade is just the slide. The scrim keeps its own
   * fade unless the pose sets `overlayColor` itself.
   *
   * ```ts
   * sheet.transition({ transform: "translateY(480px)", duration: 250 })   // bottom sheet
   * ```
   */
  transition(hidden: UIModalTransition): this,

  /** Called when `show()` mounts the modal. */
  onOpen(callback: () => void): this,
  /** Called when the modal starts closing — scrim tap, back button, or a `hide()` call. */
  onClose(callback: () => void): this,
  /** `dismissible(false)` keeps scrim taps and the back button from closing the modal
   *  (forced-choice dialogs). Default `true`. */
  dismissible(enabled: boolean): this,
}

export class ModalElement extends WidgetElement {
  _open = false
  _dismissible = true
  _transition: UIModalTransition = { opacity: 0 }
  _closeTimer?: ReturnType<typeof setTimeout>
  readonly _openListeners: (() => void)[] = []
  readonly _closeListeners: (() => void)[] = []

  constructor(style: UIModalStyle, children: UINodeChild[] | ChildrenFn) {
    super("widget", { overlayColor: "rgba(0, 0, 0, 0.5)", ...style }, children)
    this.onOverlayTap(() => { if (this._dismissible) this.hide() })
    this.onBackPressed(() => { if (this._dismissible) this.hide() })
  }

  // The full animate argument: hidden pose + duration, plus the scrim fade — only when the
  // modal actually has a scrim (overlayColor: null opts out; animating a null scrim would
  // create the intercepting layer) and the pose doesn't drive the scrim itself.
  private _pose(extra?: Record<string, unknown>): Record<string, unknown> {
    const pose: Record<string, unknown> = { duration: MODAL_ANIM_MS, ...this._transition, ...extra }
    const overlayColor = (this._style as UIModalStyle).overlayColor
    if (pose.overlayColor === undefined && overlayColor !== undefined && overlayColor !== null) {
      pose.overlayColor = "transparent"
    }
    return pose
  }

  private get _duration(): number {
    return this._transition.duration ?? MODAL_ANIM_MS
  }

  transition(hidden: UIModalTransition): this {
    this._transition = hidden
    return this
  }

  show(): void {
    if (this._open) return
    if (this._closeTimer !== undefined) {   // reopened mid exit: finish that hide now
      clearTimeout(this._closeTimer)
      this._closeTimer = undefined
      super.hide()
    }
    this._open = true
    super.show()
    this.animateFrom(this._pose())
    for (const callback of this._openListeners) callback()
  }

  hide(): void {
    if (!this._open) return
    this._open = false
    this.animateTo(this._pose({ commit: false }))
    this._closeTimer = setTimeout(() => { this._closeTimer = undefined; super.hide() }, this._duration)
    for (const callback of this._closeListeners) callback()
  }

  get isOpen(): boolean {
    return this._open
  }

  onOpen(callback: () => void): this {
    this._openListeners.push(callback)
    return this
  }
  onClose(callback: () => void): this {
    this._closeListeners.push(callback)
    return this
  }
  dismissible(enabled: boolean): this {
    this._dismissible = enabled
    return this
  }
}

const makeModal = (style: any, children: any) => new ModalElement(style, children)

/** Create a modal dialog (see {@link UIModal}) — create once at module scope, then
 *  `show()`/`hide()`. Same argument forms as `UIColumn`. */
export function UIModal(...children: UIChildArg[]): UIModal;
export function UIModal(children: UINodeChild[] | ChildrenFn): UIModal;
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
export function UIModal(style: UIModalStyle, children?: UINodeChild[] | ChildrenFn): UIModal;
export function UIModal(...args: any[]): UIModal {
  return buildUI(args, null, makeModal)
}
