// A 3D scene: a draw set, a Camera, lights and environment. Opening a scene asks the host to bring up
// the 3D engine; only the active scene renders. Grown from the Phase 0 seed. Ported from
// worker/src/Scene.ts (Scene + ARScene), with the camera exposed as scene.camera.
//
// Lights are explicit: a scene gets image-based ambient (IBL) by default; add a Light.sun() for
// direct light + shadows.

import { Color, type ColorInput } from "../core/color"
import { _installAspectFrames } from "../core/Aspect"
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
import type { FetchResponse } from "../runtime/fetch"
import { Camera } from "./Camera"
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
import { Material } from "./Material"
import { Node, nodeRegistry } from "./Node"
import { glState } from "./state"
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"

export type SceneOptions = {
  /** Image-based ambient lighting (default on). */
  ibl?: boolean
  /** IBL intensity (default 20000). */
  environmentIntensity?: number
  /** Bloom post-processing. */
  bloom?: boolean
  bloomIntensity?: number
  /** Skybox / clear color. */
  skybox?: ColorInput
  /** Multisample anti-aliasing. */
  antialias?: boolean
}

// Route aspect update(dt) through the render-synced native phases (render() early = before physics,
// late = after animations / before draw), so followers/cameras are in lockstep with the draw. Done from
// the 3D layer (not core) so a 2D-only bundle never pulls in the 3D bridge. Installed once, lazily on
// the first Scene. On an older wasm without the hooks the installer returns false → setLoop fallback.
let framesInstalled = false
const installRenderSyncedFrames = (): void => {
  if (framesInstalled) return
  framesInstalled = true
  _installAspectFrames((early, late) => {
    if (typeof _creator.setEarlyUpdate !== "function") return false
    _creator.setEarlyUpdate(early)
    _creator.setLateUpdate(late)
    return true
  })
}

export class Scene {
  /** @internal native scene handle. */
  readonly _id: number
  readonly camera: Camera

  /** @internal touch listeners, read by the dispatch system. */
  readonly _clickListeners: Array<(ev: ClickEvent<Node | null>) => void> = []
  readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node | null>) => void> = []

  private _material?: Material

  private static _active: Scene | null = null
  static get active(): Scene | null { return Scene._active }

  constructor(options: SceneOptions = {}) {
    installRenderSyncedFrames()
    const parent = (options as any)._parent as Scene | undefined
    if (parent) {
      this.camera = parent.camera
      this._id = _creator.createOverlayScene(parent._id)
    } else {
      this.camera = new Camera()
      this._id = _creator.createScene()
    }
    this.camera._attach(this._id)

    if (options.ibl !== false) _creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000)
    if (options.bloom) _creator.setBloomOptions(this._id, true, options.bloomIntensity ?? 0.2, 1)
    if (options.skybox !== undefined) _creator.setSkybox(this._id, Color.toPackedRgb(options.skybox))
    if (options.antialias) _creator.setSceneMultiSampleAntiAliasing(this._id, true, 4)
  }

  /** Scene occlusion material. */
  get occlusionMaterial(): Material {
    if (!this._material) this._material = new Material(_creator.getSceneMaterial(this._id))
    return this._material
  }

  set skybox(color: ColorInput) { _creator.setSkybox(this._id, Color.toPackedRgb(color)) }

  setMaterialGlobalParameter(i: number, x: number, y: number, z: number, w: number): void {
    _creator.setMaterialGlobalParameter(this._id, i, x, y, z, w)
  }
  setAntialias(enabled: boolean, scale = 4): void {
    _creator.setSceneMultiSampleAntiAliasing(this._id, enabled, scale)
  }

  add(...nodes: Node[]): this {
    for (const n of nodes) _creator.addEntityToScene(this._id, n.id)
    return this
  }
  remove(...nodes: Node[]): this {
    for (const n of nodes) _creator.removeEntityFromScene(this._id, n.id)
    return this
  }

  createOverlay(options: SceneOptions = {}): Scene {
    return new Scene({ ...options, _parent: this } as any)
  }

  warmRender(): Promise<void> {
    return new Promise<void>((res) => _creator.warmRender(this._id, res))
  }

  /** Make this the active scene (brings up the 3D engine; closes any other). */
  open(): void {
    _creator.openScene(this._id)
    Scene._active = this
    glState.activeScene = this
  }
  close(): void {
    _creator.closeScene()
    if (Scene._active === this) Scene._active = null
    if (glState.activeScene === this) glState.activeScene = null
  }

  addEventListener(channel: "click", callback: (ev: ClickEvent<Node | null>) => void): void
  addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<Node | null>) => void): void
  addEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
    if (channel === "click") {
      if (!this._clickListeners.includes(callback)) { this._clickListeners.push(callback); registerTouchEndEvent() }
    } else if (channel === "touchstart") {
      if (!this._touchStartListeners.includes(callback)) { this._touchStartListeners.push(callback); registerTouchStartEvent() }
    }
  }
  removeEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
    const list = channel === "click" ? this._clickListeners : this._touchStartListeners
    const i = list.indexOf(callback)
    if (i >= 0) list.splice(i, 1)
  }
}

