/**
 * PaintPuddle (Lane G) — a flat coloured strip laid ON the track. Its job is to
 * make paint ROUTE-guaranteed instead of aim-guaranteed: while the blob's ground
 * contact is inside the strip, we stamp its UNDERSIDE on a steady cadence, so
 * simply rolling through a puddle coats your belly. Ballistic cannons (Lane B)
 * become bonus pressure on top; a hold-forward racer can no longer finish clean.
 *
 * A puddle is purely a paint source: a decorative mesh (NO collider — you roll
 * straight over it) plus a footprint trigger. Each stamp goes through the SAME
 * `skin.splatAtPoint` the cannons use and emits the frozen `paint:splatted`
 * {color, target:'blob'} event (audio/VFX/netcode hook it downstream).
 *
 * Belly-coating detail: the blob's physics body doesn't tumble (rotations are
 * locked — the comedy roll is visual only), so naively stamping straight down
 * would pile every stamp onto the south pole. Instead we fake the roll: an
 * accumulated roll phase (distance / radius) walks the contact point along the
 * travel great circle, so crossing a strip paints a proper stripe around the
 * body and coverage actually climbs.
 */
import * as THREE from 'three'
import type { Blob, PaintColor, World } from '../contracts'
import { PALETTE } from '../contracts'
import type { PaintSkin } from './skin'
import { assets } from '../assets/registry'

export interface PaintPuddleConfig {
  /** Strip centre in world space; y is the track surface the strip lies on. */
  position: [number, number, number]
  /** Footprint [width(x), length(z)] in world units. */
  size: [number, number]
  color: PaintColor
  /** Underside stamps per second while rolling through. Default 6. */
  rate?: number
  /** World-space splat radius per stamp. Default 0.3. */
  splatRadius?: number
}

/** Stamps per world-unit TRAVELLED (distance-based; speed-independent coverage). */
const DEFAULT_RATE = 4
const DEFAULT_SPLAT_RADIUS = 0.3
/** Idle drip: parking in paint still coats, slowly (world-units-equivalent/s). */
const IDLE_DRIP = 0.35
/** How close the belly must be to the strip plane to count as "rolling on it". */
const CONTACT_TOLERANCE = 1.0 // integration tune: landing-bounce hops starved strips at 0.6

export class PaintPuddle {
  readonly color: PaintColor
  readonly mesh: THREE.Mesh
  /** Total underside stamps this puddle has laid (demo/telemetry). */
  stamps = 0

  private readonly px: number
  private readonly pz: number
  private readonly py: number
  private readonly hx: number
  private readonly hz: number
  private readonly period: number
  private readonly splatRadius: number
  private acc = 0

  constructor(world: World, cfg: PaintPuddleConfig) {
    this.color = cfg.color
    const [x, y, z] = cfg.position
    const [w, l] = cfg.size
    this.px = x
    this.py = y
    this.pz = z
    this.hx = w * 0.5
    this.hz = l * 0.5
    this.period = 1 / (cfg.rate ?? DEFAULT_RATE)
    this.splatRadius = cfg.splatRadius ?? DEFAULT_SPLAT_RADIUS

    // Decorative wet-paint slab — thin, flat, no collider (you roll over it).
    const mat = new THREE.MeshStandardMaterial({
      color: PALETTE[cfg.color],
      roughness: 0.25,
      metalness: 0.0,
      transparent: true,
      opacity: 0.9,
      emissive: PALETTE[cfg.color],
      emissiveIntensity: 0.12,
    })
    this.mesh = new THREE.Mesh(new THREE.BoxGeometry(w, 0.05, l), mat)
    this.mesh.position.set(x, y + 0.03, z)
    this.mesh.receiveShadow = true
    world.scene.add(this.mesh)

    // Slot `fx.puddle`: a 1x1 decal model is stretched to this strip's footprint.
    // Visual only — the trigger is pure maths on the config numbers below.
    assets().attachSlot('fx.puddle', this.mesh, {
      fit: new THREE.Vector3(w, 1, l),
      onSwap: (asset) => {
        mat.visible = false
        asset.traverse((o) => {
          const m = (o as THREE.Mesh).material as THREE.MeshStandardMaterial | undefined
          if (m && m.isMeshStandardMaterial) {
            m.color.set(PALETTE[cfg.color])
            m.emissive.set(PALETTE[cfg.color])
          }
        })
      },
    })
  }

