import * as THREE from 'three';
import {
  angDiff, clamp, damp, easeInOutQuad, easeOutQuad, easeOutQuart, lerp, TAU,
} from './util';
import type { Effects } from './effects';
import type { Sfx } from './audio';

export interface BladeArc {
  yaw: number;
  radius: number;
  halfArc: number;
  dmg: number;
  id: number;
}

const REST_ARM_X = -0.55;
const W = 0.085; // windup
const S = 0.125; // sweep
const R = 0.15; // recovery
const SPIN_T = 0.55;
const CHARGE_TIME = 0.6;

type HeroState = 'idle' | 'swing' | 'charge' | 'spin';

/**
 * Procedurally rigged hero with a 3-beat sword swing
 * (windup -> arc -> follow-through) and a hold-to-charge spin attack.
 */
export class Hero {
  root = new THREE.Group();
  yaw = 0;
  vx = 0;
  vz = 0;
  state: HeroState = 'idle';
  radius = 0.38;
  speed = 5.3;

  private rig = new THREE.Group();
  private torso = new THREE.Group();
  private armPivot = new THREE.Group();
  private shoulderR = new THREE.Group();
  private shoulderL = new THREE.Group();
  private legL = new THREE.Group();
  private legR = new THREE.Group();
  private head = new THREE.Group();
  private bladeBase = new THREE.Object3D();
  private bladeTip = new THREE.Object3D();
  private mats: THREE.MeshLambertMaterial[] = [];

  private swingT = 0;
  private swingDir = 1;
  private swingId = 0;
  private comboQueued = false;
  private arcActive = false;
  private chargeT = 0;
  private chargeAnnounced = false;
  private spinT = 0;
  private walkPhase = 0;
  private walkAmt = 0;
  private idleT = 0;
  private kbx = 0;
  private kbz = 0;
  invulnT = 0;
  private trailOn = false;
  private vA = new THREE.Vector3();
  private vB = new THREE.Vector3();

  constructor(
    private effects: Effects,
    private sfx: Sfx,
  ) {
    this.build();
  }

  private mat(color: string): THREE.MeshLambertMaterial {
    const m = new THREE.MeshLambertMaterial({ color, flatShading: true });
    this.mats.push(m);
    return m;
  }