// --- AR ---

export type ARMode = "default" | "markers" | "arcore" | "detached"

const onTrackStateChange = (entityId: number, track: boolean): void => {
  const node = nodeRegistry.get(entityId)
  if (!node) return
  node.isTracked = track
  if (track) node._dispatchEvent("track")
  else node._dispatchEvent("untrack")
}

export class ARScene<T extends ARMode = "default"> extends Scene {
  readonly mode: T
  useWarmRender = true
  private _root?: Node

  constructor(options: SceneOptions & { mode?: T } = {}) {
    super(options)
    this.mode = options.mode ?? ("default" as T)
    _creator.createARController(this._id, this.camera.id, this.mode, onTrackStateChange)
  }

  /** Root anchor node (not available in marker mode). */
  get root(): Node {
    if (this._root) return this._root
    this._root = new Node(_creator.createRootAnchor(this._id))
    return this._root
  }

  /** Create an anchor from a tracking source (e.g. an image target). */
  createAnchor(source: FetchResponse, physicalWidth = 0.2): Node {
    return new Node(_creator.createAnchor(this._id, physicalWidth, (source as any)._id))
  }

  /**
   * Let the user place & manipulate a node with touch gestures — the staple AR object-placement
   * interaction: ONE finger drags it across the floor (relative to where the camera looks), TWO fingers
   * pinch to scale and twist to rotate. Returns a handle; call `.remove()` to detach. Tunable via
   * options (min/max scale, and toggles for `pan` / `pinch` / `twist`).
   */
  addControls(target: Node, options: ControlsOptions = {}): ControlsHandle {
    return attachControls(this, target, options)
  }

  override async open(): Promise<void> {
    await new Promise<void>((res, rej) => _creator.requestCamera(res, rej))
    if (this.useWarmRender) { _creator.createGLView(); await this.warmRender() }
    await new Promise<void>((res, rej) => _creator.launchAR(this._id, res, rej))
    super.open()
  }

  override close(): void {
    _creator.stopAR()
    super.close()
  }
}

// --- VR ---

/**
 * A 3D scene rendered in a VR headset (OpenXR — e.g. Meta Quest). The headset drives the camera:
 * every frame both eyes are rendered from the tracked head pose, so `scene.camera` position/lookAt
 * have no effect while the scene is open. The world origin is at floor level (stage space) —
 * author content with the ground at y = 0 and the user standing at the origin.
 *
 * `open()` rejects on hosts without a VR runtime ("VR is not supported on this device").
 */
export class VRScene extends Scene {
  override async open(): Promise<void> {
    await new Promise<void>((res, rej) => _creator.launchVR(this._id, res, rej))
    super.open()
  }

  override close(): void {
    _creator.stopVR()
    super.close()
  }
}
