/**
 * The office-facing half of walk mode: a numeric `WalkerController` wearing an
 * original actor rig and publishing a third-person camera pose.
 *
 * There is deliberately no identity, webcam capture, keyboard or network code here.
 * Callers choose humanoid or anonymous dog, translate their own input device to
 * a normalized planar action, and decide when walk mode is active. A caller may
 * attach an already-created face texture; ownership stays with that caller.
 */

import * as THREE from "three";
import {
  buildDog,
  dogPoseStateForSpeed,
  disposeDog,
  poseDog,
  type DogRig,
} from "../assets/actors/dog.ts";
import {
  buildHumanoid,
  disposeHumanoid,
  poseHumanoid,
  type HumanoidRig,
} from "../assets/actors/humanoid.ts";
import type { Pose } from "../engine/scenekit.ts";
import type { Plan } from "./plan.ts";
import {
  createWalker,
  normalizeWalkerAction,
  type WalkerAction,
  type WalkerController,
  type WalkerOptions,
  type WalkerSpawn,
  type WalkerState,
} from "./walker.ts";

const HUMANOID_CAMERA = { distance: 3.2, height: 2.25, targetHeight: 1.25, lookAhead: 0.7 };
const DOG_CAMERA = { distance: 2.6, height: 1.45, targetHeight: 0.48, lookAhead: 0.55 };
const STRIDE_METRES = 0.72;
const GAIT_EASE_SECONDS = 0.16;

export type OfficeActorKind = "humanoid" | "anonymous-dog";

export interface OfficeActorAppearance {
  kind: OfficeActorKind;
  skinTone?: THREE.ColorRepresentation;
  outfitColor?: THREE.ColorRepresentation;
  accentColor?: THREE.ColorRepresentation;
  hairColor?: THREE.ColorRepresentation;
  bodyShape?: "slim" | "average" | "broad";
  coatColor?: THREE.ColorRepresentation;
  markingsColor?: THREE.ColorRepresentation;
  collarColor?: THREE.ColorRepresentation;
}

export interface FollowCameraOptions {
  /** Metres behind the actor. */
  distance?: number;
  /** Camera height above this level's floor, in metres. */
  height?: number;
  /** Look target height above this level's floor, in metres. */
  targetHeight?: number;
  /** Metres ahead of the actor to aim. */
  lookAhead?: number;
}

export interface OfficeWalkerOptions extends WalkerOptions {
  actor?: OfficeActorAppearance;
  camera?: FollowCameraOptions;
  /** False by default: constructing an actor must not change dollhouse controls. */
  active?: boolean;
}

export interface OfficeWalkerState extends WalkerState {
  active: boolean;
  actor: OfficeActorKind;
  action: WalkerAction;
}

export interface OfficeWalker {
  /** Floor-centred, scene-ready actor root. */
  root: THREE.Group;
  /** Stable live position for occupancy-responsive office lighting. */
  view: { position: THREE.Vector3 };
  state(): OfficeWalkerState;
  active(): boolean;
  setActive(active: boolean): void;
  action(): WalkerAction;
  setAction(action: WalkerAction): WalkerAction;
  tick(elapsedSeconds: number): OfficeWalkerState;
  reset(spawn?: WalkerSpawn): OfficeWalkerState;
  /** Rebuild only the procedural skin, retaining this root and all walker/camera state. */
  setAppearance(appearance: OfficeActorAppearance): OfficeWalkerState;
  /** Attach a caller-owned texture. False for an anonymous dog. */
  attachFaceTexture(texture: THREE.Texture): boolean;
  /** Detach the current face without disposing the caller's texture. */
  clearFaceTexture(): void;
  /** A defensive scene-space chase-camera pose for the current actor state. */
  followPose(): Pose;
  dispose(): void;
}

type Actor =
  | { kind: "humanoid"; rig: HumanoidRig }
  | { kind: "anonymous-dog"; rig: DogRig };

