import * as THREE from 'three';
import { clamp } from './util';
import { World } from './world';
import { GrassField } from './grass';
import { Effects } from './effects';
import { Hero } from './hero';
import { Pots } from './pots';
import { Slimes } from './slimes';
import { Pickups, type PickupKind } from './pickups';
import { Input } from './input';
import { Hud } from './hud';
import { Sfx } from './audio';

export type Phase = 'title' | 'playing';

const GEL_COLOR = new THREE.Color('#5fe08c');

export class Game {
  phase: Phase = 'title';
  hearts = 3;
  gems = 0;
  stats = { grassCut: 0, potsBroken: 0, slimeHits: 0 };

  world: World;
  grass: GrassField;
  hero: Hero;
  pots: Pots;
  slimes: Slimes;
  pickups: Pickups;
  effects: Effects;
  input: Input;
  hud: Hud;
  sfx = new Sfx();

  private renderer: THREE.WebGLRenderer;
  private scene = new THREE.Scene();
  private camera: THREE.PerspectiveCamera;
  private sun: THREE.DirectionalLight;
  private clock = new THREE.Clock();
  private camTarget = new THREE.Vector3();
  private camScale = 1;
  private baseFov = 42;
  private shake = 0;
  private punch = 0;
  private hitstopT = 0;
  private titleT = 0;
  private contactCooldown = 0;
  private grassSfxCooldown = 0;
  private dead = false;
  private deadT = 0;

  constructor(container: HTMLElement) {
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
    this.renderer.toneMappingExposure = 1.12;
    container.appendChild(this.renderer.domElement);

    this.scene.background = new THREE.Color('#87c2dd');
    this.camera = new THREE.PerspectiveCamera(this.baseFov, 1, 0.1, 200);

    // toy-box lighting: warm sun + soft sky bounce
    const hemi = new THREE.HemisphereLight('#cfe8ff', '#b98a3e', 0.55);
    this.scene.add(hemi);
    this.sun = new THREE.DirectionalLight('#fff2dc', 2.3);
    this.sun.position.set(9, 12, 7);
    this.sun.castShadow = true;
    this.sun.shadow.mapSize.set(2048, 2048);
    this.sun.shadow.camera.near = 1;
    this.sun.shadow.camera.far = 50;
    const d = 16;
    this.sun.shadow.camera.left = -d;
    this.sun.shadow.camera.right = d;
    this.sun.shadow.camera.top = d;
    this.sun.shadow.camera.bottom = -d;
    this.sun.shadow.bias = -0.0004;
    this.sun.shadow.normalBias = 0.03;
    this.scene.add(this.sun, this.sun.target);

    this.world = new World();
    this.scene.add(this.world.group);
    this.grass = new GrassField(this.scene, this.world);
    this.effects = new Effects(this.scene);
    this.hero = new Hero(this.effects, this.sfx);
    this.hero.teleport(0, 3.5, (x, z) => this.world.groundY(x, z));
    this.hero.yaw = 0; // face the camera on the title screen
    this.scene.add(this.hero.root);
    this.pots = new Pots(this.scene, this.world.potSpots);
    this.slimes = new Slimes(this.scene, this.world);
    this.pickups = new Pickups(
      this.scene,
      (x, z) => this.world.groundY(x, z),
      (kind, x, y, z) => this.collect(kind, x, y, z),
    );
    this.input = new Input();
    this.hud = new Hud();
    this.hud.setHearts(this.hearts);

    this.input.onAny = () => this.startGame();
    document.getElementById('title-screen')!.addEventListener('pointerdown', () => this.startGame());

    window.addEventListener('resize', () => this.fitCamera());
    this.fitCamera();
  }

  startGame(): void {
    this.sfx.resume();
    if (this.phase !== 'title') return;
    this.phase = 'playing';
    document.getElementById('title-screen')!.classList.add('hidden');
  }

  teleport(x: number, z: number): void {
    const b = this.world.bounds;
    this.hero.teleport(clamp(x, -b, b), clamp(z, -b, b), (xx, zz) => this.world.groundY(xx, zz));
  }

  /** Programmatic attack for tests/console. */
  debugAttack(): void {
    this.startGame();
    this.hero.tryAttack();
  }

