/**
 * Three.js presentation adapter for a playable city/corridor actor.
 *
 * `ActorController` remains the sole owner of motion and identity. This module
 * only turns its metre-space state into a procedural rig, pose joints and a
 * third-person scene camera contract. A state-scale California board can set a
 * small `sceneUnitsPerMetre`; a metre-scale plaza leaves it at one.
 */
import * as THREE from "three";
import {
  buildCrow,
  buildDog,
  buildHumanoid,
  dogPoseStateForSpeed,
  disposeCrow,
  disposeDog,
  disposeHumanoid,
  poseCrow,
  poseDog,
  poseHumanoid,
  type CrowRig,
  type DogRig,
  type HumanoidRig,
} from "../assets/actors/index.ts";
import type { Pose } from "../engine/scenekit.ts";
import {
  ActorController,
  NEUTRAL_ACTOR_ACTIONS,
  normalizeActorActions,
  type ActorActionSnapshot,
  type ActorControllerOptions,
  type ActorControllerSnapshot,
  type ActorIdentity,
  type ActorKind,
  type ActorMode,
} from "./controller.ts";

export interface SceneActorCameraOptions {
  /** All camera dimensions are actor-space metres and share the render scale. */
  distance?: number;
  height?: number;
  targetHeight?: number;
  lookAhead?: number;
}

export interface SceneActorOptions extends ActorControllerOptions {
  /** Actor metres to scene units. Defaults to one. */
  sceneUnitsPerMetre?: number;
  /** Optional representational scale for the rig/camera on very coarse boards. */
  visualSceneUnitsPerMetre?: number;
  /** Scene-space anchor for the controller's local metre origin. */
  sceneOrigin?: { x: number; y: number; z: number };
  camera?: SceneActorCameraOptions;
  /** False by default so constructing a layer never steals orbit navigation. */
  active?: boolean;
}

export interface SceneActorView {
  /** Stable live vector in scene coordinates for camera/light consumers. */
  position: THREE.Vector3;
}

export interface SceneActor {
  /** Stable outer group; changing actor kind only replaces its rig child. */
  root: THREE.Group;
  view: SceneActorView;
  state(): ActorControllerSnapshot;
  actions(): ActorActionSnapshot;
  setActions(actions: Partial<ActorActionSnapshot>): ActorActionSnapshot;
  tick(elapsedSeconds: number): ActorControllerSnapshot;
  /** Defensive scene-space third-person pose. */
  followPose(): Pose;
  active(): boolean;
  setActive(active: boolean): void;
  /** Change rig and optional identity/mode without replacing the stable root. */
  switchActor(kind: ActorKind, identity?: ActorIdentity, mode?: ActorMode): ActorControllerSnapshot;
  /** Rebuild the visible skin for a new serializable identity, preserving motion and kind. */
  setIdentity(identity: ActorIdentity): ActorControllerSnapshot;
  /** Attach a caller-owned live/static face texture. False when the current actor is not humanoid. */
  attachFaceTexture(texture: THREE.Texture): boolean;
  /** Detach and release the adapter's reference without disposing the caller texture. */
  clearFaceTexture(): void;
  dispose(): void;
}

type RiggedActor =
  | { kind: "humanoid"; rig: HumanoidRig }
  | { kind: "dog"; rig: DogRig }
  | { kind: "crow"; rig: CrowRig };

const CAMERA_BY_KIND: Record<ActorKind, Required<SceneActorCameraOptions>> = {
  humanoid: { distance: 3.5, height: 2.25, targetHeight: 1.25, lookAhead: 0.75 },
  dog: { distance: 2.8, height: 1.35, targetHeight: 0.48, lookAhead: 0.65 },
  crow: { distance: 4.2, height: 1.15, targetHeight: 0.18, lookAhead: 2.2 },
};

function color(value: string | undefined): THREE.ColorRepresentation | undefined {
  return value;
}

