import { ContainerElement, type BaseStyle, type ChildrenFn, type DrawableStyle, type ElementStyle, type UIElementBase, type UINodeChild } from "./UINode"
import { disposeBinding } from "../core/signals"
import type { UIScreen } from "./UIScreen"

// UIPager — the system's ONE screen-navigation element (docs/pager-component-plan.md): sibling
// tabs that swipe natively side to side, with a native navigation STACK per tab. `push` opens a
// screen on top of the currently open tab (within the pager); `pop` / the interactive edge
// back-swipe unwinds it; `replace` swaps the top. A pager with one tab is a plain stack.
//
// Reachable from any scope as a global: `UIPager.push(screen)` pushes onto the pager that owns
// the current screen (UIPager.current) — no reference threading into child screens. Router stays
// distinct: `Router.push` opens ON TOP of the pager (bars and all), the pager navigates within.
//
// Wire node type "pager": children are separate layout roots (full UIScreens), NOT yoga children.
// The wire shape is `children` flat in TAB-MAJOR order (tab 0's stack root→top, then tab 1's, …)
// partitioned by `_lens` (per-tab stack lengths) with `_index` = selected tab. State is mutated
// BEFORE each command, so the host reconciles against the node; commands only name the native
// transition to play. Host-driven changes come back via `_hostDidSelect` / `_hostDidPop` — both
// idempotent no-ops when the logical state already matches.

/** Styles a pager accepts — layout box + drawable (background, borders, …). Pages style
 *  themselves; the pager only owns its own slot. */
export type UIPagerStyle = ElementStyle & DrawableStyle

/** The pagers whose screen is currently visible, outermost → innermost. `UIPager.current` is the
 *  last (innermost). Maintained by each owned screen's onOpen/onClose via _activate/_deactivate. */
const _activeStack: PagerElement[] = []

/**
 * The screen-navigation element: sibling **tabs** that swipe natively side to side, each with its
 * own navigation **stack**. `push` opens a screen on top of the current tab (within the pager);
 * `pop` / the native edge back-swipe unwinds it. A one-tab pager is a plain navigation stack.
 *
 * ```ts
 * const pager = UIPager([ HomeScreen(), ProfileScreen() ])   // two tabs, side by side
 * Router.init(UIScreen([ pager.style({ flexGrow: 1 }), tabBar ]))
 *
 * // from anywhere inside a hosted screen — no reference threading:
 * UIPager.push(PostScreen(id))
 * ```
 *
 * To open something over *everything* (above bars, above the pager), use `Router.push` instead.
 */
export interface UIPager extends UIElementBase<UIPagerStyle, DrawableStyle & BaseStyle> {
  readonly type: "pager",

  /** Replace the pager's tabs — one screen per tab, in order. Every tab becomes a fresh
   *  one-screen stack (pushed screens are dropped) and the selection is clamped into range;
   *  screens that were already tabs keep their state. Pass a function for reactive tabs. */
  setContent(tabs: UIScreen[] | (() => UIScreen[])): this,

  /** Switch to tab `index`. **Instant by default** — the tab-bar convention (tabs are parallel
   *  modes, not neighbors); pass `animated: true` for the direction-aware slide of top-tab /
   *  pager UIs. Out-of-range and same-index calls are no-ops. Tabs keep their stacks: selecting
   *  away and back restores the tab exactly where it was, pushed screens included. */
  select(index: number, animated?: boolean): this,
  /** The selected tab index (0-based). */
  readonly index: number,
  /** Fires when the selected tab changes and has settled — a user swipe or `select()`. */
  onSelect(callback: (index: number) => void): this,

