import {
  AdditiveBlending,
  BoxGeometry,
  Color,
  Mesh,
  MeshBasicMaterial,
  MeshStandardMaterial,
  Object3D,
  Vector3,
} from "three";
import { CameraC_internal, UpdateController } from "@hitplay/playable_template";
import { ThreeC } from "./ThreeC";
import { SparkleFxC } from "./SparkleFxC";

// The map ships an actual wood-icon quad ("UI_Wood", textured with
// Icon_Wood) sitting unused in the scene — TestSceneC hands it in via
// setPickupTemplate() and every spawned resource is a clone of it. This
// placeholder cube only covers the case where that node is ever missing.
const PICKUP_SIZE = 0.28;

// Resources pop out of a hit target, scatter a short, fixed distance, land,
// bounce twice, then fly on to the HUD counter (see HudC) — the player
// never has to walk over and trigger any of this (see chat: "персонажу не
// потрібно підходити для збору"). Carrying the collected total on into the
// Pay Zone is a separate, later mechanic — see PayZoneC, which drains
// ResourceC.getCollectedCount() on its own.
const SCATTER_MIN_DISTANCE = 0.5;
const SCATTER_MAX_DISTANCE = 1.2;
const SCATTER_ARC_HEIGHT = 0.6;
const FLY_DURATION = 0.45; // seconds to reach the scattered spot

// Two bounces off the ground where it lands — decreasing height/duration
// each time, index 0 first. On the second (last) bounce's touchdown, the
// resource flashes white (like BreakablePropC's crate hit-flash) and
// shrinks while a sparkle burst plays around it (see chat: exact reference
// look). This shrink is intentional and stays (see chat: "Ти прибрав зміну
// розміру при баунсі... я тобі писав не про це" — a later, unrelated
// request to remove scaling from the toTarget *flight* got misread as
// "remove scaling everywhere" and briefly took this out too; it's back).
// The only place scale must NOT change is the toTarget flight itself —
// see that phase below, which deliberately never touches scale, so
// whatever value this phase lands on just carries through unchanged.
const BOUNCE_HEIGHTS = [0.4, 0.22];
const BOUNCE_DURATIONS = [0.26, 0.2];

// The shrink ramps in quickly (SHRINK_RAMP_DURATION) and then just holds —
// it does NOT ease back out to the original size (see chat: "стискання
// без повернення до початкового розміру"). Uniform shrink, not a
// squash-and-widen — an earlier version widened X/Z while flattening Y,
// which read as the resource *growing* (see chat: "мають навпаки
// зменшуватись"). The flash fades out on its own, longer, slower timeline
// (see chat: "нехай флеш буде довшим") — decoupled from the shrink so
// lengthening one doesn't drag the other out too.
const SHRINK_RAMP_DURATION = 0.22;
const FLASH_DURATION = 0.45;
// Must be >= FLASH_DURATION or the flash gets cut off mid-fade before the
// resource flies off — a little slack on top so the fully-faded state
// actually reads for a beat first.
const IMPACT_HOLD_DURATION = 0.55;
const SHRINK_AMOUNT = 0.15; // fraction shrunk, held once reached
const FLASH_COLOR = new Color(0xffffff);

// The world-space scale a resource settles at after the bounce shrink and
// carries all the way to the HUD (see toTarget's perspective compensation
// below, which holds this constant in apparent/on-screen size). Exported
// so PayZoneC's deposit-flight token — a separate, fresh mesh, not one of
// these FlyingResources — can start from the same baseline instead of its
// own default (1), which read as noticeably larger (see chat: "розмір...
// дуже великий, треба уніфікувати").
export const RESOURCE_ICON_BASE_SCALE = 1 - SHRINK_AMOUNT;

