import { Framebuffer } from "../engine/render/Framebuffer"
import { Frustum } from "../engine/render/Frustum"
import { Rasterizer } from "../engine/render/Rasterizer"
import type { RenderConfig } from "../engine/render/RenderConfig"
import { Sky, type SkyConfig } from "../engine/render/Sky"
import type { Camera } from "../engine/scene/Camera"
import { Mat4 } from "../engine/math/Mat4"
import type { Mesh } from "../engine/scene/Mesh"
import { Mob, type MobKind } from "./actors/Mob"
import { Sprite } from "../engine/scene/Sprite"
import type { Vec2 } from "../engine/math/Vec2"
import type { Vec3 } from "../engine/math/Vec3"
import type { Textures } from "./textures"
import type { Chunk } from "./level"

/** Everything needed to render the world: the room, the cullable chunks, the NPC
 *  billboard source, sky, and textures. Bundled so it can be handed to a worker
 *  whole (it is plain data + typed arrays, structured-clone friendly). */
export type Scene = {
  chunks: Chunk[]
  floor: Mesh
  walls: Mesh
  crate: Mesh
  npc: { position: Vec3; size: Vec2 }
  /** Canonical local-space mob meshes, one per kind, built once + shared by every
   *  instance (each instance differs only by its per-frame model matrix). */
  mobMesh: Record<MobKind, Mesh>
  /** How many mobs the sim has -- sizes the worker's shared transform buffer. */
  mobCount: number
  sky: SkyConfig
  textures: Textures
}

/** One mob's live transform for a frame: which mesh + where/how to place it.
 *  Produced by `visibleMobs` on the main thread, then either passed straight to
 *  `renderBand` (single-thread) or packed into the shared `mobState` buffer and
 *  rebuilt in each worker. `MOB_FLOATS` is that packed layout's stride. */
export type MobDraw = { kind: MobKind; x: number; y: number; z: number; heading: number; scale: number }
export const MOB_FLOATS = 6 // kind index (into MOB_KINDS), x, y, z, heading, scale

/** Chunk indices whose bounding box is inside the view frustum. Computed once on
 *  the main thread and shared with every worker (so they don't each re-cull). */
export function visibleChunks(chunks: Chunk[], viewProj: Mat4): number[] {
  const frustum = Frustum.fromViewProj(viewProj)
  const out: number[] = []
  for (let i = 0; i < chunks.length; i++) {
    const c = chunks[i]
    if (Frustum.intersectsAabb(frustum, c.minX, c.minY, c.minZ, c.maxX, c.maxY, c.maxZ)) {
      out.push(i)
    }
  }
  return out
}

/** The `MobDraw`s for mobs whose world AABB is inside the view frustum. Mobs move,
 *  so (unlike chunks) they can't be baked into the culled world -- they're culled
 *  here per frame instead. Computed once on the main thread; the visible set is
 *  what gets shipped to the workers. */
export function visibleMobs(mobs: Mob[], viewProj: Mat4): MobDraw[] {
  const frustum = Frustum.fromViewProj(viewProj)
  const out: MobDraw[] = []
  for (const m of mobs) {
    const r = Mob.boundingRadius(m.kind) * m.scale
    const h = Mob.bodyHeight(m.kind) * m.scale
    const p = m.position
    if (Frustum.intersectsAabb(frustum, p.x - r, p.y - r, p.z - r, p.x + r, p.y + h + r, p.z + r)) {
      out.push({ kind: m.kind, x: p.x, y: p.y, z: p.z, heading: m.heading, scale: m.scale })
    }
  }
  return out
}

/**
 * Render rows [y0, y1) of one frame into `fb`. This is the single source of
 * render truth: the single-thread path calls it with the full height, and each
 * worker calls it with its own disjoint band -- same output either way, and no
 * two bands touch the same pixel (so the shared framebuffer needs no locking).
 */
export function renderBand(
  fb: Framebuffer,
  scene: Scene,
  camera: Camera,
  viewProj: Mat4,
  visible: number[],
  mobDraws: MobDraw[],
  config: RenderConfig,
  skyStep: number,
  time: number,
  y0: number,
  y1: number,
): void {
  const tx = scene.textures
  Sky.render(fb, camera, scene.sky, time, skyStep, y0, y1)
  // Room: small and always near, drawn unconditionally (double-sided).
  Rasterizer.draw(fb, scene.floor, tx.floor, viewProj, config, false, y0, y1)
  Rasterizer.draw(fb, scene.walls, tx.wall, viewProj, config, false, y0, y1)
  Rasterizer.draw(fb, scene.crate, tx.crate, viewProj, config, false, y0, y1)
  for (const i of visible) {
    const c = scene.chunks[i]
    // Past lodDistance, draw the cheap impostor group set instead of full detail.
    // `chunkFar` is pure (camera + chunk bounds + config), so every worker band
    // makes the identical choice -- no full/impostor seam across bands. The loop
    // is content-agnostic: each group carries its own mesh + material.
    const groups = chunkFar(c, camera.position, config.lodDistance) ? c.far : c.near
    for (const g of groups) {
      Rasterizer.draw(fb, g.mesh, g.material.texture, viewProj, config, g.material.cull, y0, y1)
    }
  }
  const sprite: Sprite = { position: scene.npc.position, size: scene.npc.size, texture: tx.npc }
  Rasterizer.draw(fb, Sprite.billboard(sprite, camera), tx.npc, viewProj, config, false, y0, y1)
  // Roaming mobs: each is the shared local-space mesh for its kind, placed by its
  // own model matrix (viewProj x model). Drawn double-sided (cull off) -- they're
  // small and few, so the winding-correct backface cull isn't worth the fuss.
  for (const m of mobDraws) {
    const mvp = Mat4.multiply(viewProj, Mat4.compose(m.x, m.y, m.z, m.heading, m.scale))
    Rasterizer.draw(fb, scene.mobMesh[m.kind], tx[m.kind], mvp, config, false, y0, y1)
  }
  Framebuffer.quantize(fb, config, y0, y1)
}

/** Whether a chunk is far enough to draw its impostor meshes: squared distance
 *  from the camera to the chunk's AABB vs `lodDistance²`. Pure -- depends only on
 *  camera, the chunk's baked bounds, and the config constant, all of which every
 *  worker already holds, so the choice is identical across bands. */
export function chunkFar(chunk: Chunk, eye: Vec3, lodDistance: number): boolean {
  if (!(lodDistance < Infinity)) {
    return false
  }
  const dx = eye.x - Math.max(chunk.minX, Math.min(chunk.maxX, eye.x))
  const dy = eye.y - Math.max(chunk.minY, Math.min(chunk.maxY, eye.y))
  const dz = eye.z - Math.max(chunk.minZ, Math.min(chunk.maxZ, eye.z))
  return dx * dx + dy * dy + dz * dz > lodDistance * lodDistance
}