  /** Push `screen` on top of the **current tab** — slides in natively; the edge back-swipe (or
   *  `pop()`) removes it. Other tabs are unaffected. */
  push(screen: UIScreen): this,
  /** Pop the current tab's top screen (no-op at the tab root — the root never pops). The
   *  programmatic twin of the native edge back-swipe. */
  pop(): this,
  /** Unwind the current tab back to its root screen in one step. */
  popToRoot(): this,
  /** Swap the current tab's top screen for `screen` without growing the stack (at depth 1 this
   *  swaps the tab's root). */
  replace(screen: UIScreen): this,
  /** The current tab's screens, root → top (read-only snapshot). */
  readonly stack: UIScreen[],
  /** The current tab's stack depth — 1 means only the root. While `depth > 1`, swiping between
   *  tabs is disabled (the horizontal gesture belongs to the back-swipe). */
  readonly depth: number,
  /** Fires when the current tab's depth changes — push, pop, replace, or a back-swipe. */
  onChange(callback: (depth: number) => void): this,
}

export class PagerElement extends ContainerElement<"pager"> {
  /** Per-tab stack lengths — partitions the flat tab-major `children`. Named `_*` so chisel
   *  keeps it (the host reads it live). */
  _lens: number[]
  /** Selected tab. */
  _index = 0
  // >0 while a screen owned by this pager is the visible one (its onOpen fired, onClose hasn't).
  // Only one owned screen is ever open, so this is effectively 0/1 — the count only makes the
  // transient during a transition's old-close/new-open pair order-independent.
  _openCount = 0
  private scl: ((index: number) => void)[] = []   // onSelect listeners
  private ncl: ((depth: number) => void)[] = []   // onChange listeners

  constructor(style: UIPagerStyle | null, tabs: UIScreen[]) {
    // flexShrink: 1 default (SDK-owned, like UIScrollable's): pages are separate layout roots
    // sized to the pager's box, so yoga sees no shrinkable content — without this a pager in a
    // flex column overflows the screen instead of leaving room for the tab bar.
    super("pager", { flexShrink: 1, ...style }, tabs as any)
    this._lens = tabs.map(() => 1)
    for (const s of this.children) this._track(s)
  }

  /** Start offset of tab `i`'s segment in the flat `children`. */
  private _offset(i: number): number {
    let off = 0
    for (let t = 0; t < i; t++) off += this._lens[t]
    return off
  }

  get index(): number {
    return this._index
  }
  get stack(): UIScreen[] {
    const off = this._offset(this._index)
    return this.children.slice(off, off + (this._lens[this._index] ?? 0))
  }
  get depth(): number {
    return this._lens[this._index] ?? 0
  }

  onSelect(callback: (index: number) => void): this {
    this.scl.push(callback)
    return this
  }
  onChange(callback: (depth: number) => void): this {
    this.ncl.push(callback)
    return this
  }

  select(index: number, animated = false): this {
    if (index < 0 || index >= this._lens.length || index === this._index) return this
    this._index = index
    if (this._id !== 0) _creatorUI.command(this, "select", animated)
    this._emitSelect()
    return this
  }

  push(screen: UIScreen): this {
    if (this._lens.length === 0) {
      console.warn("UIPager.push: pager has no tabs")
      return this
    }
    this.children.splice(this._offset(this._index) + this._lens[this._index], 0, screen)
    this._lens[this._index]++
    this._track(screen)
    if (this._id !== 0) _creatorUI.command(this, "push")
    this._emitChange()
    return this
  }

  pop(): this {
    if ((this._lens[this._index] ?? 0) <= 1) return this
    this.children.splice(this._offset(this._index) + this._lens[this._index] - 1, 1)
    this._lens[this._index]--
    if (this._id !== 0) _creatorUI.command(this, "pop")
    this._emitChange()
    return this
  }

  popToRoot(): this {
    const len = this._lens[this._index] ?? 0
    if (len <= 1) return this
    this.children.splice(this._offset(this._index) + 1, len - 1)
    this._lens[this._index] = 1
    if (this._id !== 0) _creatorUI.command(this, "popToRoot")
    this._emitChange()
    return this
  }

  replace(screen: UIScreen): this {
    if (this._lens.length === 0) {
      console.warn("UIPager.replace: pager has no tabs")
      return this
    }
    this.children[this._offset(this._index) + this._lens[this._index] - 1] = screen
    this._track(screen)
    if (this._id !== 0) _creatorUI.command(this, "replace")
    this._emitChange()
    return this
  }