  private build(): void {
    const skin = this.mat('#f7c990');
    const tunic = this.mat('#2e9c8a');
    const tunicDark = this.mat('#1f7263');
    const boots = this.mat('#7a5a3a');
    const hair = this.mat('#5a3a22');
    const hatMat = this.mat('#e2723c');
    const wood = this.mat('#8a5a33');
    const gold = this.mat('#e0b23e');
    const steel = new THREE.MeshStandardMaterial({
      color: '#e6edf5',
      metalness: 0.55,
      roughness: 0.3,
      emissive: '#9fc4ff',
      emissiveIntensity: 0.18,
      flatShading: true,
    });

    this.root.add(this.rig);

    // blob shadow
    const blob = new THREE.Mesh(
      new THREE.CircleGeometry(0.4, 18),
      new THREE.MeshBasicMaterial({ color: '#1a2e16', transparent: true, opacity: 0.25, depthWrite: false }),
    );
    blob.rotation.x = -Math.PI / 2;
    blob.position.y = 0.03;
    this.root.add(blob);

    // legs
    const legGeo = new THREE.CapsuleGeometry(0.078, 0.18, 3, 7);
    for (const [grp, sx] of [[this.legL, -0.12], [this.legR, 0.12]] as [THREE.Group, number][]) {
      grp.position.set(sx, 0.34, 0);
      const leg = new THREE.Mesh(legGeo, boots);
      leg.position.y = -0.16;
      leg.castShadow = true;
      grp.add(leg);
      this.rig.add(grp);
    }

    // torso
    this.torso.position.y = 0.42;
    this.rig.add(this.torso);
    const body = new THREE.Mesh(new THREE.CylinderGeometry(0.21, 0.3, 0.46, 8), tunic);
    body.position.y = 0.22;
    body.castShadow = true;
    this.torso.add(body);
    const belt = new THREE.Mesh(new THREE.CylinderGeometry(0.295, 0.305, 0.08, 8), tunicDark);
    belt.position.y = 0.06;
    this.torso.add(belt);

    // shield on the back
    const shield = new THREE.Group();
    shield.position.set(0, 0.26, -0.28);
    shield.rotation.x = 0.12;
    const shieldDisc = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.24, 0.045, 12), wood);
    shieldDisc.rotation.x = Math.PI / 2;
    shieldDisc.castShadow = true;
    const shieldRim = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.028, 6, 14), tunicDark);
    const shieldBoss = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 5), gold);
    shieldBoss.position.z = -0.04;
    shield.add(shieldDisc, shieldRim, shieldBoss);
    this.torso.add(shield);

    // arms hang from a pivot that yaws around the body during swings
    this.armPivot.position.y = 0.46;
    this.torso.add(this.armPivot);
    const armGeo = new THREE.CapsuleGeometry(0.068, 0.2, 3, 7);
    const handGeo = new THREE.SphereGeometry(0.075, 6, 5);

    this.shoulderR.position.set(0.27, 0, 0);
    this.shoulderR.rotation.x = REST_ARM_X;
    const armR = new THREE.Mesh(armGeo, tunic);
    armR.position.y = -0.16;
    armR.castShadow = true;
    const handR = new THREE.Mesh(handGeo, skin);
    handR.position.y = -0.31;
    this.shoulderR.add(armR, handR);
    this.armPivot.add(this.shoulderR);

    this.shoulderL.position.set(-0.27, 0, 0);
    const armL = new THREE.Mesh(armGeo, tunic);
    armL.position.y = -0.16;
    armL.castShadow = true;
    const handL = new THREE.Mesh(handGeo, skin);
    handL.position.y = -0.31;
    this.shoulderL.add(armL, handL);
    this.armPivot.add(this.shoulderL);

    // sword in the right hand, blade extending along the arm
    const sword = new THREE.Group();
    sword.position.y = -0.33;
    sword.rotation.x = Math.PI;
    const handle = new THREE.Mesh(new THREE.CylinderGeometry(0.026, 0.032, 0.13, 6), wood);
    handle.position.y = -0.02;
    const guard = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.03, 0.052), gold);
    guard.position.y = 0.055;
    const blade = new THREE.Mesh(new THREE.BoxGeometry(0.048, 0.46, 0.016), steel);
    blade.position.y = 0.3;
    blade.castShadow = true;
    const tip = new THREE.Mesh(new THREE.ConeGeometry(0.034, 0.1, 4), steel);
    tip.position.y = 0.58;
    tip.rotation.y = Math.PI / 4;
    this.bladeBase.position.y = 0.16;
    this.bladeTip.position.y = 0.62;
    sword.add(handle, guard, blade, tip, this.bladeBase, this.bladeTip);
    this.shoulderR.add(sword);

    // head
    this.head.position.y = 0.62;
    this.torso.add(this.head);
    const skull = new THREE.Mesh(new THREE.SphereGeometry(0.26, 10, 8), skin);
    skull.castShadow = true;
    const hairCap = new THREE.Mesh(new THREE.SphereGeometry(0.275, 10, 8), hair);
    hairCap.position.set(0, 0.05, -0.05);
    hairCap.scale.set(1, 0.85, 1);
    const eyeGeo = new THREE.SphereGeometry(0.034, 6, 5);
    const eyeMat = this.mat('#23191a');
    const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
    eyeL.position.set(-0.1, 0.02, 0.23);
    const eyeR = new THREE.Mesh(eyeGeo, eyeMat);
    eyeR.position.set(0.1, 0.02, 0.23);
    const nose = new THREE.Mesh(new THREE.SphereGeometry(0.04, 5, 4), skin);
    nose.position.set(0, -0.05, 0.26);
    const hat = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.4, 8), hatMat);
    hat.position.set(0, 0.28, -0.02);
    hat.rotation.x = -0.16;
    hat.castShadow = true;
    this.head.add(skull, hairCap, eyeL, eyeR, nose, hat);
  }

  get x(): number { return this.root.position.x; }
  get z(): number { return this.root.position.z; }

  teleport(x: number, z: number, groundY: (x: number, z: number) => number): void {
    this.root.position.set(x, groundY(x, z), z);
    this.yaw = Math.PI; // face up-screen, deterministic for tests
    this.root.rotation.y = this.yaw;
    this.vx = 0;
    this.vz = 0;
  }

  /** Press attack: swing, or queue a combo follow-up mid-swing. */
  tryAttack(): void {
    if (this.state === 'idle' || this.state === 'charge') {
      this.startSwing();
    } else if (this.state === 'swing' && this.swingT > W + S * 0.5) {
      this.comboQueued = true;
    }
  }

  private startSwing(fromCombo = false): void {
    this.state = 'swing';
    this.swingT = fromCombo ? 0.03 : 0;
    this.swingId++;
    this.swingDir *= -1;
    this.comboQueued = false;
    this.chargeT = 0;
    this.sfx.swing();
  }

  private startSpin(): void {
    this.state = 'spin';
    this.spinT = 0;
    this.swingId++;
    this.chargeT = 0;
    this.chargeAnnounced = false;
    this.sfx.spin();
  }

  hurt(dirX: number, dirZ: number): void {
    this.kbx = dirX * 7.5;
    this.kbz = dirZ * 7.5;
    this.invulnT = 1.0;
    if (this.state === 'swing' || this.state === 'spin') {
      this.state = 'idle';
      this.arcActive = false;
    }
  }

  /** Active damage arc this frame, or null. */
  bladeArc(): BladeArc | null {
    if (this.state === 'swing' && this.arcActive) {
      return {
        yaw: this.yaw + this.armPivot.rotation.y,
        radius: 2.3,
        halfArc: 0.62,
        dmg: 1,
        id: this.swingId,
      };
    }
    if (this.state === 'spin' && this.spinT > 0.06 && this.spinT < SPIN_T - 0.04) {
      return { yaw: this.yaw, radius: 2.55, halfArc: 0.85, dmg: 2, id: this.swingId };
    }
    return null;
  }

  update(
    dt: number,
    mx: number,
    mz: number,
    attackHeld: boolean,
    groundY: (x: number, z: number) => number,
  ): void {
    this.idleT += dt;
    const moving = Math.hypot(mx, mz) > 0.01;

    // --- locomotion ---
    let control = 1;
    let maxSpeed = this.speed;
    if (this.state === 'swing') {
      const t = this.swingT;
      control = t < W + S ? 0.1 : clamp((t - W - S) / R, 0, 1);
    } else if (this.state === 'spin') {
      control = 0.15;
    } else if (this.state === 'charge') {
      maxSpeed = this.speed * 0.45;
    }

    const txv = mx * maxSpeed * control;
    const tzv = mz * maxSpeed * control;
    this.vx = damp(this.vx, txv, 11, dt);
    this.vz = damp(this.vz, tzv, 11, dt);

    // lunge forward during the sweep
    if (this.state === 'swing' && this.swingT >= W && this.swingT < W + S) {
      const k = 1 - (this.swingT - W) / S;
      this.vx += Math.sin(this.yaw) * 5.5 * k * dt * 8;
      this.vz += Math.cos(this.yaw) * 5.5 * k * dt * 8;
      const sp = Math.hypot(this.vx, this.vz);
      const cap = 4.2;
      if (sp > cap) {
        this.vx *= cap / sp;
        this.vz *= cap / sp;
      }
    }

    // knockback decay
    this.kbx *= Math.exp(-7 * dt);
    this.kbz *= Math.exp(-7 * dt);

    this.root.position.x += (this.vx + this.kbx) * dt;
    this.root.position.z += (this.vz + this.kbz) * dt;
    this.root.position.y = groundY(this.x, this.z);

    // facing
    if (moving && (this.state === 'idle' || this.state === 'charge')) {
      const target = Math.atan2(mx, mz);
      this.yaw += angDiff(this.yaw, target) * Math.min(1, 14 * dt);
      this.yaw = ((this.yaw % TAU) + TAU) % TAU;
    }
    this.root.rotation.y = this.yaw;

    // --- state machines ---
    if (this.state === 'swing') this.updateSwing(dt, attackHeld);
    else if (this.state === 'spin') this.updateSpin(dt);
    else if (this.state === 'charge') this.updateCharge(dt, attackHeld);
    else this.poseRest();

    // walk cycle
    const speedNow = Math.hypot(this.vx, this.vz);
    this.walkAmt = damp(this.walkAmt, clamp(speedNow / this.speed, 0, 1), 10, dt);
    this.walkPhase += speedNow * dt * 2.7;
    const swing = Math.sin(this.walkPhase * Math.PI) * 0.65 * this.walkAmt;
    this.legL.rotation.x = swing;
    this.legR.rotation.x = -swing;
    this.rig.position.y = Math.abs(Math.sin(this.walkPhase * Math.PI)) * 0.045 * this.walkAmt
      + Math.sin(this.idleT * 2.2) * 0.012;
    if (this.state === 'idle') {
      this.shoulderL.rotation.x = -swing * 0.7 + Math.sin(this.idleT * 2.2) * 0.04;
      this.shoulderR.rotation.x = REST_ARM_X + swing * 0.55;
    }

    // invuln blink
    if (this.invulnT > 0) {
      this.invulnT -= dt;
      this.rig.visible = Math.floor(this.invulnT * 14) % 2 === 0;
      if (this.invulnT <= 0) this.rig.visible = true;
    }

    // sword trail sampling
    if (this.trailOn) {
      this.root.updateMatrixWorld(true);
      this.bladeBase.getWorldPosition(this.vA);
      this.bladeTip.getWorldPosition(this.vB);
      this.effects.trail.push(this.vA, this.vB);
    }
  }

  private updateSwing(dt: number, attackHeld: boolean): void {
    this.swingT += dt;
    const t = this.swingT;
    const dir = this.swingDir;
    this.arcActive = false;
    this.trailOn = t > W * 0.55 && t < W + S + 0.03;

    const start = 1.4 * dir;
    const end = -1.45 * dir;
    let pivotY: number;
    let armX: number;
    let twist: number;

    if (t < W) {
      const k = easeOutQuad(t / W);
      pivotY = start * k;
      armX = lerp(REST_ARM_X, -1.52, k);
      twist = 0.32 * dir * k;
    } else if (t < W + S) {
      const k = easeOutQuart((t - W) / S);
      pivotY = lerp(start, end, k);
      armX = -1.52;
      twist = lerp(0.32, -0.4, k) * dir;
      this.arcActive = k > 0.02 && k < 0.985;
    } else if (t < W + S + R) {
      const k = easeInOutQuad((t - W - S) / R);
      pivotY = lerp(end, 0, k);
      armX = lerp(-1.52, REST_ARM_X, k);
      twist = lerp(-0.4 * dir, 0, k);
      if (this.comboQueued && k > 0.3) {
        this.startSwing(true);
        return;
      }
    } else {
      this.armPivot.rotation.y = 0;
      this.shoulderR.rotation.x = REST_ARM_X;
      this.torso.rotation.y = 0;
      this.shoulderL.rotation.x = 0;
      this.state = attackHeld ? 'charge' : 'idle';
      this.chargeT = 0;
      this.chargeAnnounced = false;
      return;
    }

    this.armPivot.rotation.y = pivotY;
    this.shoulderR.rotation.x = armX;
    this.torso.rotation.y = twist;
    this.shoulderL.rotation.x = 0.3; // tucked
    this.head.rotation.y = -twist * 0.5;
  }

  private updateSpin(dt: number): void {
    this.spinT += dt;
    const t = this.spinT;
    this.trailOn = t < SPIN_T - 0.02;
    if (t >= SPIN_T) {
      this.state = 'idle';
      this.trailOn = false;
      this.head.rotation.y = 0;
      return;
    }
    const k = easeInOutQuad(t / SPIN_T);
    this.yaw += (k - easeInOutQuad(Math.max(0, t - dt) / SPIN_T)) * TAU * 1.0 * -this.swingDir;
    this.root.rotation.y = this.yaw;
    this.shoulderR.rotation.x = -1.52;
    this.armPivot.rotation.y = 0;
    this.torso.rotation.y = 0;
    this.shoulderL.rotation.x = 0.5;
  }

  private updateCharge(dt: number, attackHeld: boolean): void {
    this.chargeT += dt;
    this.shoulderR.rotation.x = damp(this.shoulderR.rotation.x, -1.15, 14, dt);
    this.armPivot.rotation.y = damp(this.armPivot.rotation.y, 0.3, 14, dt);
    this.torso.rotation.y = damp(this.torso.rotation.y, 0.2, 14, dt);
    this.shoulderL.rotation.x = 0.2;
    if (this.chargeT >= CHARGE_TIME) {
      if (!this.chargeAnnounced) {
        this.chargeAnnounced = true;
        this.sfx.chargeReady();
      }
      // sparkle on the blade tip while charged
      if (Math.random() < 0.35) {
        this.root.updateMatrixWorld(true);
        this.bladeTip.getWorldPosition(this.vA);
        this.effects.sparkle(this.vA.x, this.vA.y, this.vA.z);
      }
    }
    if (!attackHeld) {
      if (this.chargeT >= CHARGE_TIME) {
        this.startSpin();
      } else {
        this.state = 'idle';
        this.chargeT = 0;
        this.chargeAnnounced = false;
      }
    }
  }

  private poseRest(): void {
    this.trailOn = false;
    this.armPivot.rotation.y = damp(this.armPivot.rotation.y, 0, 18, 1 / 60);
    this.torso.rotation.y = damp(this.torso.rotation.y, 0, 18, 1 / 60);
    this.head.rotation.y = damp(this.head.rotation.y, 0, 18, 1 / 60);
  }
}
