import * as THREE from 'three'
import RAPIER from '@dimforge/rapier3d-compat'
import type { Events, System, World } from './contracts'

const FIXED_DT = 1 / 60

function makeEvents(): Events {
  const map = new Map<string, Set<(p: any) => void>>()
  return {
    on(name, fn) {
      if (!map.has(name)) map.set(name, new Set())
      map.get(name)!.add(fn)
      return () => map.get(name)!.delete(fn)
    },
    emit(name, payload) {
      map.get(name)?.forEach((fn) => fn(payload))
    },
  }
}

export async function createWorld(container: HTMLElement): Promise<World> {
  await RAPIER.init()

  const scene = new THREE.Scene()
  scene.background = new THREE.Color('#bfe3ff')
  scene.fog = new THREE.Fog('#bfe3ff', 60, 160)

  const camera = new THREE.PerspectiveCamera(
    60, container.clientWidth / container.clientHeight, 0.1, 400)
  camera.position.set(0, 6, 12)

  const renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(container.clientWidth, container.clientHeight)
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
  renderer.shadowMap.enabled = true
  container.appendChild(renderer.domElement)

  const sun = new THREE.DirectionalLight('#fffbe8', 2.2)
  sun.position.set(18, 30, 10)
  sun.castShadow = true
  sun.shadow.mapSize.set(2048, 2048)
  sun.shadow.camera.left = sun.shadow.camera.bottom = -50
  sun.shadow.camera.right = sun.shadow.camera.top = 50
  scene.add(sun, new THREE.AmbientLight('#cfe8ff', 0.9))

  const physics = new RAPIER.World({ x: 0, y: -14, z: 0 }) // gamey gravity

  const systems: System[] = []
  const frameHooks: Array<(dt: number) => void> = []
  const events = makeEvents()

  const step = () => {
    physics.step()
    for (const s of systems) s.update(FIXED_DT)
  }

  const world: World = {
    scene, camera, renderer, physics, rapier: RAPIER, events,
    addSystem: (s) => void systems.push(s),
    onFrame: (fn) => void frameHooks.push(fn),
    tick(steps = 1) {
      for (let i = 0; i < steps; i++) step()
    },
    renderOnce() {
      for (const fn of frameHooks) fn(FIXED_DT)
      renderer.render(scene, camera)
    },
    start() {
      let last = performance.now()
      let acc = 0
      const advance = (now: number) => {
        const dt = Math.min((now - last) / 1000, 0.1)
        last = now
        acc += dt
        while (acc >= FIXED_DT) {
          step()
          acc -= FIXED_DT
        }
        return dt
      }
      const loop = (now: number) => {
        requestAnimationFrame(loop)
        const dt = advance(now)
        for (const fn of frameHooks) fn(dt)
        renderer.render(scene, camera)
      }
      requestAnimationFrame(loop)
      // Hidden tabs suspend rAF entirely — keep the sim alive on a coarse
      // interval so the game (and headless verification) survives backgrounding.
      setInterval(() => {
        if (performance.now() - last > 500) advance(performance.now())
      }, 250)
    },
  }

  addEventListener('resize', () => {
    camera.aspect = container.clientWidth / container.clientHeight
    camera.updateProjectionMatrix()
    renderer.setSize(container.clientWidth, container.clientHeight)
  })

  return world
}
