import { ModalElement, type UIModalStyle } from "./UIModal"
import type { UIWidget } from "./UIWidget"
import { buildUI, type ChildrenFn, type UIChildArg, type UINodeChild } from "./UINode"

export type UIBottomSheetStyle = UIModalStyle

const SHEET_ANIM_MS = 250

/**
 * A bottom sheet: a `UIWidget` pinned to the bottom edge. By default it is **content-sized** —
 * as tall as what's inside (capped at the screen), one position, drag down to dismiss (the
 * action-sheet shape). Call `detents()` for the multi-position map-app model instead: snap
 * positions the user drags between on native hosts. Ships as a widget node on the wire (no new
 * node type — docs/bottom-sheet-plan.md): hosts without sheet support show a static bottom panel.
 *
 * With detents, content lays out at the HIGHEST detent's size; lower detents show the top
 * slice. The scrim, `onOpen`/`onClose`, back-button and scrim-tap dismissal all come from the
 * modal machinery.
 *
 * ```ts
 * const actions = UIBottomSheet({ bgColor: "#1c1c1e", borderRadius: 16 }, [rows])   // content-sized
 *
 * const mapSheet = UIBottomSheet({ bgColor: "#1c1c1e", borderRadius: 16 }, [results])
 *   .detents([0.3, 0.6, 1])
 *   .onDetentChange(i => map.setPadding(...))
 * ```
 */
export interface UIBottomSheet extends UIWidget {
  /** Snap positions as fractions of screen height, ascending. Also sizes the sheet: its box is
   *  the last (highest) detent; set this before `show()`. Without a `detents()` call the sheet
   *  is CONTENT-SIZED instead — one position, as tall as its children (up to the screen). */
  detents(list: number[]): this,
  /** Snap to a detent (animated). Clamped to the detents range; fires `onDetentChange`. */
  setDetent(index: number): void,
  /** Current detent index. */
  readonly detent: number,
  /** Fires on every settle: a finger snap on native hosts, or a `setDetent()` call. */
  onDetentChange(callback: (index: number) => void): this,

  /** Mount and slide in to the current detent. No-op while already open. */
  show(): void,
  /** Slide out, fade the scrim, then unmount. No-op while closed. */
  hide(): void,
  /** True from `show()` until `hide()` starts. */
  readonly isOpen: boolean,

  /** Called when `show()` mounts the sheet. */
  onOpen(callback: () => void): this,
  /** Called when the sheet starts closing — scrim tap, back button, drag-dismiss, or `hide()`. */
  onClose(callback: () => void): this,
  /** `dismissible(false)`: scrim taps, the back button and dragging below the lowest detent no
   *  longer close the sheet (a drag collapses to the lowest detent instead). Default `true`. */
  dismissible(enabled: boolean): this,
}

export class BottomSheetElement extends ModalElement {
  _detents: number[] = [1]
  _detent = 0
  /** Host-facing (the `sl`/`otl` contract): hosts read `dtl` at mount and call it with the
   *  settled detent index for GESTURE settles only — programmatic `setDetent` fires JS-side.
   *  `-1` = dragged below the lowest detent while dismissible. */
  readonly dtl: any[] = []
  readonly _detentListeners: ((index: number) => void)[] = []

  constructor(style: UIBottomSheetStyle, children: UINodeChild[] | ChildrenFn) {
    // No default height: an absolute node with only bottom set is CONTENT-sized by yoga. A
    // single detent makes the geometry height-independent on every host (shown = offset 0,
    // hidden = own height), so content mode needs no host support beyond the detents path.
    super({ left: 0, right: 0, bottom: 0, maxHeight: "100%", sheetDetents: "1", ...style } as UIModalStyle, children)
    // The inherited show/hide pose animates ONLY the scrim: sheet geometry rides the
    // sheetDetent style key, never the pose system.
    this._transition = { duration: SHEET_ANIM_MS }
    this.dtl.push((index: number) => {
      if (index < 0) { this.hide(); return }
      this._detent = index
      for (const callback of this._detentListeners) callback(index)
    })
  }

  detents(list: number[]): this {
    const sorted = [...list].filter(d => d > 0).sort((a, b) => a - b)
    if (sorted.length === 0) return this
    this._detents = sorted
    if (this._detent >= sorted.length) this._detent = sorted.length - 1
    this.style({
      height: `${sorted[sorted.length - 1] * 100}%`,
      sheetDetents: sorted.join(","),
    } as UIBottomSheetStyle)
    return this
  }

  get detent(): number {
    return this._detent
  }

  setDetent(index: number): void {
    const clamped = Math.max(0, Math.min(Math.floor(index), this._detents.length - 1))
    this._detent = clamped
    if (!this._open) return                     // applied by the next show()
    this.style({ sheetDetent: clamped } as UIBottomSheetStyle)
    for (const callback of this._detentListeners) callback(clamped)
  }

  onDetentChange(callback: (index: number) => void): this {
    this._detentListeners.push(callback)
    return this
  }

  show(): void {
    if (this._open) return
    // Before the mount, so the built node carries the target and hosts play the entrance to it.
    this.style({ sheetDetent: this._detent } as UIBottomSheetStyle)
    super.show()
  }

  hide(): void {
    if (!this._open) return
    this.style({ sheetDetent: -1 } as UIBottomSheetStyle)   // hosts slide out; unmount follows
    super.hide()
  }

  dismissible(enabled: boolean): this {
    super.dismissible(enabled)
    this.style({ sheetDismissible: enabled ? 1 : 0 } as UIBottomSheetStyle)
    return this
  }
}

const makeBottomSheet = (style: any, children: any) => new BottomSheetElement(style, children)

/** Create a bottom sheet (see {@link UIBottomSheet}) — content-sized by default; call
 *  `.detents([...])` for snap positions. Same argument forms as `UIColumn`. */
export function UIBottomSheet(...children: UIChildArg[]): UIBottomSheet;
export function UIBottomSheet(children: UINodeChild[] | ChildrenFn): UIBottomSheet;
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
export function UIBottomSheet(style: UIBottomSheetStyle, children?: UINodeChild[] | ChildrenFn): UIBottomSheet;
export function UIBottomSheet(...args: any[]): UIBottomSheet {
  return buildUI(args, null, makeBottomSheet)
}