  // ── Children ARE the tabs ──────────────────────────────────────────────────────────────────
  // Every inherited container mutation would send a generic setContent/insertNode/removeNode wire
  // op, and a host must never run those on a pager: its children are page ROOTS the host owns
  // (each laid out independently at the pager's box), not yoga children — the generic path frees
  // the live pages and rebuilds them as ordinary children stacked on the first tab. So each one is
  // overridden to mutate the element first (children, `_lens`, `_index`) and then notify over the
  // same command channel every other pager op uses, where the host reconciles against the node.
  //
  // Content set this way is always one screen per tab: a per-tab STACK only ever grows through
  // push/replace, so `_lens` resets to all-ones here.
  private _rebuild(children: any[]): this {
    const tabs = children.filter(Boolean)
    const prevIndex = this._index
    this.children.length = 0
    this.children.push(...tabs)
    this._lens = tabs.map(() => 1)
    if (this._index >= tabs.length) this._index = Math.max(0, tabs.length - 1)
    for (const s of tabs) this._track(s)
    if (this._id !== 0) _creatorUI.command(this, "content")
    if (this._index !== prevIndex) this._emitSelect()
    this._emitChange()
    return this
  }

  setContent(children: UINodeChild[] | ChildrenFn): this {
    if (typeof children === "function") {
      this._bindChildren(children)   // reactive children land in _reconcile below
      return this
    }
    disposeBinding(this, "#children")   // static content overrides a reactive binding
    return this._rebuild(children as any[])
  }
  append(...nodes: UINodeChild[]): this {
    return this._rebuild([...this.children, ...nodes])
  }
  insert(index: number, ...nodes: UINodeChild[]): this {
    const next = this.children.slice()
    next.splice(index, 0, ...(nodes as any[]))
    return this._rebuild(next)
  }
  remove(...nodesToDelete: UIScreen[]): this {
    const drop = new Set<any>(nodesToDelete)
    return this._rebuild(this.children.filter((c: any) => !drop.has(c)))
  }
  _reconcile(next: UINodeChild[]): void {
    this._rebuild(next as any[])
  }

  // Host-driven tab change (committed swipe): the native pager already moved — sync the logical
  // index + fire onSelect. Named `_*` so chisel keeps it (hosts call it).
  _hostDidSelect(index: number): void {
    if (index < 0 || index >= this._lens.length || index === this._index) return
    this._index = index
    this._emitSelect()
  }

  // Host-driven pop on the current tab (interactive edge back-swipe): the native stack already
  // popped — sync the logical stack + fire onChange. Idempotent, safe after every transition.
  _hostDidPop(depth: number): void {
    if (depth < 1) depth = 1
    const len = this._lens[this._index] ?? 0
    if (depth >= len) return
    this.children.splice(this._offset(this._index) + depth, len - depth)
    this._lens[this._index] = depth
    this._emitChange()
  }

  private _emitSelect(): void {
    const i = this._index
    for (const cb of this.scl) cb(i)
  }
  private _emitChange(): void {
    const d = this._lens[this._index] ?? 0
    for (const cb of this.ncl) cb(d)
  }

  // Register a screen for `current` tracking: while a screen owned by this pager is the visible
  // top, this pager is active. The hook reads screen._pagerOwner LIVE (not a captured `this`), so
  // re-homing a screen into another pager activates the right one.
  private _track(screen: any): void {
    screen._pagerOwner = this
    if (!screen._pagerHooked) {
      screen._pagerHooked = true
      screen.onOpen(() => screen._pagerOwner?._activate())
      screen.onClose(() => screen._pagerOwner?._deactivate())
    }
  }
  _activate(): void {
    if (++this._openCount === 1 && _activeStack.indexOf(this) < 0) {
      _activeStack.push(this)
    }
  }
  _deactivate(): void {
    if (--this._openCount <= 0) {
      this._openCount = 0
      const i = _activeStack.indexOf(this)
      if (i >= 0) _activeStack.splice(i, 1)
    }
  }
}

const isScreenArg = (v: any): boolean => v != null && typeof v === "object" && v.type === "screen"

