/**
 * Lane A — the FEEL layer. This is BLOBBO's comedy engine.
 *
 * The physics body is a rigid ball. Everything soft and wobbly happens here, on
 * the *visual* mesh only, once per rendered frame. We run underdamped springs so
 * the jiggle OVERSHOOTS — a landing squash should read from across the room, a
 * jump should pop tall, and the body should keep quivering for a beat after.
 *
 *   squash  : signed vertical squash/stretch (+ = tall, - = flat), spring→0
 *   leanX/Z : inertial jelly lean, driven by horizontal acceleration
 *   idle    : always-on breathing so a resting blob is never dead-still
 *
 * Wired to the frozen events by the demo/integration:
 *   world.events.on('blob:jumped', () => feel.onJump())
 *   world.events.on('blob:landed', (p) => feel.onLand(p.impact))
 */
import * as THREE from 'three'
import type { BlobHandle } from './createBlob'

export interface BlobFeel {
  update(dt: number): void
  onJump(): void
  onLand(impact: number): void
}

// Underdamped so it overshoots and quivers. Higher K = snappier, lower C = wobblier.
const SQUASH_K = 190
const SQUASH_C = 11
const LEAN_K = 150
const LEAN_C = 13

function spring(pos: number, vel: number, k: number, c: number, dt: number): [number, number] {
  // semi-implicit Euler — stable at 60fps for these constants.
  const acc = -k * pos - c * vel
  const nv = vel + acc * dt
  return [pos + nv * dt, nv]
}

export function createBlobFeel(blob: BlobHandle): BlobFeel {
  let squash = 0
  let squashVel = 0
  let leanX = 0
  let leanXVel = 0
  let leanZ = 0
  let leanZVel = 0
  let t = 0

  let yaw = 0
  const prevVel = new THREE.Vector3()
  const mat = blob.mesh.material as THREE.MeshStandardMaterial

  return {
    onJump() {
      // Pop tall on the way up.
      squashVel += 8
    },
    onLand(impact: number) {
      // Squash flat proportional to how hard we hit. Overshoots, then rebounds.
      const k = THREE.MathUtils.clamp(impact / 9, 0, 1)
      squashVel -= 5 + 9 * k
    },

    update(dt: number) {
      // Clamp dt so a stall / tab-switch can't explode the springs.
      const d = Math.min(dt, 1 / 30)
      t += d

      const bt = blob.body.translation()
      const bv = blob.body.linvel()

      // ---- position sync (visual follows the rigid ball) ----
      blob.group.position.set(bt.x, bt.y, bt.z)

      // ---- facing: turn to face horizontal travel, keep last heading when idle ----
      const horizSpeed = Math.hypot(bv.x, bv.z)
      if (horizSpeed > 1.2) {
        const target = Math.atan2(bv.x, bv.z)
        // shortest-arc ease
        let delta = target - yaw
        delta = Math.atan2(Math.sin(delta), Math.cos(delta))
        yaw += delta * (1 - Math.exp(-d * 9))
      }
      blob.group.rotation.y = yaw

      // ---- inertial lean, computed in the blob's local frame ----
      const ax = (bv.x - prevVel.x) / Math.max(d, 1e-4)
      const az = (bv.z - prevVel.z) / Math.max(d, 1e-4)
      prevVel.set(bv.x, bv.y, bv.z)
      // world -> local (undo yaw)
      const cy = Math.cos(yaw)
      const sy = Math.sin(yaw)
      const localAx = ax * cy - az * sy
      const localAz = ax * sy + az * cy
      const leanTargetX = THREE.MathUtils.clamp(-localAz * 0.012, -0.22, 0.22) // nod on accel/brake
      const leanTargetZ = THREE.MathUtils.clamp(localAx * 0.012, -0.22, 0.22)   // bank on strafe/turn
      ;[leanX, leanXVel] = spring(leanX - leanTargetX, leanXVel, LEAN_K, LEAN_C, d)
      leanX += leanTargetX
      ;[leanZ, leanZVel] = spring(leanZ - leanTargetZ, leanZVel, LEAN_K, LEAN_C, d)
      leanZ += leanTargetZ

      // ---- squash/stretch spring ----
      ;[squash, squashVel] = spring(squash, squashVel, SQUASH_K, SQUASH_C, d)

      // ---- procedural life: idle breathing + a gallop bob while running ----
      const idle = Math.sin(t * 2.4) * 0.02
      const speedNorm = THREE.MathUtils.clamp(horizSpeed / 8, 0, 1)
      const bob = Math.sin(t * 15) * 0.05 * speedNorm

      const s = squash + idle + bob
      let syScale = 1 + s
      let sxzScale = 1 - 0.55 * s // rough volume preservation: tall => thin
      syScale = THREE.MathUtils.clamp(syScale, 0.3, 1.95)
      sxzScale = THREE.MathUtils.clamp(sxzScale, 0.55, 1.7)

      // Pivot the squash at the blob's base so a landing plants on the floor
      // instead of shrinking toward its own center. (group scale handles `size`.)
      blob.mesh.scale.set(sxzScale, syScale, sxzScale)
      blob.mesh.position.y = blob.radius * (syScale - 1)
      blob.mesh.rotation.set(leanX, 0, leanZ)

      // Face rides the squash a touch (googly life) and never gets painted.
      blob.face.position.y = blob.radius * (syScale - 1) * 0.7

      // ---- super-state glow (modifiers.glow) ----
      const glow = blob.modifiers.glow
      mat.emissiveIntensity = glow > 0 ? glow * (0.6 + 0.4 * Math.sin(t * 8)) : 0
    },
  }
}