export function createOfficeWalker(plan: Plan, options: OfficeWalkerOptions): OfficeWalker {
  let actor = buildActor(options.actor ?? { kind: "humanoid" });
  const controller: WalkerController = createWalker(plan, options);
  const baseCamera = actor.kind === "humanoid" ? HUMANOID_CAMERA : DOG_CAMERA;
  const camera = {
    distance: positive(options.camera?.distance ?? baseCamera.distance, "camera.distance"),
    height: positive(options.camera?.height ?? baseCamera.height, "camera.height"),
    targetHeight: nonNegative(options.camera?.targetHeight ?? baseCamera.targetHeight, "camera.targetHeight"),
    lookAhead: nonNegative(options.camera?.lookAhead ?? baseCamera.lookAhead, "camera.lookAhead"),
  };

  let enabled = options.active ?? false;
  let desired: WalkerAction = { x: 0, z: 0 };
  let gait = 0;
  let presentationSeconds = 0;
  let previousSpeed = 0;
  const previousFacing = new THREE.Vector2(0, -1);
  let faceTexture: THREE.Texture | null = null;
  let disposed = false;
  const root = new THREE.Group();
  root.name = "office-walker-actor";
  root.userData.kind = "playable-actor";
  root.userData.forwardAxis = "-Z";
  root.userData.actorType = actor.rig.root.userData.actorType;
  root.add(actor.rig.root);
  const view = { position: new THREE.Vector3() };

  function sync(state: WalkerState, elapsedSeconds = 0, travelled = 0): void {
    const level = plan.level(state.levelId);
    if (!level) throw new Error(`office walker lost level "${state.levelId}"`);
    root.position.set(state.position.x, level.floorY, state.position.z);
    view.position.copy(root.position);
    root.rotation.y = Math.atan2(-state.facing.x, -state.facing.z);

    const speed = elapsedSeconds > 0 ? Math.max(0, travelled / elapsedSeconds) : 0;
    const targetGait = enabled && speed > 1e-5 ? Math.min(1, speed / (options.speed ?? 1.6)) : 0;
    gait += (targetGait - gait) * Math.min(1, elapsedSeconds / GAIT_EASE_SECONDS);
    presentationSeconds += elapsedSeconds;
    const phase = speed > 1e-5
      ? (state.distance / STRIDE_METRES) * Math.PI * 2
      : presentationSeconds * 2.2;
    const facingDot = THREE.MathUtils.clamp(
      previousFacing.x * state.facing.x + previousFacing.y * state.facing.z,
      -1,
      1,
    );
    const facingCross = previousFacing.x * state.facing.z - previousFacing.y * state.facing.x;
    const turn = elapsedSeconds > 0
      ? THREE.MathUtils.clamp(Math.atan2(facingCross, facingDot) / elapsedSeconds / 4, -1, 1)
      : 0;
    const acceleration = elapsedSeconds > 0
      ? THREE.MathUtils.clamp((speed - previousSpeed) / elapsedSeconds / 5, -1, 1)
      : 0;
    if (actor.kind === "humanoid") {
      poseHumanoid(actor.rig, { walkPhase: phase, stride: gait * 0.62 });
    } else {
      const dogState = dogPoseStateForSpeed(speed * Math.max(gait, 0.2));
      poseDog(actor.rig, {
        state: dogState,
        phase,
        amount: dogState === "idle" ? 1 : THREE.MathUtils.clamp(0.38 + gait * 0.62, 0, 1),
        turn,
        acceleration,
        attentionYaw: -turn * 0.08,
      });
    }
    previousSpeed = speed;
    previousFacing.set(state.facing.x, state.facing.z);
  }

  function snapshot(): OfficeWalkerState {
    const state = controller.state();
    return {
      ...state,
      position: { ...state.position },
      facing: { ...state.facing },
      active: enabled,
      actor: actor.kind,
      action: { ...desired },
    };
  }

  function replaceActor(appearance: OfficeActorAppearance): void {
    const previous = actor;
    actor = buildActor(appearance);
    root.add(actor.rig.root);
    if (actor.kind === "humanoid" && faceTexture) applyOfficeFaceTexture(actor.rig, faceTexture);
    if (previous.kind === "humanoid") applyOfficeFaceTexture(previous.rig, null);
    if (actor.kind !== "humanoid") faceTexture = null;
    previous.rig.root.removeFromParent();
    if (previous.kind === "humanoid") disposeHumanoid(previous.rig);
    else disposeDog(previous.rig);
    root.userData.actorType = actor.rig.root.userData.actorType;
    sync(controller.state());
  }

  sync(controller.state());

  return {
    root,
    view,
    state: snapshot,
    active: () => enabled,
    setActive(active) {
      if (active !== enabled) desired = { x: 0, z: 0 };
      enabled = active;
    },
    action: () => ({ ...desired }),
    setAction(action) {
      desired = normalizeWalkerAction(action);
      return { ...desired };
    },
    tick(elapsedSeconds) {
      if (disposed) return snapshot();
      const before = controller.state();
      const next = enabled
        ? controller.tick(elapsedSeconds, desired)
        : controller.tick(elapsedSeconds, { x: 0, z: 0 });
      sync(next, Number.isFinite(elapsedSeconds) ? Math.max(0, elapsedSeconds) : 0, next.distance - before.distance);
      return snapshot();
    },
    reset(spawn) {
      const state = controller.reset(spawn);
      desired = { x: 0, z: 0 };
      gait = 0;
      presentationSeconds = 0;
      previousSpeed = 0;
      previousFacing.set(state.facing.x, state.facing.z);
      sync(state);
      return snapshot();
    },
    setAppearance(appearance) {
      if (disposed) return snapshot();
      replaceActor(appearance);
      return snapshot();
    },
    attachFaceTexture(texture) {
      if (disposed || actor.kind !== "humanoid") return false;
      if (!texture || texture.isTexture !== true) {
        throw new RangeError("office walker: face texture must be a Three.js Texture");
      }
      faceTexture = texture;
      applyOfficeFaceTexture(actor.rig, texture);
      return true;
    },
    clearFaceTexture() {
      faceTexture = null;
      if (!disposed && actor.kind === "humanoid") applyOfficeFaceTexture(actor.rig, null);
    },
    followPose() {
      const state = controller.state();
      const level = plan.level(state.levelId);
      if (!level) throw new Error(`office walker lost level "${state.levelId}"`);
      return {
        position: new THREE.Vector3(
          state.position.x - state.facing.x * camera.distance,
          level.floorY + camera.height,
          state.position.z - state.facing.z * camera.distance,
        ),
        target: new THREE.Vector3(
          state.position.x + state.facing.x * camera.lookAhead,
          level.floorY + camera.targetHeight,
          state.position.z + state.facing.z * camera.lookAhead,
        ),
      };
    },
    dispose() {
      if (disposed) return;
      disposed = true;
      root.removeFromParent();
      if (actor.kind === "humanoid") applyOfficeFaceTexture(actor.rig, null);
      faceTexture = null;
      if (actor.kind === "humanoid") disposeHumanoid(actor.rig);
      else disposeDog(actor.rig);
    },
  };
}

