/**
 * TELEGRAPH — the readable-chaos pillar (GDD §2.3, §5.4).
 *
 * Every contraption that applies force or dumps paint MUST wind up visibly for
 * ~0.5s before it fires. This helper plays that windup on a mesh/group — an
 * accelerating flash + shake + scale pulse that is unmissable — then resolves
 * (and calls `onFire`) at the moment of firing. A part that fires without first
 * awaiting a telegraph is a bug, not a shortcut.
 *
 * The animation is ticked once per render frame via a single `world.onFrame`
 * hook installed lazily per world (so importing this module has no side effects
 * and parts never wire their own tickers).
 */
import * as THREE from 'three'
import type { World } from '../contracts'

export interface TelegraphOptions {
  /** Windup length in seconds before firing. Pillar default is ~0.5s. */
  duration?: number
  /** Peak positional jitter amplitude (world units). */
  shake?: number
  /** Peak extra uniform scale (1 -> 1 + scalePulse at the crescendo). */
  scalePulse?: number
  /** Emissive flash colour — the "danger" tint. */
  flashColor?: THREE.ColorRepresentation
  /** Called exactly once, at the instant the windup completes (firing). */
  onFire?: () => void
}

const DEFAULTS: Required<Omit<TelegraphOptions, 'onFire'>> = {
  duration: 0.5,
  shake: 0.08,
  scalePulse: 0.22,
  flashColor: '#FFEB3B',
}

interface EmissiveCache {
  mat: THREE.MeshStandardMaterial
  color: THREE.Color
  intensity: number
}

interface ActiveTelegraph {
  root: THREE.Object3D
  basePos: THREE.Vector3
  baseScale: THREE.Vector3
  emissives: EmissiveCache[]
  flash: THREE.Color
  duration: number
  shake: number
  scalePulse: number
  t: number
  onFire?: () => void
  resolve: () => void
}

const perWorld = new WeakMap<World, ActiveTelegraph[]>()

function collectEmissives(root: THREE.Object3D): EmissiveCache[] {
  const out: EmissiveCache[] = []
  root.traverse((o) => {
    const mesh = o as THREE.Mesh
    const mat = mesh.material as THREE.Material | THREE.Material[] | undefined
    const mats = Array.isArray(mat) ? mat : mat ? [mat] : []
    for (const m of mats) {
      if ((m as THREE.MeshStandardMaterial).isMeshStandardMaterial) {
        const sm = m as THREE.MeshStandardMaterial
        out.push({ mat: sm, color: sm.emissive.clone(), intensity: sm.emissiveIntensity })
      }
    }
  })
  return out
}

function tick(list: ActiveTelegraph[], dt: number) {
  for (let i = list.length - 1; i >= 0; i--) {
    const tg = list[i]
    tg.t += dt
    const p = Math.min(tg.t / tg.duration, 1)

    // Intensity ramps up (p^1.6) so the windup visibly *builds* toward firing.
    const ramp = Math.pow(p, 1.6)
    const wobble = 0.5 + 0.5 * Math.sin(tg.t * 34) // fast panic flicker

    // shake around the base position
    const amp = tg.shake * ramp
    tg.root.position.set(
      tg.basePos.x + (Math.random() * 2 - 1) * amp,
      tg.basePos.y + (Math.random() * 2 - 1) * amp,
      tg.basePos.z + (Math.random() * 2 - 1) * amp,
    )
    // scale pulse
    const s = 1 + tg.scalePulse * ramp * (0.55 + 0.45 * wobble)
    tg.root.scale.set(tg.baseScale.x * s, tg.baseScale.y * s, tg.baseScale.z * s)
    // emissive flash
    const glow = ramp * (0.4 + 1.8 * wobble)
    for (const e of tg.emissives) {
      e.mat.emissive.copy(tg.flash)
      e.mat.emissiveIntensity = glow
    }

    if (p >= 1) {
      // restore the untouched look, then fire
      tg.root.position.copy(tg.basePos)
      tg.root.scale.copy(tg.baseScale)
      for (const e of tg.emissives) {
        e.mat.emissive.copy(e.color)
        e.mat.emissiveIntensity = e.intensity
      }
      list.splice(i, 1)
      tg.onFire?.()
      tg.resolve()
    }
  }
}

/**
 * Play a windup on `target` and resolve when it fires. Awaitable so a part can
 * do `await telegraph(world, mesh, {onFire})` and only apply force afterwards.
 */
export function telegraph(
  world: World,
  target: THREE.Object3D,
  opts: TelegraphOptions = {},
): Promise<void> {
  let list = perWorld.get(world)
  if (!list) {
    list = []
    perWorld.set(world, list)
    // Fixed-step, not onFrame: the windup GATES gameplay (forces, paint dumps),
    // so it must advance with the sim — onFrame stalls in hidden tabs, which
    // would freeze every contraption mid-telegraph (found at integration).
    world.addSystem({ update: (dt) => tick(list!, dt) })
  }
  const o = { ...DEFAULTS, ...opts }
  return new Promise<void>((resolve) => {
    list!.push({
      root: target,
      basePos: target.position.clone(),
      baseScale: target.scale.clone(),
      emissives: collectEmissives(target),
      flash: new THREE.Color(o.flashColor),
      duration: o.duration,
      shake: o.shake,
      scalePulse: o.scalePulse,
      t: 0,
      onFire: opts.onFire,
      resolve,
    })
  })
}

/** True while `target` is mid-windup — parts use it to avoid re-triggering. */
export function isTelegraphing(world: World, target: THREE.Object3D): boolean {
  const list = perWorld.get(world)
  return !!list && list.some((tg) => tg.root === target)
}
