"use client";

import React, { useMemo, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { COLORS } from "./constants";
import { buildRoutes, RouteDef } from "./routes";
import { seeded, smoothstep } from "./math";

type Props = {
  progress: React.RefObject<number>;
  reduced?: boolean;
};

type VType = "truck" | "van" | "bike";

type VehicleDef = {
  curve: THREE.CatmullRomCurve3;
  type: VType;
  speed: number;
  offset: number;
  color: string;
  nodes: THREE.Vector3[];
};

const TMP_POS = new THREE.Vector3();
const TMP_TAN = new THREE.Vector3();
const TRAIL_TMP = new THREE.Vector3();

// Glowing energy comet-tail trailing each optimized vehicle (see ref image:
// a delivery van with flowing green light streaks). Sampled along the vehicle's
// own route curve behind its current position, so it always hugs the track and
// never smears on a scroll-jump.
const TRAIL_N = 16;
const TRAIL_GAP = 0.011;

/** Flat billboarded truck cutout from the user's artwork (modelled facing +Z;
 *  the fleet loop rotates each one to face the camera). */
function TruckBillboard({
  texture,
  tint,
  width,
  aspect,
}: {
  texture: THREE.Texture;
  tint: string;
  width: number;
  aspect: number;
}) {
  const h = width / aspect;
  return (
    <mesh position={[0, h / 2.4, 0]}>
      <planeGeometry args={[width, h]} />
      <meshBasicMaterial
        map={texture}
        color={tint}
        transparent
        depthWrite={false}
        side={THREE.DoubleSide}
        toneMapped={false}
      />
    </mesh>
  );
}

const TruckBillboardMemo = React.memo(TruckBillboard);

const HUB = new THREE.Vector3(0, 0.5, 0);

/**
 * Moving fleet. 12 erratic vehicles ride the chaotic routes; on the reorganize
 * phase they fade out and 8 optimally-assigned vehicles take over the clean
 * routes. Position + heading are driven along the shared curves each frame.
 */
function VehicleFleet({ progress, reduced = false }: Props) {
  const { chaotic, optimized } = useMemo(() => buildRoutes(), []);

  const types: VType[] = ["truck", "van", "bike"];

  const chaosFleet = useMemo<VehicleDef[]>(() => {
    return Array.from({ length: 7 }, (_, i) => ({
      curve: (chaotic[i % chaotic.length] as RouteDef).curve,
      type: types[i % 3],
      speed: 0.018 + seeded(i * 5 + 1) * 0.03,
      offset: seeded(i * 5 + 2),
      color: COLORS.red,
      nodes: (chaotic[i % chaotic.length] as RouteDef).nodes || [],
    }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [chaotic]);

  const optFleet = useMemo<VehicleDef[]>(() => {
    return Array.from({ length: 5 }, (_, i) => ({
      curve: (optimized[i % optimized.length] as RouteDef).curve,
      type: types[i % 3],
      speed: 0.05 + seeded(i * 7 + 1) * 0.025,
      offset: seeded(i * 7 + 2),
      color: i % 2 === 0 ? COLORS.cyan : COLORS.green,
      nodes: (optimized[i % optimized.length] as RouteDef).nodes || [],
    }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [optimized]);

  const chaosRefs = useRef<(THREE.Group | null)[]>([]);
  const optRefs = useRef<(THREE.Group | null)[]>([]);
  const optTrailRefs = useRef<(THREE.Mesh | null)[]>([]);

  // The optimized fleet uses the user's isometric truck artwork as a flat
  // billboarded cutout (loaded imperatively so we don't need a Suspense fence).
  const truckTex = useMemo(() => {
    const t = new THREE.TextureLoader().load("/images/truck.png");
    t.colorSpace = THREE.SRGBColorSpace;
    t.anisotropy = 8;
    return t;
  }, []);
  const TRUCK_ASPECT = 1080 / 948; // rasterized PNG dimensions
  const TRUCK_W = 1.75;

  // Persistent progress values
  const chaosProgress = useRef<number[]>([]);
  const optProgress = useRef<number[]>([]);

  const place = (
    group: THREE.Group | null,
    def: VehicleDef,
    progressArray: number[],
    index: number,
    dt: number,
    opacity: number,
  ) => {
    if (!group || !def?.curve) return;
    const visible = opacity > 0.02;
    group.visible = visible;
    if (!visible) return;

    let u = progressArray[index];
    if (u === undefined || Number.isNaN(u))
      u = progressArray[index] = def.offset ?? 0;

    // Get position to compute distance to nodes
    def.curve.getPointAt(u, TMP_POS);

    // Compute distance to nearest node
    let minDist = TMP_POS.distanceTo(HUB);
    if (def.nodes && def.nodes.length > 0) {
      def.nodes.forEach((node) => {
        const dist = TMP_POS.distanceTo(node);
        if (dist < minDist) minDist = dist;
      });
    }

    // Slow at nodes (speed reduction to 20%), accelerate on long corridors (up to 125%)
    const speedFactor = 0.2 + smoothstep(0.4, 2.5, minDist) * 1.05;

    // Increment progress continuously
    u = (u + dt * def.speed * speedFactor) % 1;
    progressArray[index] = u;

    // Get final position and orientation
    def.curve.getPointAt(u, TMP_POS);
    def.curve.getTangentAt(u, TMP_TAN);

    group.position.copy(TMP_POS);
    group.rotation.y = Math.atan2(TMP_TAN.x, TMP_TAN.z);

    group.traverse((o) => {
      const m = (o as THREE.Mesh).material as THREE.Material | undefined;
      if (m && "opacity" in m) {
        (m as THREE.MeshBasicMaterial).opacity = opacity * baseOpacity(o);
      }
    });
  };

  // preserve per-material relative opacity (fill vs wire vs light)
  const baseOpacity = (o: THREE.Object3D) => {
    const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined;
    if (!m) return 1;
    if (m.map) return 1; // textured truck cutout fades at full strength
    if ((m as THREE.MeshBasicMaterial).wireframe) return 1;
    if (m.color && m.color.getHex() === 0xffffff) return 1;
    if (m.color && m.color.getHex() === 0xef4444) return 1; // headlight/taillight colors
    return 0.2;
  };

  useFrame((state, dt) => {
    // Re-sync on length change (not just when empty) so a fleet-size edit / HMR
    // doesn't leave indices reading `undefined` and crashing getPointAt().
    if (chaosProgress.current.length !== chaosFleet.length) {
      chaosProgress.current = chaosFleet.map((v) => v.offset);
    }
    if (optProgress.current.length !== optFleet.length) {
      optProgress.current = optFleet.map((v) => v.offset);
    }

    const p = progress.current ?? 0;
    const t = state.clock.elapsedTime;
    const safeDt = Math.min(0.06, dt);

    const chaosFadeIn = smoothstep(0.1, 0.22, p);
    const chaosFadeOut = 1 - smoothstep(0.7, 0.82, p);
    const chaosOp = chaosFadeIn * chaosFadeOut * (0.85 + Math.sin(t * 6) * 0.1);
    const optOp = smoothstep(0.68, 0.82, p);

    const cam = state.camera;
    for (let i = 0; i < chaosFleet.length; i++) {
      place(
        chaosRefs.current[i],
        chaosFleet[i],
        chaosProgress.current,
        i,
        safeDt,
        chaosOp,
      );
      const g = chaosRefs.current[i];
      if (g && g.visible) {
        g.rotation.y = Math.atan2(
          cam.position.x - g.position.x,
          cam.position.z - g.position.z,
        );
      }
    }
    for (let i = 0; i < optFleet.length; i++) {
      place(
        optRefs.current[i],
        optFleet[i],
        optProgress.current,
        i,
        safeDt,
        optOp,
      );
      // The truck is 2D cutout art, so billboard it around Y to always face the
      // camera (overrides the tangent heading set inside place()).
      const g = optRefs.current[i];
      if (g && g.visible) {
        g.rotation.y = Math.atan2(
          cam.position.x - g.position.x,
          cam.position.z - g.position.z,
        );
      }
      // Energy comet-tail behind each optimized vehicle.
      const u = optProgress.current[i];
      const curve = optFleet[i]?.curve;
      if (!curve || u === undefined || Number.isNaN(u)) continue;
      for (let k = 0; k < TRAIL_N; k++) {
        const mesh = optTrailRefs.current[i * TRAIL_N + k];
        if (!mesh) continue;
        if (optOp < 0.02) {
          mesh.visible = false;
          continue;
        }
        let uk = u - (k + 1) * TRAIL_GAP;
        if (uk < 0) uk = 0;
        mesh.visible = true;
        curve.getPointAt(uk, TRAIL_TMP);
        mesh.position.copy(TRAIL_TMP);
        const taper = 1 - k / TRAIL_N;
        const size = 0.05 + taper * 0.13;
        mesh.scale.setScalar(size / 0.1);
        const mat = mesh.material as THREE.MeshBasicMaterial;
        mat.opacity =
          optOp * taper * taper * (0.65 + Math.sin(t * 9 - k * 0.7) * 0.35);
      }
    }
  });

  return (
    <group>
      {/* Chaotic fleet — the user's truck art tinted red (congested/before) */}
      {chaosFleet.map((_v, i) => (
        <group
          key={`cv${i}`}
          ref={(el) => {
            chaosRefs.current[i] = el;
          }}
        >
          <TruckBillboardMemo
            texture={truckTex}
            tint="#ff5a5a"
            width={TRUCK_W * 0.92}
            aspect={TRUCK_ASPECT}
          />
        </group>
      ))}
      {/* Optimized fleet — the truck art in clean white */}
      {optFleet.map((_v, i) => (
        <group
          key={`ov${i}`}
          visible={false}
          ref={(el) => {
            optRefs.current[i] = el;
          }}
        >
          <TruckBillboardMemo
            texture={truckTex}
            tint="#d7dce4"
            width={TRUCK_W}
            aspect={TRUCK_ASPECT}
          />
        </group>
      ))}

      {/* Glowing energy comet-tails for the optimized fleet */}
      {optFleet.map((v, i) =>
        Array.from({ length: TRAIL_N }, (_, k) => (
          <mesh
            key={`ot-${i}-${k}`}
            visible={false}
            ref={(el) => {
              optTrailRefs.current[i * TRAIL_N + k] = el;
            }}
          >
            <sphereGeometry args={[0.1, 8, 8]} />
            <meshBasicMaterial
              color={v.color}
              transparent
              opacity={0}
              depthWrite={false}
              blending={reduced ? THREE.NormalBlending : THREE.AdditiveBlending}
            />
          </mesh>
        )),
      )}
    </group>
  );
}

export default React.memo(VehicleFleet);
