/** Three.js presentation adapter for the deterministic electric aircraft. */

import * as THREE from "three";
import type { Pose } from "../engine/scenekit.ts";
import {
  advanceAircraftFans,
  buildElectricAircraft,
  disposeElectricAircraft,
  setAircraftControlSurfaces,
  setAircraftFanRotation,
  type ElectricAircraftBuildOptions,
  type ElectricAircraftRig,
} from "./asset.ts";
import {
  AircraftController,
  NEUTRAL_AIRCRAFT_ACTIONS,
  normalizeAircraftActions,
  type AircraftActionSnapshot,
  type AircraftControllerOptions,
  type AircraftControllerSnapshot,
  type AircraftWaypoint,
} from "./controller.ts";

/**
 * Assisted corridor: KLAX to KSFO.
 *
 * Downtown coordinates used to stand in for the airports, so "fly to San
 * Francisco" arrived over City Hall. These are the field centres. Climb is
 * authored on the waypoints; the envelope still caps the ceiling.
 */
export const CALIFORNIA_AIR_ROUTE: readonly AircraftWaypoint[] = Object.freeze([
  Object.freeze({ id: "klax", lat: 33.9416, lng: -118.4085, altitudeM: 400 }),
  Object.freeze({ id: "central-valley", lat: 36.45, lng: -120.45, altitudeM: 1_700 }),
  Object.freeze({ id: "ksfo", lat: 37.6213, lng: -122.379, altitudeM: 400 }),
]);

export type AircraftProjection = (lat: number, lng: number) => readonly [x: number, z: number];

export interface SceneAircraftCameraOptions {
  distance?: number;
  height?: number;
  lookAhead?: number;
}

export interface SceneAircraftOptions extends AircraftControllerOptions {
  project: AircraftProjection;
  groundAt?: (lat: number, lng: number) => number;
  /** Aircraft altitude metres to vertical scene units. */
  altitudeSceneUnitsPerMetre?: number;
  /** Aircraft model metres to scene units; useful as a state-board glyph. */
  visualSceneUnitsPerMetre?: number;
  camera?: SceneAircraftCameraOptions;
  asset?: ElectricAircraftBuildOptions;
  /** False by default, so construction never steals input or camera ownership. */
  active?: boolean;
}

export interface SceneAircraftView {
  /** Stable live scene position suitable for lights, labels, and camera consumers. */
  position: THREE.Vector3;
}

export interface SceneAircraft {
  root: THREE.Group;
  view: SceneAircraftView;
  state(): AircraftControllerSnapshot;
  actions(): AircraftActionSnapshot;
  setActions(actions: Partial<AircraftActionSnapshot>): AircraftActionSnapshot;
  tick(elapsedSeconds: number): AircraftControllerSnapshot;
  active(): boolean;
  setActive(active: boolean): void;
  followPose(): Pose;
  reset(): AircraftControllerSnapshot;
  dispose(): void;
}

function positive(value: number, name: string): number {
  if (!(value > 0) || !Number.isFinite(value)) throw new RangeError(`${name} must be finite and positive`);
  return value;
}

function nonNegative(value: number, name: string): number {
  if (value < 0 || !Number.isFinite(value)) throw new RangeError(`${name} must be finite and non-negative`);
  return value;
}

function checkedPoint(value: readonly [number, number]): readonly [number, number] {
  if (value.length < 2 || !Number.isFinite(value[0]) || !Number.isFinite(value[1])) {
    throw new RangeError("aircraft projection must return finite x/z coordinates");
  }
  return value;
}

