// The scene camera — a Node (so position / lookAt / transform all work) plus projection reads and
// screen↔world ray helpers. Created and attached by Scene. Ported from worker/src/components/Camera.ts.

import { Vec3 } from "../math/vec"
import { Node } from "./Node"
import { Ray } from "./Ray"

/** Projection every host starts a scene with (creator-gl scene.cpp / the lite core agree on these). */
export const CAMERA_DEFAULTS = { fov: 60, near: 0.01, far: 1000 }

export class Camera extends Node {
  private _sceneId = -1
  /** Live projection matrix (updated by the host when the viewport/fov changes). */
  readonly projectionMatrix = new Float32Array(16)

  // Projection the app asked for (host defaults until it does). The host owns the aspect and
  // re-applies these on every viewport change, so they are set, not maintained.
  private _fov = CAMERA_DEFAULTS.fov
  private _near = CAMERA_DEFAULTS.near
  private _far = CAMERA_DEFAULTS.far
  /** Stay quiet until the app actually sets something — hosts may be configured with their own
   *  defaults (the headless core takes a `fov` option) and a blind push would overwrite them. */
  private _projSet = false

  /** @internal — Scene wires the camera to its native scene id + projection callback. */
  _attach(sceneId: number): void {
    this._sceneId = sceneId
    _creator.attachCameraToAR(sceneId, this.id, (mat) => {
      for (let i = 0; i < 16; i++) this.projectionMatrix[i] = mat[i]
    })
    if (this._projSet) this._applyProjection()
  }

  /** Vertical field of view in degrees (default 60) — a smaller angle is a longer lens. */
  get fov(): number { return _creator.getCameraFov(this._sceneId, 0) }
  set fov(degrees: number) { this._fov = degrees; this._projSet = true; this._applyProjection() }
  get horizontalFov(): number { return _creator.getCameraFov(this._sceneId, 1) }

  /** Near clip distance (default 0.01) — nothing closer than this draws. */
  get near(): number { return this._near }
  set near(distance: number) { this._near = distance; this._projSet = true; this._applyProjection() }

  /** Far clip distance / view range (default 1000) — geometry past it is culled. Shadows keep
   *  their own (shorter) range, so a long view distance doesn't cost shadow sharpness. */
  get far(): number { return this._far }
  set far(distance: number) { this._far = distance; this._projSet = true; this._applyProjection() }

  /** Set any of fov / near / far in one call: `camera.setProjection({ fov: 45, far: 5000 })`. */
  setProjection(projection: { fov?: number, near?: number, far?: number }): this {
    if (projection.fov !== undefined) this._fov = projection.fov
    if (projection.near !== undefined) this._near = projection.near
    if (projection.far !== undefined) this._far = projection.far
    this._projSet = true
    this._applyProjection()
    return this
  }

  /** Hosts that predate the call keep the fixed defaults (older wasm / native builds). */
  private _applyProjection(): void {
    if (this._sceneId < 0 || typeof _creator.setCameraProjection !== "function") return
    _creator.setCameraProjection(this._sceneId, this._fov, this._near, this._far)
  }
  get displaySize(): [number, number] {
    const s = _creator.getDisplaySize()
    return [ s[0], s[1] ]
  }

  /** World-space ray direction through a screen pixel. */
  getViewDirection(screenX: number, screenY: number): Vec3 {
    if (this._sceneId < 0) throw new Error("Camera is not attached to a scene")
    const v = new Float32Array(3)
    _creator.getCameraViewDirection(this._sceneId, screenX, screenY, v)
    return new Vec3(v[0], v[1], v[2])
  }

  /** A pick ray from the camera through a screen pixel. */
  getRay(screenX: number, screenY: number): Ray {
    return new Ray(this.worldPosition, this.getViewDirection(screenX, screenY))
  }
}
