import { Box3, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, Vector3 } from "three";
import { CameraC_internal, TweenC, UpdateController } from "@hitplay/playable_template";
import { Easing, Tween } from "@tweenjs/tween.js";
import { ThreeC } from "./ThreeC";
import { RESOURCE_ICON_BASE_SCALE, ResourceC } from "./ResourceC";
import { HudC } from "./HudC";

// How many resources need to actually make it into the zone before it's
// done and disappears — what happens after that is intentionally out of
// scope for now (see chat).
const RESOURCES_TO_CLOSE_ZONE = 20;

// While standing in the zone with resources still to deliver, one flies in
// every DEPOSIT_INTERVAL seconds — a steady drain rather than a single
// instant dump.
const DEPOSIT_INTERVAL = 0.3;
const DEPOSIT_FLIGHT_DURATION_MS = 500;
// Redundant safety-net cleanup delay past the tween's own duration — see
// the note in playDepositFlight on why this exists.
const DEPOSIT_FLIGHT_CLEANUP_MARGIN_MS = 300;

const PAY_ZONE_SHRINK_MS = 400;

// Extra multiplier on top of RESOURCE_ICON_BASE_SCALE (see ResourceC), just
// for this deposit flight — tune this, not the shared constant, if only
// the fly-to-Pay-Zone token needs to look smaller/bigger (see chat: "де я
// можу зменшити розмір ресурсу під час анімації заповнення пей зони?").
const DEPOSIT_FLIGHT_SCALE_MULTIPLIER = 0.3;

// Fill overlay: a flat quad laid over the zone's own footprint, growing
// from its near edge (fixed, closest to the camera) toward the far edge as
// deposits come in — a ground-level wipe, not a rising column (see chat:
// two earlier attempts — a horizontal X wipe read as "right to left", then
// a rising box read as "water filling a pool" — both wrong; this reads as
// "bottom to top" on screen because +Z, this game's forward/"away from
// camera" direction, projects toward the top of the screen under this
// isometric-ish camera).
const FILL_COLOR = 0x5ce65c;
const FILL_OPACITY = 0.55;
// Sits just above the zone's own ground-level quad so it doesn't z-fight
// with it, but still reads as "on the ground" rather than floating.
const FILL_Y_OFFSET = 0.02;

// Reuses the map's existing (already-visible) "UI_Interactive_Zone_02"
// marker as both the pay zone's location and its visual — see CLAUDE.md.
// ResourceC.getCollectedCount() keeps counting resources the moment
// they're picked up near whatever crate dropped them (unrelated to this
// zone); this controller separately drains that total into the zone,
// resource by resource, only while the player is standing inside it.
export class PayZoneC {
  private static payZoneNode: Object3D | null = null;
  private static target: Object3D | null = null;
  // The zone's actual world-space footprint (an XZ rectangle), computed
  // once from its real geometry rather than guessed as a fixed radius — a
  // hardcoded radius previously undershot the visible dashed square, so
  // standing anywhere but right on its center pivot silently never
  // counted as "inside".
  private static zoneBounds: Box3 | null = null;
  private static closed = false;
  private static deposited = 0;
  private static depositCooldown = 0;
  private static fillMesh: Mesh | null = null;

  static init(payZoneNode: Object3D | undefined, target: Object3D) {
    this.payZoneNode = payZoneNode ?? null;
    this.target = target;
    this.zoneBounds = this.payZoneNode ? new Box3().setFromObject(this.payZoneNode) : null;
    this.closed = false;
    this.deposited = 0;
    this.depositCooldown = 0;
    this.fillMesh = this.zoneBounds ? this.buildFillMesh(this.zoneBounds) : null;
    this.updateFillVisual();

    UpdateController.Instance.onUpdate.addDelegate((delta) => this.update(delta));
  }