  private collect(kind: PickupKind, x: number, y: number, z: number): void {
    if (kind === 'heart') {
      this.hearts = Math.min(3, this.hearts + 1);
      this.hud.setHearts(this.hearts);
      this.sfx.heart();
    } else {
      this.gems++;
      this.hud.setGems(this.gems);
      this.sfx.gem();
    }
    this.effects.sparkle(x, y + 0.2, z);
    this.effects.sparkle(x, y + 0.4, z);
  }

  private maybeDrop(x: number, y: number, z: number, gemChance: number, heartChance: number): void {
    const roll = Math.random();
    if (roll < gemChance) this.pickups.spawn('gem', x, y, z);
    else if (roll < gemChance + heartChance) this.pickups.spawn('heart', x, y, z);
  }

  requestHitstop(t: number): void {
    this.hitstopT = Math.max(this.hitstopT, t);
  }

  addShake(amt: number): void {
    this.shake = Math.max(this.shake, amt);
  }

  private fitCamera(): void {
    const w = window.innerWidth;
    const h = window.innerHeight;
    const aspect = w / h;
    this.camera.aspect = aspect;
    this.baseFov = aspect < 1 ? 46 : 40;
    this.camScale = clamp(0.8 / aspect, 0.9, 1.35);
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(w, h);
  }

  run(): void {
    this.renderer.setAnimationLoop(() => this.frame());
  }

  private frame(): void {
    const raw = Math.min(this.clock.getDelta(), 0.05);
    let dt = raw;
    if (this.hitstopT > 0) {
      this.hitstopT -= raw;
      dt = raw * 0.05; // near-freeze: things still creep, which reads better than a full stop
    }

    this.input.poll();

    if (this.phase === 'playing' && !this.dead) {
      while (this.input.consumePress()) this.hero.tryAttack();
      this.hero.update(
        dt,
        this.input.mx,
        this.input.mz,
        this.input.attackHeld,
        (x, z) => this.world.groundY(x, z),
      );
      const pos = this.world.resolve(
        { x: this.hero.root.position.x, z: this.hero.root.position.z },
        this.hero.radius,
        this.pots.colliders(),
      );
      this.hero.root.position.x = pos.x;
      this.hero.root.position.z = pos.z;
      this.combat();
    } else if (this.phase === 'playing' && this.dead) {
      this.deadT -= raw;
      if (this.deadT <= 0) {
        this.dead = false;
        this.hearts = 3;
        this.hud.setHearts(this.hearts);
        this.teleport(0, 3.5);
        this.hero.invulnT = 1.2;
        document.getElementById('fade')!.classList.remove('show');
      }
    }

    const touchers = this.slimes.update(dt, this.hero.x, this.hero.z);
    this.contactDamage(touchers, dt);

    this.world.update(dt);
    this.grass.update(dt);
    this.pots.update(dt);
    this.pickups.update(dt, this.hero.x, this.hero.z);
    this.effects.update(dt, this.camera);
    this.grassSfxCooldown -= dt;

    this.updateCamera(raw);
    this.renderer.render(this.scene, this.camera);
  }