// Once settled, a resource flies on to wherever setFlyTarget() points (the
// HUD counter, see HudC). Without a target set, it's counted immediately
// instead (no flight) — see update() below.
//
// An accelerating (ease-in) version of this was tried and reverted (see
// chat: "зникають раніше часу") — CONVERGE_START_FRACTION triggers the
// shrink-to-nothing off raw elapsed time `t`, not spatial progress, and
// ease-in makes spatial progress lag well behind `t` (at t=0.7 an eased-in
// t² curve has only covered half the distance), so the resource visibly
// vanished while still far short of the target. Smoothstep keeps those two
// closely matched enough that convergence reads as "arriving", not
// "disappearing early".
const TO_TARGET_DURATION = 0.5;
const TO_TARGET_ARC_HEIGHT = 1;
// Fraction of the toTarget flight (0..1) where the arrival brighten (see
// update()) starts ramping in — 0.7 means the last 30% of the flight.
const CONVERGE_START_FRACTION = 0.7;

// Resources dropped by the same hit shouldn't all launch toward the HUD in
// the same instant — each one already bounces on identical timing
// constants, so without this they'd all reach "toTarget" together and fly
// as one visual clump (see chat: "не мають летіти однією кучею"). Instead,
// each resource in a burst waits STAGGER_INTERVAL longer than the previous
// one before launching — see spawnOne's staggerDelay param and its use in
// the impactHold exit condition below.
const STAGGER_INTERVAL = 0.12;

function randomInt(min: number, max: number): number {
  return min + Math.floor(Math.random() * (max - min + 1));
}

// Same trick as BreakablePropC's hit-flash: a same-geometry clone layered
// on top with additive blending, opacity animated for the flash — lerping
// an unlit mesh's own (already-white) material color is a silent no-op
// (see BreakablePropC/CLAUDE.md for why). Collected up front, before
// anything mutates the tree, so adding overlay children mid-traversal
// can't make traverse() walk into the overlays it just added.
function buildFlashOverlays(root: Object3D): Mesh[] {
  const meshes: Mesh[] = [];
  root.traverse((child) => {
    if (child instanceof Mesh) meshes.push(child);
  });

  return meshes.map((mesh) => {
    const overlay = mesh.clone();
    // clone() copies mesh's own local transform (relative to ITS parent) —
    // parenting the overlay under the mesh itself, one level deeper than
    // that, would reinterpret those same numbers in the mesh's local space
    // and displace the overlay instead of overlaying it exactly on top.
    // Reset to identity so it sits precisely where its new parent (mesh)
    // already is.
    overlay.position.set(0, 0, 0);
    overlay.rotation.set(0, 0, 0);
    overlay.scale.set(1, 1, 1);
    overlay.material = new MeshBasicMaterial({
      color: FLASH_COLOR,
      transparent: true,
      opacity: 0,
      blending: AdditiveBlending,
      depthWrite: false,
    });
    overlay.castShadow = false;
    overlay.receiveShadow = false;
    mesh.add(overlay);
    return overlay;
  });
}

type Phase = "scatter" | "bounce" | "impactHold" | "toTarget";

interface FlyingResource {
  mesh: Object3D;
  flashOverlays: Mesh[];
  phase: Phase;
  age: number;
  from: Vector3;
  to: Vector3;
  groundY: number; // the y level the two bounces oscillate around
  bounceIndex: number; // which of BOUNCE_HEIGHTS/BOUNCE_DURATIONS is playing
  staggerDelay: number; // extra hold time before launching to the HUD — see STAGGER_INTERVAL
  baseScale: number; // scale.x when toTarget starts — see the perspective compensation there
  startDistanceFromCamera: number; // camera distance when toTarget starts — see toTarget phase
}

export class ResourceC {
  private static flying: FlyingResource[] = [];
  private static collected = 0;
  private static initialized = false;
  private static pickupTemplate: Object3D | null = null;
  private static flyTarget: (() => Vector3) | null = null;

  static getCollectedCount() {
    return this.collected;
  }

  // The real wood-icon node from the map (see TestSceneC.createMap) — every
  // spawned pickup is a clone of this rather than the fallback cube.
  static setPickupTemplate(template: Object3D) {
    this.pickupTemplate = template;
  }

  // Where settled resources fly to before being counted — the HUD counter
  // (see HudC.getWorldAnchorPosition).
  static setFlyTarget(getTarget: () => Vector3) {
    this.flyTarget = getTarget;
  }