  // A unit quad, later scaled/positioned per-frame in updateFillVisual — a
  // fixed width (full X span of the zone) but a depth (world Z) that grows
  // from 0 up to the zone's full Z span as `deposited` approaches
  // RESOURCES_TO_CLOSE_ZONE.
  private static buildFillMesh(bounds: Box3): Mesh {
    const mesh = new Mesh(
      new PlaneGeometry(1, 1),
      new MeshBasicMaterial({
        color: FILL_COLOR,
        transparent: true,
        opacity: FILL_OPACITY,
        depthWrite: false,
      })
    );
    mesh.rotation.x = -Math.PI / 2; // lie flat on the ground, facing up
    mesh.position.y = bounds.min.y + FILL_Y_OFFSET;
    mesh.position.x = (bounds.min.x + bounds.max.x) / 2;
    mesh.scale.x = bounds.max.x - bounds.min.x;
    ThreeC.addToScene(mesh);
    return mesh;
  }

  // Grows the fill quad from the zone's min-Z edge (fixed, nearest the
  // camera) toward its max-Z edge as `fraction` (0..1) increases — scaling
  // a centered quad also moves its center, so the Z position is re-derived
  // each time to keep that near edge pinned in place instead of drifting.
  private static updateFillVisual() {
    if (!this.fillMesh || !this.zoneBounds) return;

    const fraction = this.closed ? 0 : Math.min(this.deposited / RESOURCES_TO_CLOSE_ZONE, 1);
    const depth = this.zoneBounds.max.z - this.zoneBounds.min.z;
    const filledDepth = depth * fraction;

    this.fillMesh.scale.y = filledDepth; // local Y — maps to world Z after the rotation above
    this.fillMesh.position.z = this.zoneBounds.min.z + filledDepth / 2;
  }

  static getTargetWorldPosition(): Vector3 {
    return this.payZoneNode ? this.payZoneNode.getWorldPosition(new Vector3()) : new Vector3();
  }

  // How many resources have actually been delivered so far — HudC reads
  // this (via an injected getter, not a direct import, to avoid a
  // HudC <-> PayZoneC import cycle) to show a live balance that rises on
  // collection and falls as resources leave for the Pay Zone.
  static getDepositedCount(): number {
    return this.deposited;
  }

  // XZ containment against the zone's real footprint (ignores Y — the
  // zone lies flat on the ground, height doesn't matter).
  static isPlayerInside(playerPosition: Vector3): boolean {
    if (!this.zoneBounds) return false;
    return (
      playerPosition.x >= this.zoneBounds.min.x &&
      playerPosition.x <= this.zoneBounds.max.x &&
      playerPosition.z >= this.zoneBounds.min.z &&
      playerPosition.z <= this.zoneBounds.max.z
    );
  }

  private static update(delta: number) {
    if (this.closed || !this.payZoneNode || !this.target) return;
    if (this.depositCooldown > 0) this.depositCooldown -= delta;

    const backlog = ResourceC.getCollectedCount() - this.deposited;
    if (backlog <= 0) return;
    if (this.depositCooldown > 0) return;
    if (!this.isPlayerInside(this.target.position)) return;

    this.depositCooldown = DEPOSIT_INTERVAL;
    this.deposited++;
    this.playDepositFlight(HudC.getWorldAnchorPosition(), this.payZoneNode);
    this.updateFillVisual();

    if (this.deposited >= RESOURCES_TO_CLOSE_ZONE) {
      this.closed = true;
      console.log(`[PayZoneC] zone full (${this.deposited} delivered) — closing`);
      this.playDisappear(this.payZoneNode);
    }
  }