/** Callable factory + ambient statics. The name `UIPager` is a value (this factory) and a type
 *  (the instance interface above) — a standard value/type merge. */
interface UIPagerConstructor {
  /** Tabs — each argument is a screen or an array of screens (arrays flatten one level, falsy
   *  entries are skipped). One screen → a plain navigation stack rooted at it (push/pop, no tab
   *  swiping); several → swipeable sibling tabs, fixed at construction, each starting as its own
   *  one-screen stack. */
  (...tabs: (UIScreen | UIScreen[] | false | null | undefined)[]): UIPager
  /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
  (style: UIPagerStyle, content?: UIScreen | UIScreen[]): UIPager
  /** The pager that owns the currently visible screen, or `null` when the visible screen isn't
   *  hosted by one. With pagers nested inside pages, resolves to the **innermost** — so ambient
   *  calls act on the pager the user is actually looking at. */
  readonly current: UIPager | null
  /** `UIPager.current.push(screen)` — push onto the pager owning the visible screen, no
   *  reference threading. No-op with a console warning when there is no current pager. */
  push(screen: UIScreen): void
  /** `UIPager.current.pop()` — pop the visible pager's current tab (no-op at the tab root, or
   *  with a warning when there is no current pager). */
  pop(): void
  /** `UIPager.current.popToRoot()` — unwind the visible pager's current tab to its root. */
  popToRoot(): void
}

function pagerFactory(...args: any[]): UIPager {
  let style: any = null
  let start = 0
  const a0 = args.length > 0 ? args[0] : undefined
  if (a0 != null && typeof a0 === "object" && !Array.isArray(a0) && !isScreenArg(a0)) {
    style = a0
    start = 1
  }
  const tabs: any[] = []
  for (let i = start; i < args.length; i++) {
    const a = args[i]
    if (Array.isArray(a)) {
      for (let j = 0; j < a.length; j++) if (a[j]) tabs.push(a[j])
    } else if (a) {
      tabs.push(a)
    }
  }
  return new PagerElement(style, tabs) as unknown as UIPager
}

/**
 * Create a pager — THE screen-navigation element: an array of screens becomes swipeable sibling
 * tabs, a single screen becomes a plain stack. Navigate within it via `pager.push/pop/replace`
 * (per-tab) and `pager.select(i)` (between tabs), or from any child screen via the ambient
 * `UIPager.push` / `UIPager.pop` / `UIPager.current`. Pages are full `UIScreen`s (own
 * `onOpen`/`onClose`); give the pager `flexGrow: 1` or a size so it doesn't
 * collapse. `Router` stays the way to open a destination ON TOP of the pager.
 */
export const UIPager = pagerFactory as unknown as UIPagerConstructor

const _resolveCurrent = (): UIPager | null => {
  const cur = _activeStack[_activeStack.length - 1]
  if (!cur) {
    console.warn("UIPager.push/pop: no active pager on the current screen")
    return null
  }
  return cur as unknown as UIPager
}

// The ambient statics. These are top-level statements in a `sideEffects: false` module, so they only
// survive bundling because chisel treats a statement that *mutates a binding this module declares*
// as part of that binding's declaration (`ItemKind::AttachedTo`, chisel >= 0.4.2) — kept iff
// `UIPager` is reached, shaken away with it otherwise. Under chisel <= 0.4.1 they were dropped
// outright and `UIPager.push` arrived as `undefined` at runtime, in bundled builds only; don't lower
// the pinned `@letary/chisel` back past 0.4.2. Covered by sdk/tests/compile.test.ts.
// `Object.defineProperty` for `current`, not `Object.assign({ get current() {} })` — Object.assign
// *invokes* the getter and copies the resulting value, freezing it at null forever.
Object.defineProperty(UIPager, "current", {
  get: () => (_activeStack[_activeStack.length - 1] ?? null) as unknown as UIPager | null,
})
;(UIPager as any).push = (screen: UIScreen) => { _resolveCurrent()?.push(screen) }
;(UIPager as any).pop = () => { _resolveCurrent()?.pop() }
;(UIPager as any).popToRoot = () => { _resolveCurrent()?.popToRoot() }