function buildActor(appearance: OfficeActorAppearance): Actor {
  if (appearance.kind === "anonymous-dog") {
    return {
      kind: "anonymous-dog",
      rig: buildDog({
        ...(appearance.coatColor !== undefined ? { coatColor: appearance.coatColor } : {}),
        ...(appearance.markingsColor !== undefined ? { markingsColor: appearance.markingsColor } : {}),
        ...(appearance.collarColor !== undefined ? { collarColor: appearance.collarColor } : {}),
      }),
    };
  }
  return {
    kind: "humanoid",
    rig: buildHumanoid({
      ...(appearance.skinTone !== undefined ? { skinTone: appearance.skinTone } : {}),
      ...(appearance.outfitColor !== undefined ? { outfitColor: appearance.outfitColor } : {}),
      ...(appearance.accentColor !== undefined ? { accentColor: appearance.accentColor } : {}),
      ...(appearance.hairColor !== undefined ? { hairColor: appearance.hairColor } : {}),
      ...(appearance.bodyShape !== undefined ? { bodyShape: appearance.bodyShape } : {}),
    }),
  };
}

const DEFAULT_OFFICE_FACE_COLOR = 0x18242b;

function applyOfficeFaceTexture(rig: HumanoidRig, texture: THREE.Texture | null): void {
  const material = rig.face.material;
  if (!(material instanceof THREE.MeshBasicMaterial)) {
    throw new Error("office walker: humanoid face must use one MeshBasicMaterial");
  }
  material.map = texture;
  material.color.set(texture ? 0xffffff : DEFAULT_OFFICE_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;
}
