// The Presentable contract — the one navigation model (docs/navigation-presentable-plan.md).
// Anything that can occupy the app's main slot implements it: UIScreen, Scene (+ARScene),
// Scene2D, NativeView, UIVideo. Exactly one Presentable is visible at a time — a screen no
// longer overlays a scene, it replaces it (with a transition); UI over a scene goes through
// `UIWidget.attachTo(scene)`.
//
// This module is dependency-free on purpose: gl/ and g2/ import it, so it must not pull any UI
// element code into engine-only bundles.

/** Built-in transition catalog. `push`/`pop` are the stacked-navigation pair (slide + parallax +
 *  dim + edge shadow); the rest are simple two-view moves. */
export type TransitionName =
  | "push" | "pop"
  | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom"
  | "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"

/** Start/end pose of one side of a custom transition. `x`/`y` are logical px, or a percentage of
 *  the destination's size (e.g. `"100%"` = one full width to the right). */
export type TransitionTransform = {
  x?: number | `${number}%`
  y?: number | `${number}%`
  scale?: number
  opacity?: number
}

/**
 * A custom transition: declarative poses for the incoming/outgoing destination, played natively.
 * Serializes onto each platform's existing transition machinery — there is no per-frame JS.
 * Fields omitted keep the identity pose. Interactive back-swipe stays system-native (push/pop
 * only); a spec applies to non-interactive transitions.
 */
export interface TransitionSpec {
  /** Duration in ms (default 300). */
  duration?: number
  /** cubic-bezier control points (default ease-out). */
  easing?: [number, number, number, number]
  /** The entering destination animates FROM this pose to identity. */
  incoming?: { from?: TransitionTransform }
  /** The leaving destination animates from identity TO this pose. */
  outgoing?: { to?: TransitionTransform }
  /** Scrim alpha (0..1) painted over whichever destination is underneath. */
  dim?: number
  /** Which side stacks on top during the animation (default "incoming"). On Android, transitions
   *  involving a scene always animate the screen side on top — the scene view itself never moves. */
  onTop?: "incoming" | "outgoing"
}

export type Transition = TransitionName | TransitionSpec

export type PresentOptions = {
  /** Transition to play while this destination replaces the current one (default "none" for a
   *  direct open(); the Router applies its own defaults — push/pop, and the theme's
   *  `replaceTransition` for replace). */
  transition?: Transition
}

/**
 * Anything that can be shown as the app's current destination: a `UIScreen`, a `Scene` /
 * `ARScene`, a `Scene2D`, a `NativeView`, or a `UIVideo`. One Presentable is visible at a time;
 * open it directly (`p.open()` — replaces the current destination, suspending an active Router
 * until `Router.restore()`) or navigate with `Router.push/replace/pop`.
 *
 * `onOpen`/`onClose` are the presentation lifecycle (fired when the destination becomes / stops
 * being the visible one — including router pushes covering it and pops revealing it).
 */
export interface Presentable {
  // Return type covers the implementers: UIScreen → void, Scene2D → this, ARScene → Promise.
  open(options?: PresentOptions): void | this | Promise<void>
  close(): void
  onOpen(callback: () => void): this
  onClose(callback: () => void): this
  onBackPressed(callback: () => void): this
  /** @internal Async work that must finish BEFORE this destination replaces the current one
   *  (camera permission, warm render, session launch — ARScene). The previous destination —
   *  typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
   *  await it and skip the swap entirely if it rejects (permission denied, or superseded by
   *  another navigation that happened while preparing). */
  _prepare?(): Promise<void>
}

// The destination visible right now. Set optimistically by every successful open()/router
// navigation, cleared by close(), and corrected by router onChange (hosts hand the wire desc
// back on every stack change, including back gestures — `_p` resolves it to the SDK instance).
let current: Presentable | null = null
/** @internal Call whenever the visible destination changes. */
export const _setCurrent = (p: Presentable | null): void => { current = p }

/** Runtime companion of the `Presentable` interface (declaration merging): the navigation
 *  state that isn't tied to the Router. */
export const Presentable = /*#__PURE__*/ Object.freeze({
  /** The destination visible right now (a `UIScreen`, `Scene`, `Scene2D`, `NativeView` or
   *  `UIVideo`), or `null` before the first open. Distinct from `Router.current` — that is the
   *  top of the router stack, which stays meaningful while the router is suspended by a direct
   *  `open()`; this is what is actually on screen. `UIWidget.show()` attaches to it. */
  get current(): Presentable | null { return current },
})

// Monotonic navigation epoch: every destination change (open/close/router nav) bumps it. Async
// preparers capture the epoch before their awaits and reject as "superseded" if it moved — so a
// slow AR prepare can't steal the screen from a navigation the user made in the meantime.
let navEpoch = 0
/** @internal */
export const _navEpoch = (): number => navEpoch
/** @internal Call on every action that changes the visible destination. */
export const _bumpNavEpoch = (): void => { navEpoch++ }

/** @internal Does this host implement the Presentable navigation surface? The SDK falls back to
 *  the legacy openScreen/openScene bridges when it doesn't (screens keep working; scene/native
 *  destinations in the Router then throw). */
export const _navSupported = (): boolean =>
  typeof _creatorUI !== "undefined" && typeof _creatorUI.openView === "function"

/** @internal The wire descriptor for a Presentable. Screens (and anything without `_viewDesc`)
 *  travel as themselves; scenes/native views/video provide a stable descriptor object carrying
 *  `_p` (the SDK instance) so hosts can hand it back through router onChange / lifecycle. */
export const _descOf = (p: Presentable): object =>
  typeof (p as any)._viewDesc === "function" ? (p as any)._viewDesc() : (p as object)
