import { _bumpNavEpoch, _descOf, _navSupported, _setCurrent, Presentable, type Transition } from "./presentable"
import { _navDefaults } from "./theme"

type RouterOptions = {
  showDefaultBackButton?: boolean
}

type NavigateOptions = {
  /** Transition to play. Defaults: `"push"` for push, `"pop"` for pop; replace follows the
   *  theme's `replaceTransition` (`"none"` out of the box). */
  transition?: Transition
}

/** Stack navigation over `Presentable` destinations. `init` once with the home page, then
 *  `push`/`replace`/`pop`; the back gesture/button pops natively. A direct `page.open()` suspends
 *  the router until `restore()`. */
type Router = {
  /** The top of the router stack (not necessarily what's on screen — see `Presentable.current`). */
  get current(): Presentable
  /** Mount the router with `homePage` as the stack root — the app's navigation entry point. */
  init(homePage: Presentable, opts?: RouterOptions): void
  /** Push a destination onto the stack — a `UIScreen`, a `Scene`, a `Scene2D`, a `NativeView`,
   *  or a `UIVideo`. The back gesture/button pops it. Destinations with async preparation
   *  (`ARScene`: camera permission + warm render) return a Promise — the current page stays
   *  visible until they're ready, and a rejection (permission denied / superseded) leaves the
   *  stack untouched. */
  push(page: Presentable, opts?: NavigateOptions): void | Promise<void>
  /** Swap the top of the stack for `page` — no back entry to the replaced one. */
  replace(page: Presentable, opts?: NavigateOptions): void | Promise<void>
  /** Pop the top destination (the programmatic twin of the back gesture); `to` unwinds to a
   *  stack index in one step. */
  pop(to?: number, opts?: NavigateOptions): void
  /** Blank the router's page, keeping the stack — `restore()` brings it back. A destination
   *  opened directly stays visible. */
  hide(): void
  /** Re-present the stack top after `hide()` or a direct `open()` suspended the router. */
  restore(): void

  /** `"change"` fires on every stack change — pushes, pops, and native back gestures — with the
   *  new top destination. */
  addEventListener(channel: "change", callback: (page: Presentable) => void): void
  removeEventListener(channel: "change", callback: (page: Presentable) => void): void
}

let currentPage: Presentable | null = null
let onChange = (page: any) => {
  // Hosts hand back the wire object: the screen itself, or a descriptor carrying `_p`.
  currentPage = page && page._p ? page._p : page
  // The router's page is the visible destination (hosts fire this on every stack change,
  // including back gestures) — keep Presentable.current in sync.
  _setCurrent(currentPage)
  for (let callback of (Router as any)._changeListeners) {
    callback(currentPage)
  }
}

// Non-screen destinations have no legacy bridge to fall back to — surface that clearly instead
// of letting an old host try to build a screen out of a scene descriptor.
const wireDesc = (page: Presentable): object => {
  const desc = _descOf(page)
  if (desc !== page && !_navSupported()) {
    throw new Error("This host does not support scene/native-view navigation (requires _creatorUI.openView)")
  }
  return desc
}

export const Router: Router = /*#__PURE__*/ Object.freeze({

  init(homePage: Presentable, opts: RouterOptions = {}): void {
    _bumpNavEpoch()
    _creatorUI.routerOpen(wireDesc(homePage), onChange, opts.showDefaultBackButton ?? false)
    currentPage = homePage
    _setCurrent(homePage)
  },
  push(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
    const desc = wireDesc(page)   // throws early on unsupported hosts, before any async work
    const go = () => {
      _bumpNavEpoch()
      // Legacy hosts ignore the extra transition argument.
      _creatorUI.routerPush(desc, opts?.transition ?? "push")
      _setCurrent(page)   // optimistic — host onChange confirms (async on native hosts)
    }
    // A preparing destination (ARScene) keeps the current page visible until it's ready; a
    // rejection (camera denied / superseded) propagates to the caller and nothing is pushed.
    const prep = page._prepare?.()
    return prep ? prep.then(go) : go()
  },
  replace(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
    const desc = wireDesc(page)
    const go = () => {
      _bumpNavEpoch()
      _creatorUI.routerReplace(desc, opts?.transition ?? _navDefaults.replace)
      _setCurrent(page)   // optimistic — host onChange confirms (async on native hosts)
    }
    const prep = page._prepare?.()
    return prep ? prep.then(go) : go()
  },
  pop(to?: number, opts?: NavigateOptions): void {
    _bumpNavEpoch()
    _creatorUI.routerPop(to ?? -1, opts?.transition ?? "pop")
  },
  hide() {
    _bumpNavEpoch()
    _creatorUI.routerHide()
    // hide() only blanks the router's own page — a directly-opened destination stays visible.
    if (Presentable.current === currentPage) _setCurrent(null)
  },
  restore() {
    _bumpNavEpoch()
    _creatorUI.routerRestore()
    _setCurrent(currentPage)   // the stack top is visible again
  },

  get current(): Presentable {
    return currentPage!
  },

  _changeListeners: [] as any[],
  addEventListener(channel: "change", callback: (page: Presentable) => void): void {
    if (channel === "change") {
      this._changeListeners.push(callback)
    }
  },
  removeEventListener(channel: "change", callback: (page: Presentable) => void): void {
    if (channel === "change") {
      let index = this._changeListeners.indexOf(callback)
      if (index >= 0) {
        this._changeListeners.splice(index, 1)
      }
    }
  }
})