  // A standalone clone of the pickup visual, for callers (PayZoneC) that
  // need to fly a "resource" token somewhere outside of the normal
  // spawn/scatter/collect lifecycle below. No flash overlays — those are
  // only built for the actual bounce-and-collect resources below.
  static createVisual(): Object3D {
    return this.createPickupMesh();
  }

  static spawnBurst(origin: Vector3, count: number) {
    this.ensureUpdating();
    for (let i = 0; i < count; i++) this.spawnOne(origin, i * STAGGER_INTERVAL);
  }

  static spawnBurstInRange(origin: Vector3, min: number, max: number) {
    this.spawnBurst(origin, randomInt(min, max));
  }

  private static spawnOne(origin: Vector3, staggerDelay: number) {
    const angle = Math.random() * Math.PI * 2;
    const distance = SCATTER_MIN_DISTANCE + Math.random() * (SCATTER_MAX_DISTANCE - SCATTER_MIN_DISTANCE);
    const target = origin
      .clone()
      .add(new Vector3(Math.cos(angle) * distance, 0, Math.sin(angle) * distance));

    const mesh = this.createPickupMesh();
    mesh.position.copy(origin);
    ThreeC.addToScene(mesh);

    this.flying.push({
      mesh,
      flashOverlays: buildFlashOverlays(mesh),
      phase: "scatter",
      age: 0,
      from: origin.clone(),
      to: target,
      groundY: 0,
      bounceIndex: 0,
      staggerDelay,
      baseScale: 1,
      startDistanceFromCamera: 1,
    });
  }

  private static createPickupMesh(): Object3D {
    if (this.pickupTemplate) {
      const clone = this.pickupTemplate.clone();
      // The template itself is hidden in its resting spot in the map (see
      // TestSceneC) — clone() copies that visible:false too, so force it
      // back on regardless of which order those two happen in.
      clone.visible = true;
      return clone;
    }

    const mesh = new Mesh(
      new BoxGeometry(PICKUP_SIZE, PICKUP_SIZE, PICKUP_SIZE),
      new MeshStandardMaterial({ color: 0x8a5a34 })
    );
    mesh.castShadow = true;
    return mesh;
  }

  private static ensureUpdating() {
    if (this.initialized) return;
    this.initialized = true;
    UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
  }