function buildActor(kind: ActorKind, identity: Readonly<ActorIdentity>): RiggedActor {
  const appearance = identity.profile.appearance;
  if (kind === "dog") {
    return {
      kind,
      rig: buildDog({
        ...(color(appearance?.primaryColor) === undefined ? {} : { coatColor: color(appearance?.primaryColor) }),
        ...(color(appearance?.skinTone) === undefined ? {} : { markingsColor: color(appearance?.skinTone) }),
        ...(color(appearance?.accentColor) === undefined ? {} : { collarColor: color(appearance?.accentColor) }),
      }),
    };
  }
  if (kind === "crow") {
    return {
      kind,
      rig: buildCrow({
        ...(color(appearance?.primaryColor) === undefined ? {} : { featherColor: color(appearance?.primaryColor) }),
      }),
    };
  }
  // `faceImageUrl` intentionally remains data, not an implicit network fetch.
  // A realtime/video integration may attach its caller-owned texture to the
  // named `humanoid.face` mesh without changing simulation or this lifecycle.
  return {
    kind,
    rig: buildHumanoid({
      ...(color(appearance?.skinTone) === undefined ? {} : { skinTone: color(appearance?.skinTone) }),
      ...(color(appearance?.primaryColor) === undefined ? {} : { outfitColor: color(appearance?.primaryColor) }),
      ...(color(appearance?.accentColor) === undefined ? {} : { accentColor: color(appearance?.accentColor) }),
      ...(color(appearance?.hairColor) === undefined ? {} : { hairColor: color(appearance?.hairColor) }),
      ...(appearance?.bodyShape === undefined ? {} : { bodyShape: appearance.bodyShape }),
    }),
  };
}

function disposeRig(actor: RiggedActor): void {
  actor.rig.root.removeFromParent();
  if (actor.kind === "humanoid") disposeHumanoid(actor.rig);
  else if (actor.kind === "dog") disposeDog(actor.rig);
  else disposeCrow(actor.rig);
}

const DEFAULT_FACE_COLOR = 0x18242b;

function faceMaterial(rig: HumanoidRig): THREE.MeshBasicMaterial {
  const material = rig.face.material;
  if (!(material instanceof THREE.MeshBasicMaterial)) {
    throw new Error("scene actor: humanoid face must use one MeshBasicMaterial");
  }
  return material;
}

function applyFaceTexture(rig: HumanoidRig, texture: THREE.Texture | null): void {
  const material = faceMaterial(rig);
  material.map = texture;
  material.color.set(texture ? 0xffffff : DEFAULT_FACE_COLOR);
  material.needsUpdate = true;
}

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 finiteOrigin(origin: SceneActorOptions["sceneOrigin"]): THREE.Vector3 {
  if (!origin) return new THREE.Vector3();
  if (![origin.x, origin.y, origin.z].every(Number.isFinite)) {
    throw new RangeError("sceneOrigin must contain finite coordinates");
  }
  return new THREE.Vector3(origin.x, origin.y, origin.z);
}