  private combat(): void {
    const arc = this.hero.bladeArc();
    if (!arc) return;
    const hx = this.hero.x;
    const hz = this.hero.z;
    const heroY = this.hero.root.position.y;

    const cut = this.grass.cutInArc(hx, hz, arc.yaw, arc.radius, arc.halfArc, (x, y, z) => {
      this.effects.grassBurst(x, y, z);
      this.maybeDrop(x, y, z, 0.07, 0.04);
    });
    if (cut > 0) {
      this.stats.grassCut += cut;
      if (this.grassSfxCooldown <= 0) {
        this.sfx.grass();
        this.grassSfxCooldown = 0.07;
      }
    }

    const smashed = this.pots.tryHit(hx, hz, heroY, arc.yaw, arc.radius, arc.halfArc, (x, y, z) => {
      this.effects.potBurst(x, y, z, y);
      this.effects.sparkBurst(x, y + 0.35, z, 5);
      this.maybeDrop(x, y, z, 0.4, 0.15);
      this.sfx.pot();
    });
    if (smashed > 0) {
      this.stats.potsBroken += smashed;
      this.addShake(0.3);
      this.requestHitstop(0.045);
    }

    const hits = this.slimes.tryHit(hx, hz, arc.yaw, arc.radius, arc.halfArc, arc.dmg, arc.id);
    for (const hit of hits) {
      this.stats.slimeHits++;
      this.effects.gelBurst(hit.x, hit.y - 0.2, hit.z, GEL_COLOR, 6);
      this.effects.sparkBurst(hit.x, hit.y, hit.z, 8);
      this.effects.ring(hit.x, hit.y, hit.z, 0.9);
      this.requestHitstop(0.07);
      this.addShake(0.35);
      this.sfx.squish();
      if (hit.died) {
        this.effects.deathPop(hit.x, hit.y, hit.z, GEL_COLOR);
        this.addShake(0.55);
        this.punch = 1;
        this.sfx.pop();
        this.maybeDrop(hit.slime.x, this.world.groundY(hit.slime.x, hit.slime.z), hit.slime.z, 0.6, 0.25);
        if (hit.slime.big) this.addShake(0.7);
      }
    }
  }

  private contactDamage(touchers: { x: number; z: number; r: number }[], dt: number): void {
    this.contactCooldown -= dt;
    if (this.phase !== 'playing' || this.dead || this.hero.invulnT > 0 || this.contactCooldown > 0) return;
    for (const t of touchers) {
      const dx = this.hero.x - t.x;
      const dz = this.hero.z - t.z;
      const d = Math.hypot(dx, dz);
      if (d < t.r + this.hero.radius * 0.9) {
        const nx = d > 1e-4 ? dx / d : 0;
        const nz = d > 1e-4 ? dz / d : 1;
        this.hearts = Math.max(0, this.hearts - 0.5);
        this.hud.setHearts(this.hearts);
        this.hero.hurt(nx, nz);
        this.contactCooldown = 0.4;
        this.sfx.hurt();
        this.addShake(0.6);
        this.requestHitstop(0.05);
        const flash = document.getElementById('flash')!;
        flash.classList.add('show');
        setTimeout(() => flash.classList.remove('show'), 120);
        if (this.hearts <= 0) {
          this.dead = true;
          this.deadT = 1.1;
          document.getElementById('fade')!.classList.add('show');
        }
        break;
      }
    }
  }

  private updateCamera(dt: number): void {
    this.shake *= Math.exp(-6.5 * dt);
    this.punch *= Math.exp(-8 * dt);

    if (this.phase === 'title') {
      this.titleT += dt;
      const a = this.titleT * 0.1;
      const hx = this.hero.x;
      const hz = this.hero.z;
      this.camTarget.set(hx, 0.8, hz);
      const px = hx + Math.sin(a) * 10;
      const pz = hz + Math.cos(a) * 10;
      this.camera.position.lerp(new THREE.Vector3(px, 7.5, pz), Math.min(1, dt * 2));
      this.camera.lookAt(this.camTarget);
    } else {
      const hx = this.hero.x;
      const hz = this.hero.z;
      const look = new THREE.Vector3(hx + this.hero.vx * 0.12, this.hero.root.position.y, hz + this.hero.vz * 0.12);
      this.camTarget.lerp(look, Math.min(1, dt * 6));
      const s = this.camScale * (1 - this.punch * 0.04);
      const want = new THREE.Vector3(
        this.camTarget.x,
        this.camTarget.y + 10.0 * s,
        this.camTarget.z + 7.9 * s,
      );
      this.camera.position.lerp(want, Math.min(1, dt * 7));
      if (this.shake > 0.002) {
        this.camera.position.x += (Math.random() - 0.5) * this.shake * 0.45;
        this.camera.position.y += (Math.random() - 0.5) * this.shake * 0.3;
        this.camera.position.z += (Math.random() - 0.5) * this.shake * 0.45;
      }
      this.camera.lookAt(this.camTarget);
      this.camera.fov = this.baseFov - this.punch * 3.5;
      this.camera.updateProjectionMatrix();

      // sun follows so shadows stay crisp around the action
      this.sun.position.set(hx + 9, 12, hz + 7);
      this.sun.target.position.set(hx, 0, hz);
    }
  }
}