  // One resource, flying from the HUD counter (see chat: "від лічильника
  // до пей зони") into the zone itself — the player only needs to be
  // standing in the zone to trigger it, not to be the flight's origin.
  //
  // Traced via temporary logging (see chat): occasionally one of these
  // tweens never fires onComplete and freezes mid-flight forever — a
  // visible board-textured icon just hanging in the air (user report:
  // "текстура доски... постійно статично відображається"). Couldn't pin
  // down why inside tween.js/TweenC (a single, non-chained tween, added to
  // the group before .start() — the usual "chain() doesn't register the
  // second link" bug from Day 6 doesn't apply here). Rather than keep
  // digging, added a hard, redundant cleanup: a plain timeout that removes
  // the mesh regardless of whether the tween ever completes. Calling
  // removeFromScene twice (once from onComplete, once from the timeout, if
  // both fire) is harmless — the second call just finds no parent to
  // detach from.
  private static playDepositFlight(from: Vector3, payZoneNode: Object3D) {
    const mesh = ResourceC.createVisual();
    const to = payZoneNode.getWorldPosition(new Vector3());
    mesh.position.copy(from);
    ThreeC.addToScene(mesh);

    // No rotation (see chat: "ротейту не має бути") — same as the
    // ground-to-HUD flight in ResourceC, this is position/scale only.
    //
    // Same perspective-compensation trick as ResourceC's toTarget phase,
    // just in reverse: this flight starts right at the HUD anchor (close
    // to the camera, see HudC.getWorldAnchorPosition) and moves out to the
    // Pay Zone (typically much farther away), so without this it would
    // visibly *shrink* as it recedes — pure perspective, not an actual
    // size change. Scaling by the ratio of current to starting
    // camera-distance cancels that, keeping it the one size the whole
    // flight (see chat: "розмір має бути таким самим як і в цій
    // анімації"). Anchored to RESOURCE_ICON_BASE_SCALE, not the mesh's own
    // default (1) — this token is a fresh ResourceC.createVisual(), not one
    // of ResourceC's own bounced-and-shrunk FlyingResources, so without
    // this it started noticeably larger than the resource that was just
    // sitting at that same HUD icon a moment ago (see chat: "дуже великий,
    // требя його уніфікувати").
    const cameraPosition = CameraC_internal.getCamera().position;
    const startDistance = cameraPosition.distanceTo(from);

    const flight = new Tween({ t: 0 })
      .to({ t: 1 }, DEPOSIT_FLIGHT_DURATION_MS)
      .easing(Easing.Quadratic.InOut)
      .onUpdate(({ t }) => {
        mesh.position.lerpVectors(from, to, t);
        const currentDistance = cameraPosition.distanceTo(mesh.position);
        const perspectiveScale = startDistance > 1e-6 ? currentDistance / startDistance : 1;
        mesh.scale.setScalar(perspectiveScale * RESOURCE_ICON_BASE_SCALE * DEPOSIT_FLIGHT_SCALE_MULTIPLIER);
      })
      .onComplete(() => ThreeC.removeFromScene(mesh));

    TweenC.add(flight);
    flight.start();

    setTimeout(() => ThreeC.removeFromScene(mesh), DEPOSIT_FLIGHT_DURATION_MS + DEPOSIT_FLIGHT_CLEANUP_MARGIN_MS);
  }

  // Shrinks the zone's own marker AND the fill overlay together, as one
  // piece — the fill previously just popped invisible instantly once the
  // marker's shrink finished (see chat: "прогрес заповненості не
  // зменшується разом з пей зоною"). Scaling the fill quad down from
  // whatever it currently is (full, since this only runs once the zone is
  // done) shrinks it toward its own center in lockstep with the marker.
  private static playDisappear(node: Object3D) {
    const fillMesh = this.fillMesh;
    const fillBaseScaleX = fillMesh?.scale.x ?? 0;
    const fillBaseScaleY = fillMesh?.scale.y ?? 0;

    const shrink = new Tween({ s: 1 })
      .to({ s: 0 }, PAY_ZONE_SHRINK_MS)
      .easing(Easing.Quadratic.In)
      .onUpdate(({ s }) => {
        node.scale.setScalar(s);
        fillMesh?.scale.set(fillBaseScaleX * s, fillBaseScaleY * s, 1);
      })
      .onComplete(() => {
        node.visible = false;
        if (fillMesh) fillMesh.visible = false;
      });

    TweenC.add(shrink);
    shrink.start();
  }
}
