/**
 * Lane A — blob controller (the fixed-step System).
 *
 * Physics only. Reads keyboard + camera, drives the rigid ball with impulses,
 * respects `blob.modifiers` EVERY frame, and emits the frozen wire events
 * `blob:jumped` / `blob:landed`. It never sets velocity outright (except the
 * jump kick), so external forces — catapults, fans, shoves from Lane C — always
 * stack on top instead of being clobbered.
 *
 * Movement is CAMERA-RELATIVE: W is "away from camera", derived from the live
 * camera yaw, so it stays correct as the follow-cam swings around.
 */
import * as THREE from 'three'
import type { System, World } from '../contracts'
import type { BlobHandle } from './createBlob'

export interface BlobControllerOptions {
  /** Base top speed (m/s) before speedMul. */
  maxSpeed?: number
  /** Ground acceleration (m/s^2) before massMul sluggishness. */
  accel?: number
  /** Take-off vertical speed (m/s) before jumpMul. */
  jumpSpeed?: number
}

export function createBlobController(
  world: World,
  blob: BlobHandle,
  opts: BlobControllerOptions = {},
): System {
  const { physics, rapier, events, camera } = world
  const maxSpeed = opts.maxSpeed ?? 8
  const accel = opts.accel ?? 60
  const jumpSpeed = opts.jumpSpeed ?? 7.6

  // ---- input (self-contained; debug keys live in the demo) ----
  const keys = new Set<string>()
  let jumpBuffer = 0
  addEventListener('keydown', (e) => {
    keys.add(e.code)
    if (e.code === 'Space') jumpBuffer = 0.12 // buffered so an early tap still fires
  })
  addEventListener('keyup', (e) => keys.delete(e.code))
  const held = (...codes: string[]) => codes.some((c) => keys.has(c))

  // ---- gamepad (poll API — read fresh each fixed step) ----
  // Left stick = analog camera-relative move (radial deadzone, magnitude kept
  // so gentle stick = gentle roll), d-pad = digital, A/Cross or B/Circle = jump
  // through the same buffered-edge path as Space.
  const pad = { x: 0, z: 0 }
  let padJumpHeld = false
  function pollGamepad(): void {
    pad.x = 0
    pad.z = 0
    let jump = false
    const pads = typeof navigator !== 'undefined' && navigator.getGamepads
      ? navigator.getGamepads() : null
    if (pads) {
      for (const p of pads) {
        if (!p || !p.connected) continue
        const dz = 0.15
        const ax = p.axes[0] ?? 0
        const ay = p.axes[1] ?? 0
        const mag = Math.hypot(ax, ay)
        if (mag > dz) {
          const scale = Math.min(1, (mag - dz) / (1 - dz)) / mag
          pad.x = ax * scale
          pad.z = ay * scale
        }
        if (p.buttons[14]?.pressed) pad.x = -1 // d-pad left
        if (p.buttons[15]?.pressed) pad.x = 1  // d-pad right
        if (p.buttons[12]?.pressed) pad.z = -1 // d-pad up
        if (p.buttons[13]?.pressed) pad.z = 1  // d-pad down
        jump = !!(p.buttons[0]?.pressed || p.buttons[1]?.pressed)
        break // first connected pad wins
      }
    }
    if (jump && !padJumpHeld) jumpBuffer = 0.12
    padJumpHeld = jump
  }

  // ---- state ----
  let grounded = false
  let coyote = 0 // seconds of "still counts as grounded" after leaving ground
  let prevVy = 0
  let lastSize = -1
  let lastMassMul = -1

  const fwd = new THREE.Vector3()
  const right = new THREE.Vector3()
  const move = new THREE.Vector3()

  function applyModifiers(): void {
    const m = blob.modifiers
    if (m.size !== lastSize) {
      blob.collider.setRadius(blob.radius * m.size)
      blob.group.scale.setScalar(m.size)
      lastSize = m.size
      lastMassMul = -1 // radius change resets mass props → re-apply mass below
    }
    if (m.massMul !== lastMassMul) {
      blob.collider.setMass(Math.max(0.05, m.massMul))
      lastMassMul = m.massMul
    }
    // grip boosts real contact friction (slopes / standing) ...
    blob.collider.setFriction(0.6 + m.grip * 1.6)
  }

  return {
    update(dt: number): void {
      applyModifiers()
      const m = blob.modifiers
      const body = blob.body
      const r = blob.radius * m.size

      // ---- grounded check: short ray straight down, excluding self ----
      const t = body.translation()
      // Sync the visual root to the body in the FIXED step: gameplay reads this
      // transform (paint proximity gating), so it must track the sim even when
      // render frames stall (hidden tab). The feel layer re-sets it per frame
      // with wobble on top — that stays visual-only.
      blob.group.position.set(t.x, t.y, t.z)
      const ray = new rapier.Ray({ x: t.x, y: t.y, z: t.z }, { x: 0, y: -1, z: 0 })
      const hit = physics.castRay(ray, r + 0.18, true, undefined, undefined, blob.collider, body)
      const nowGrounded = hit !== null
      coyote = nowGrounded ? 0.1 : Math.max(0, coyote - dt)

      // landing edge → squash event, impact = downward speed just before contact
      if (nowGrounded && !grounded) {
        const impact = Math.max(0, -prevVy)
        if (impact > 0.8) events.emit('blob:landed', { impact })
      }
      grounded = nowGrounded

      // ---- camera-relative move basis (flatten camera dir onto ground) ----
      camera.getWorldDirection(fwd)
      fwd.y = 0
      if (fwd.lengthSq() < 1e-6) fwd.set(0, 0, -1)
      fwd.normalize()
      right.set(-fwd.z, 0, fwd.x) // forward × up

      pollGamepad()
      move.set(0, 0, 0)
      if (held('KeyW', 'ArrowUp')) move.add(fwd)
      if (held('KeyS', 'ArrowDown')) move.sub(fwd)
      if (held('KeyD', 'ArrowRight')) move.add(right)
      if (held('KeyA', 'ArrowLeft')) move.sub(right)
      if (pad.x !== 0 || pad.z !== 0) {
        move.addScaledVector(fwd, -pad.z).addScaledVector(right, pad.x) // stick up = away from camera
      }
      const hasInput = move.lengthSq() > 1e-6
      if (move.lengthSq() > 1) move.normalize() // keep sub-unit analog magnitudes

      const v = body.linvel()
      const mass = body.mass()
      const speedCap = maxSpeed * m.speedMul
      const control = grounded ? 1 : 0.4 // reduced air control
      const sluggish = 1 / Math.sqrt(Math.max(0.3, m.massMul)) // heavy = accelerates slower

      // ---- drive: additive impulse, but stop pushing past the cap ----
      if (hasInput) {
        const along = v.x * move.x + v.z * move.z
        if (along < speedCap) {
          const imp = mass * accel * control * sluggish * dt
          body.applyImpulse({ x: move.x * imp, y: 0, z: move.z * imp }, true)
        }
        if (grounded) {
          // scrub sideways drift so turns don't feel like ice (grip tightens it)
          const along2 = v.x * move.x + v.z * move.z
          const perpX = v.x - along2 * move.x
          const perpZ = v.z - along2 * move.z
          const k = Math.min(1, (2 + m.grip * 10) * dt)
          body.applyImpulse({ x: -perpX * mass * k, y: 0, z: -perpZ * mass * k }, true)
        }
      } else if (grounded) {
        // no input on ground: brake. grip = how hard we kill the slide.
        const k = Math.min(1, (6 + m.grip * 22) * dt)
        body.applyImpulse({ x: -v.x * mass * k, y: 0, z: -v.z * mass * k }, true)
      }

      // ---- jump (buffered + coyote) ----
      jumpBuffer = Math.max(0, jumpBuffer - dt)
      if (jumpBuffer > 0 && coyote > 0) {
        const vy = body.linvel().y
        const dvy = Math.max(0, jumpSpeed * m.jumpMul - vy) // bring vy up to target
        body.applyImpulse({ x: 0, y: mass * dvy, z: 0 }, true)
        jumpBuffer = 0
        coyote = 0
        grounded = false
        events.emit('blob:jumped', {})
      }

      prevVy = body.linvel().y
    },
  }
}