  private static update(delta: number) {
    for (let i = this.flying.length - 1; i >= 0; i--) {
      const resource = this.flying[i];
      resource.age += delta;

      if (resource.phase === "scatter") {
        if (resource.age <= FLY_DURATION) {
          const t = resource.age / FLY_DURATION;
          const eased = 1 - (1 - t) * (1 - t); // easeOutQuad outward
          resource.mesh.position.lerpVectors(resource.from, resource.to, eased);
          // simple up-then-down arc on top of the outward lerp, so it "pops"
          // rather than sliding flat along the ground
          resource.mesh.position.y = resource.from.y + Math.sin(t * Math.PI) * SCATTER_ARC_HEIGHT;
          continue;
        }

        resource.phase = "bounce";
        resource.age = 0;
        resource.bounceIndex = 0;
        resource.groundY = resource.to.y;
        continue;
      }

      if (resource.phase === "bounce") {
        const duration = BOUNCE_DURATIONS[resource.bounceIndex];
        const height = BOUNCE_HEIGHTS[resource.bounceIndex];

        if (resource.age <= duration) {
          const t = resource.age / duration;
          resource.mesh.position.y = resource.groundY + Math.sin(t * Math.PI) * height;
          continue;
        }

        resource.mesh.position.y = resource.groundY;

        if (resource.bounceIndex < BOUNCE_HEIGHTS.length - 1) {
          resource.bounceIndex++;
          resource.age = 0;
          continue;
        }

        // Last bounce just touched down — flash/sparkle, once.
        SparkleFxC.spawnBurst(resource.mesh.getWorldPosition(new Vector3()));
        resource.phase = "impactHold";
        resource.age = 0;
        continue;
      }

      if (resource.phase === "impactHold") {
        const shrinkT = Math.min(resource.age / SHRINK_RAMP_DURATION, 1);
        // Ease in toward the shrink and stay there — no easing back out to
        // the original size (see chat).
        const shrink = shrinkT * shrinkT * (3 - 2 * shrinkT) * SHRINK_AMOUNT;
        resource.mesh.scale.setScalar(1 - shrink);

        const flashT = Math.min(resource.age / FLASH_DURATION, 1);
        const flashOpacity = 1 - flashT;
        for (const overlay of resource.flashOverlays) {
          (overlay.material as MeshBasicMaterial).opacity = flashOpacity;
        }

        // The stagger delay (see STAGGER_INTERVAL) just extends this wait
        // per-resource — the shrink/flash above are already fully settled
        // by IMPACT_HOLD_DURATION regardless, so waiting longer here only
        // delays the *launch*, not any of the visible impact feedback.
        if (resource.age < IMPACT_HOLD_DURATION + resource.staggerDelay) continue;

        for (const overlay of resource.flashOverlays) {
          (overlay.material as MeshBasicMaterial).opacity = 0;
        }

        if (!this.flyTarget) {
          this.collect(resource);
          this.flying.splice(i, 1);
          continue;
        }

        resource.phase = "toTarget";
        resource.age = 0;
        resource.from = resource.mesh.position.clone();
        resource.baseScale = resource.mesh.scale.x;
        resource.startDistanceFromCamera = CameraC_internal.getCamera().position.distanceTo(resource.from);
        continue;
      }

      // phase === "toTarget"
      if (!this.flyTarget) {
        this.collect(resource);
        this.flying.splice(i, 1);
        continue;
      }

      const t = Math.min(resource.age / TO_TARGET_DURATION, 1);
      const eased = t * t * (3 - 2 * t); // smoothstep — eases in and out of the flight
      // Re-sampled every frame, not snapshotted once at launch — the
      // camera (and so the HUD anchor's world position, see
      // HudC.getWorldAnchorPosition) can keep moving during the ~0.5s
      // flight (player walking, camera follow), so a one-time snapshot
      // would leave the resource flying toward a stale point that no
      // longer lines up with the icon (see chat: "втрачають потрібну
      // позицію і летять не туди"). Re-fetching each frame means it always
      // curves toward wherever the icon actually is *right now*.
      resource.to = this.flyTarget();
      resource.mesh.position.lerpVectors(resource.from, resource.to, eased);
      resource.mesh.position.y += Math.sin(t * Math.PI) * TO_TARGET_ARC_HEIGHT * (1 - t);

      // The flight target sits much closer to the camera than where
      // resources spawn (see HudC.getWorldAnchorPosition — it's pinned a
      // fixed, short distance in front of the camera), so without this the
      // resource would visibly *grow* as it approaches — pure perspective
      // foreshortening from closing distance, not any actual change to
      // world-space scale (see chat: "ресурс не має змінюватись в розмірі
      // в залежності від відстані"). Scaling by the ratio of current to
      // starting camera-distance exactly cancels that: apparent (on-screen)
      // size stays pinned to whatever it already was leaving the bounce,
      // no matter how close it gets.
      const cameraPosition = CameraC_internal.getCamera().position;
      const currentDistance = cameraPosition.distanceTo(resource.mesh.position);
      const perspectiveScale =
        resource.startDistanceFromCamera > 1e-6 ? currentDistance / resource.startDistanceFromCamera : 1;
      resource.mesh.scale.setScalar(resource.baseScale * perspectiveScale);

      // Over the last stretch of the flight, brightens right as it reaches
      // the target — reads as being absorbed into the counter's icon,
      // rather than just popping out of existence mid-air (see chat: "має
      // наприкінці збиратись в одиницю ресурсу і... залітати на місце
      // іконки"). Reuses the same flashOverlays the bounce-impact flash
      // used, idle since then.
      const convergeT = Math.max(0, (t - CONVERGE_START_FRACTION) / (1 - CONVERGE_START_FRACTION));
      for (const overlay of resource.flashOverlays) {
        (overlay.material as MeshBasicMaterial).opacity = convergeT;
      }

      if (t >= 1) {
        this.collect(resource);
        this.flying.splice(i, 1);
      }
    }
  }

  private static collect(resource: FlyingResource) {
    this.collected++;
    console.log(`[ResourceC] gathered wood (${this.collected} total)`);
    ThreeC.removeFromScene(resource.mesh);
  }
}