export function createSceneAircraft(options: SceneAircraftOptions): SceneAircraft {
  if (typeof options.project !== "function") throw new RangeError("aircraft project callback is required");
  if (options.groundAt !== undefined && typeof options.groundAt !== "function") {
    throw new RangeError("aircraft groundAt must be a function");
  }
  const altitudeScale = positive(
    options.altitudeSceneUnitsPerMetre ?? 0.004,
    "altitudeSceneUnitsPerMetre",
  );
  const visualScale = positive(options.visualSceneUnitsPerMetre ?? 0.12, "visualSceneUnitsPerMetre");
  const camera = {
    distance: options.camera?.distance ?? 20,
    height: options.camera?.height ?? 7,
    lookAhead: options.camera?.lookAhead ?? 32,
  };
  positive(camera.distance, "camera.distance");
  nonNegative(camera.height, "camera.height");
  nonNegative(camera.lookAhead, "camera.lookAhead");

  const controller = new AircraftController(options);
  const rig: ElectricAircraftRig = buildElectricAircraft(options.asset);
  const root = new THREE.Group();
  root.name = "playable-scene-aircraft";
  root.userData.kind = "playable-aircraft";
  root.userData.forwardAxis = "-Z";
  root.scale.setScalar(visualScale);
  root.add(rig.root);
  const view = { position: new THREE.Vector3() };
  let desired: AircraftActionSnapshot = { ...NEUTRAL_AIRCRAFT_ACTIONS };
  let enabled = options.active ?? false;
  let disposed = false;

  function groundAt(lat: number, lng: number): number {
    const value = options.groundAt?.(lat, lng) ?? 0;
    if (!Number.isFinite(value)) throw new RangeError("aircraft groundAt must return a finite height");
    return value;
  }

  function sync(previousFanRadians?: number): void {
    const state = controller.state();
    const [x, z] = checkedPoint(options.project(state.lat, state.lng));
    root.position.set(x, groundAt(state.lat, state.lng) + state.altitudeM * altitudeScale, z);
    root.rotation.order = "YXZ";
    root.rotation.set(
      state.pitchDeg * Math.PI / 180,
      -state.headingDeg * Math.PI / 180,
      -state.rollDeg * Math.PI / 180,
    );
    view.position.copy(root.position);
    setAircraftControlSurfaces(rig, {
      roll: state.rollInput,
      pitch: state.pitchInput,
      yaw: state.yawInput,
    });
    if (previousFanRadians === undefined) setAircraftFanRotation(rig, state.fanRadians);
    else {
      let delta = state.fanRadians - previousFanRadians;
      if (delta < -Math.PI) delta += Math.PI * 2;
      if (delta > Math.PI) delta -= Math.PI * 2;
      advanceAircraftFans(rig, delta);
    }
  }

  function clearEdges(): void {
    desired.modeRequest = "none";
    desired.reset = false;
  }

  sync();

  return {
    root,
    view,
    state: () => controller.snapshot(),
    actions: () => ({ ...desired }),
    setActions(actions) {
      desired = normalizeAircraftActions(actions);
      return { ...desired };
    },
    tick(elapsedSeconds) {
      if (!disposed && enabled) {
        const previousFanRadians = controller.state().fanRadians;
        controller.tick(elapsedSeconds, desired);
        sync(previousFanRadians);
        clearEdges();
      }
      return controller.snapshot();
    },
    active: () => enabled,
    setActive(active) {
      if (active !== enabled) desired = { ...NEUTRAL_AIRCRAFT_ACTIONS };
      enabled = active;
    },
    followPose() {
      const state = controller.state();
      const heading = state.headingDeg * Math.PI / 180;
      const pitch = state.pitchDeg * Math.PI / 180;
      // Compass heading zero follows projected north at -Z, matching Tera's map convention.
      const forward = new THREE.Vector3(
        Math.sin(heading) * Math.cos(pitch),
        Math.sin(pitch),
        -Math.cos(heading) * Math.cos(pitch),
      );
      return {
        position: root.position.clone()
          .addScaledVector(forward, -camera.distance * visualScale)
          .add(new THREE.Vector3(0, camera.height * visualScale, 0)),
        target: root.position.clone().addScaledVector(forward, camera.lookAhead * visualScale),
      };
    },
    reset() {
      if (!disposed) {
        controller.reset();
        desired = { ...NEUTRAL_AIRCRAFT_ACTIONS };
        sync();
      }
      return controller.snapshot();
    },
    dispose() {
      if (disposed) return;
      disposed = true;
      root.removeFromParent();
      root.remove(rig.root);
      disposeElectricAircraft(rig);
    },
  };
}