export function createSceneActor(options: SceneActorOptions): SceneActor {
  const scale = positive(options.sceneUnitsPerMetre ?? 1, "sceneUnitsPerMetre");
  const visualScale = positive(options.visualSceneUnitsPerMetre ?? scale, "visualSceneUnitsPerMetre");
  const origin = finiteOrigin(options.sceneOrigin);
  const cameraOverrides = options.camera ?? {};
  for (const [name, value] of Object.entries(cameraOverrides)) {
    if (value === undefined) continue;
    if (name === "distance" || name === "height") positive(value, `camera.${name}`);
    else nonNegative(value, `camera.${name}`);
  }

  const controller = new ActorController(options);
  const root = new THREE.Group();
  root.name = "playable-scene-actor";
  root.userData.kind = "playable-actor";
  root.userData.forwardAxis = "-Z";
  root.scale.setScalar(visualScale);
  const view = { position: new THREE.Vector3() };
  let actor = buildActor(controller.state().kind, controller.state().identity);
  root.add(actor.rig.root);
  let enabled = options.active ?? false;
  let desired: ActorActionSnapshot = { ...NEUTRAL_ACTOR_ACTIONS };
  let faceTexture: THREE.Texture | null = null;
  let disposed = false;

  function sync(elapsedSeconds = 0, previousSpeedMps?: number): void {
    const state = controller.state();
    root.position.set(
      origin.x + state.x * scale,
      origin.y + state.y * scale,
      origin.z + state.z * scale,
    );
    root.rotation.order = "YXZ";
    root.rotation.set(
      state.mode === "flight" ? state.pitch : 0,
      state.yaw,
      state.mode === "flight" && state.kind === "crow" ? -state.roll * 0.42 : 0,
    );
    view.position.copy(root.position);
    if (actor.kind === "humanoid") {
      poseHumanoid(actor.rig, { walkPhase: state.posePhase, stride: state.poseAmount * 0.68 });
    } else if (actor.kind === "dog") {
      const dogState = dogPoseStateForSpeed(state.speedMps);
      const acceleration = elapsedSeconds > 0 && previousSpeedMps !== undefined
        ? THREE.MathUtils.clamp((state.speedMps - previousSpeedMps) / elapsedSeconds / 5, -1, 1)
        : 0;
      poseDog(actor.rig, {
        state: dogState,
        phase: state.posePhase,
        amount: dogState === "idle" ? 1 : THREE.MathUtils.clamp(0.38 + state.poseAmount * 0.62, 0, 1),
        turn: desired.turn,
        acceleration,
        attentionYaw: -desired.turn * 0.08,
      });
    } else if (state.mode === "flight") {
      // The controller names the aerodynamic state; the rig owns how that
      // state bends shoulder, elbow, wrist, tail and feet.
      poseCrow(actor.rig, {
        state: state.crowPose,
        phase: state.posePhase,
        amount: Math.max(state.poseAmount, enabled ? 0.68 : 0.5),
        bank: THREE.MathUtils.clamp(state.roll / 0.62, -1, 1),
        flight: 1,
      });
    } else {
      poseCrow(actor.rig, { state: "perch", amount: 1, flight: 0 });
    }
  }

  function replaceActor(kind: ActorKind): void {
    const previous = actor;
    actor = buildActor(kind, controller.state().identity);
    root.add(actor.rig.root);
    if (actor.kind === "humanoid" && faceTexture) applyFaceTexture(actor.rig, faceTexture);
    if (previous.kind === "humanoid") applyFaceTexture(previous.rig, null);
    // A face is a humanoid-only, potentially sensitive live reference. Do not
    // retain it invisibly while the player is represented as an animal.
    if (actor.kind !== "humanoid") faceTexture = null;
    disposeRig(previous);
    sync();
  }

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

  sync();

  return {
    root,
    view,
    state: () => controller.snapshot(),
    actions: () => ({ ...desired }),
    setActions(actions) {
      desired = normalizeActorActions(actions);
      return { ...desired };
    },
    tick(elapsedSeconds) {
      if (!disposed && enabled) {
        const beforeKind = controller.state().kind;
        const previousSpeedMps = controller.state().speedMps;
        controller.tick(elapsedSeconds, desired);
        if (controller.state().kind !== beforeKind) replaceActor(controller.state().kind);
        else sync(Number.isFinite(elapsedSeconds) ? Math.max(0, elapsedSeconds) : 0, previousSpeedMps);
        clearEdges();
      }
      return controller.snapshot();
    },
    followPose() {
      const state = controller.state();
      const defaults = CAMERA_BY_KIND[state.kind];
      const camera = {
        distance: cameraOverrides.distance ?? defaults.distance,
        height: cameraOverrides.height ?? defaults.height,
        targetHeight: cameraOverrides.targetHeight ?? defaults.targetHeight,
        lookAhead: cameraOverrides.lookAhead ?? defaults.lookAhead,
      };
      const forwardX = -Math.sin(state.yaw);
      const forwardZ = -Math.cos(state.yaw);
      const forwardY = state.mode === "flight" ? Math.sin(state.pitch) : 0;
      const atX = origin.x + state.x * scale;
      const atY = origin.y + state.y * scale;
      const atZ = origin.z + state.z * scale;
      return {
        position: new THREE.Vector3(
          atX - forwardX * camera.distance * visualScale,
          atY + camera.height * visualScale,
          atZ - forwardZ * camera.distance * visualScale,
        ),
        target: new THREE.Vector3(
          atX + forwardX * camera.lookAhead * visualScale,
          atY + (camera.targetHeight + forwardY * camera.lookAhead) * visualScale,
          atZ + forwardZ * camera.lookAhead * visualScale,
        ),
      };
    },
    active: () => enabled,
    setActive(active) {
      if (active !== enabled) desired = { ...NEUTRAL_ACTOR_ACTIONS };
      enabled = active;
    },
    switchActor(kind, identity, mode) {
      if (disposed) return controller.snapshot();
      if (identity) controller.setIdentity(identity);
      const changed = kind !== controller.state().kind || identity !== undefined;
      controller.setActorKind(kind);
      if (mode) controller.setMode(mode);
      desired = { ...NEUTRAL_ACTOR_ACTIONS };
      if (changed) replaceActor(kind);
      else sync();
      return controller.snapshot();
    },
    setIdentity(identity) {
      if (disposed) return controller.snapshot();
      controller.setIdentity(identity);
      replaceActor(controller.state().kind);
      return controller.snapshot();
    },
    attachFaceTexture(texture) {
      if (disposed || actor.kind !== "humanoid") return false;
      if (!texture || texture.isTexture !== true) {
        throw new RangeError("scene actor: face texture must be a Three.js Texture");
      }
      faceTexture = texture;
      applyFaceTexture(actor.rig, texture);
      return true;
    },
    clearFaceTexture() {
      faceTexture = null;
      if (!disposed && actor.kind === "humanoid") applyFaceTexture(actor.rig, null);
    },
    dispose() {
      if (disposed) return;
      disposed = true;
      root.removeFromParent();
      if (actor.kind === "humanoid") applyFaceTexture(actor.rig, null);
      faceTexture = null;
      disposeRig(actor);
    },
  };
}