  /** True while the blob's belly is resting inside this strip's footprint. */
  private covers(cx: number, cz: number, bellyY: number): boolean {
    return (
      cx >= this.px - this.hx && cx <= this.px + this.hx &&
      cz >= this.pz - this.hz && cz <= this.pz + this.hz &&
      Math.abs(bellyY - this.py) <= CONTACT_TOLERANCE
    )
  }

  /**
   * Advance one fixed step. `stampPoint(out, phase)` writes the world contact
   * point for the current roll phase (shared across all puddles so the fake
   * roll is continuous). Returns the number of stamps laid this step.
   */
  step(
    dt: number,
    moved: number,
    cx: number,
    cz: number,
    bellyY: number,
    phase: number,
    stampPoint: (out: THREE.Vector3, phase: number) => void,
    skin: PaintSkin,
    events: World['events'],
    scratch: THREE.Vector3,
  ): number {
    if (!this.covers(cx, cz, bellyY)) {
      this.acc = 0
      return 0
    }
    // Distance-based accumulation (integration fix, 2026-07-18): a time-based
    // cadence made coverage inversely proportional to speed — a racer at 9 u/s
    // crossed a 7u strip with ~5 stamps while a slow test ball got dozens.
    // Stamp per unit TRAVELLED, plus a small idle drip so parking still coats.
    this.acc += moved + IDLE_DRIP * dt
    let laid = 0
    while (this.acc >= this.period) {
      this.acc -= this.period
      stampPoint(scratch, phase)
      skin.splatAtPoint(scratch, this.color, this.splatRadius)
      events.emit('paint:splatted', { color: this.color, target: 'blob' })
      this.stamps++
      laid++
    }
    return laid
  }
}

export interface PuddlesHandle {
  puddles: PaintPuddle[]
  /** Total stamps laid across all puddles (demo/telemetry). */
  totalStamps(): number
}

/**
 * Build the puddles from `configs` and register the single fixed-step system
 * that coats the blob's belly while it rolls through any of them. Integration
 * (and zones.ts) call this; it owns no course geometry beyond the strips.
 */
export function installPuddles(
  world: World,
  blob: Blob,
  skin: PaintSkin,
  configs: PaintPuddleConfig[],
): PuddlesHandle {
  const puddles = configs.map((c) => new PaintPuddle(world, c))

  // Continuous fake-roll state (see class docstring).
  const prev = new THREE.Vector3()
  let hasPrev = false
  let phase = 0
  const dir = new THREE.Vector3(0, 0, -1) // course runs toward -Z; sane default
  const axis = new THREE.Vector3()
  const down = new THREE.Vector3(0, -1, 0)
  const center = new THREE.Vector3()
  const scratch = new THREE.Vector3()
  const rotated = new THREE.Vector3()

  world.addSystem({
    update(dt: number): void {
      const t = blob.body.translation()
      center.set(t.x, t.y, t.z)
      const r = skin.boundingRadius * Math.max(0.01, blob.modifiers.size)
      const bellyY = t.y - r

      // advance the roll phase from horizontal displacement (roll w/o slipping)
      let movedStep = 0
      if (hasPrev) {
        const dx = t.x - prev.x
        const dz = t.z - prev.z
        const moved = Math.hypot(dx, dz)
        if (moved > 1e-5) {
          dir.set(dx / moved, 0, dz / moved)
          phase += moved / r
          movedStep = moved
        }
      }
      prev.set(t.x, t.y, t.z)
      hasPrev = true

      // horizontal axis perpendicular to travel; contact point rides the great
      // circle in the travel-vertical plane as `phase` grows.
      axis.set(-dir.z, 0, dir.x)
      const stampPoint = (out: THREE.Vector3, ph: number) => {
        rotated.copy(down).applyAxisAngle(axis, ph)
        out.copy(center).addScaledVector(rotated, r)
      }

      for (const p of puddles) {
        p.step(dt, movedStep, t.x, t.z, bellyY, phase, stampPoint, skin, world.events, scratch)
      }
    },
  })

  return {
    puddles,
    totalStamps: () => puddles.reduce((s, p) => s + p.stamps, 0),
  }
}
