import { Framebuffer } from "../engine/render/Framebuffer"
import type { RenderConfig } from "../engine/render/RenderConfig"
import type { Mat4 } from "../engine/math/Mat4"
import type { Camera } from "../engine/scene/Camera"
import { RenderProtocol, type RenderWorkerInit } from "../engine/render/RenderProtocol"
import { RenderScene, type RenderInstance } from "../engine/render/RenderScene"

/** Clouds are drawn at 1/SKY_STEP resolution (the sky base + sun stay per-pixel);
 *  band splits align to it so the cloud block grid stays seamless across workers. */
const SKY_STEP = 2
/** Use worker threads when the page can share memory (else single-thread). */
const ENABLE_WORKERS = true
/** Cap on render worker threads. Deliberately low: a browser game shares the
 *  machine with the browser itself, the compositor, and whatever else is open, so
 *  grabbing every core backfires -- the frame-time *tail* blows up even while the
 *  median improves. Measured (`bun run bench:browser`) on a busy 16-core desktop:
 *  3 workers beat single-thread on both median (~1.5x) and p95 (~1.2x); 6 were far
 *  worse. Raise only if the target is a dedicated/idle machine; retune via the
 *  bench. */
const MAX_WORKERS = 3

/**
 * Render driver. When the page is cross-origin-isolated it runs a pool of worker
 * threads, each owning a disjoint row band of a `SharedArrayBuffer` framebuffer;
 * otherwise it renders inline on the main thread. Same output either way.
 *
 * The worker barrier is lock-free: workers `Atomics.wait` on a frame counter, so
 * there are **no per-frame messages** (the old postMessage barrier was the jitter
 * source). Per-frame inputs (camera, matrix, visible list) live in shared arrays.
 * `dispatch` starts a frame without blocking; the caller polls `done()` and
 * presents when ready, so pacing stays on the caller's `requestAnimationFrame`.
 */
export type Renderer = {
  readonly fb: Framebuffer
  readonly parallel: boolean
  reconfigure: (config: RenderConfig) => void
  /** Start rendering one frame (non-blocking in the worker path). */
  dispatch: (
    camera: Camera,
    viewProj: Mat4,
    visible: number[],
    time: number,
    instances: RenderInstance[],
  ) => void
  /** Has the dispatched frame finished? (always true single-threaded.) */
  done: () => boolean
  /** Critical-path render time of the last frame, ms (max band time / inline time). */
  workMs: () => number
}

type PendingFrame = {
  camera: Camera
  viewProjection: Mat4
  visibleChunks: number[]
  instances: RenderInstance[]
  time: number
}

export function createRenderer(
  scene: RenderScene,
  initial: RenderConfig,
  forceWorkers?: boolean,
): Renderer {
  const hw =
    (globalThis.navigator as Navigator | undefined)?.hardwareConcurrency ?? 4
  const workerCount = Math.max(1, Math.min(MAX_WORKERS, hw - 1))
  const maxVis = Math.max(1, scene.chunks.length)
  const maxInstances = Math.max(1, scene.maxInstances)
  let config = initial
  const want = forceWorkers ?? ENABLE_WORKERS
  let parallel = want && canShare()

  let fb = Framebuffer.create(1, 1)
  let workers: Worker[] = []
  let ctrl: Int32Array<ArrayBufferLike> = new Int32Array(0)
  let cam: Float64Array<ArrayBufferLike> = new Float64Array(0) // pos x/y/z, yaw, pitch, fov, time
  let vp: Float32Array<ArrayBufferLike> = new Float32Array(0) // the view-projection matrix
  let vis: Int32Array<ArrayBufferLike> = new Int32Array(0) // visible chunk indices
  let instanceIds: Int32Array<ArrayBufferLike> = new Int32Array(0)
  let instanceTransforms: Float32Array<ArrayBufferLike> = new Float32Array(0)
  let times: Float64Array<ArrayBufferLike> = new Float64Array(0) // per-worker band render ms
  let lastWork = 0
  let generation = 0
  let pending: PendingFrame | null = null

  function stopWorkers(): void {
    for (const worker of workers) {
      worker.terminate()
    }
    workers = []
  }

  function renderInline(frame: PendingFrame): void {
    const t0 = performance.now()
    RenderScene.renderBand(
      fb,
      scene,
      frame.camera,
      frame.viewProjection,
      frame.visibleChunks,
      frame.instances,
      config,
      SKY_STEP,
      frame.time,
      0,
      fb.height,
    )
    lastWork = performance.now() - t0
  }

  function disableParallel(currentGeneration: number): void {
    if (!parallel || currentGeneration !== generation) {
      return
    }
    const frame = pending
    parallel = false
    stopWorkers()
    if (frame !== null) {
      renderInline(frame)
      pending = null
    }
  }

  function setup(): void {
    generation++
    const currentGeneration = generation
    pending = null
    stopWorkers()
    const width = config.internalWidth
    const height = config.internalHeight
    if (parallel) {
      const n = width * height
      fb = {
        width,
        height,
        color: new Uint32Array(new SharedArrayBuffer(n * 4)),
        depth: new Float32Array(new SharedArrayBuffer(n * 4)),
      }
      const bands = splitBands(height, workerCount, SKY_STEP)
      ctrl = new Int32Array(
        new SharedArrayBuffer(RenderProtocol.CONTROL_LENGTH * 4),
      )
      cam = new Float64Array(
        new SharedArrayBuffer(RenderProtocol.CAMERA_LENGTH * 8),
      )
      vp = new Float32Array(
        new SharedArrayBuffer(RenderProtocol.VIEW_PROJECTION_LENGTH * 4),
      )
      vis = new Int32Array(new SharedArrayBuffer(maxVis * 4))
      instanceIds = new Int32Array(new SharedArrayBuffer(maxInstances * 4))
      instanceTransforms = new Float32Array(
        new SharedArrayBuffer(
          maxInstances * RenderProtocol.TRANSFORM_FLOATS * 4,
        ),
      )
      times = new Float64Array(new SharedArrayBuffer(bands.length * 8))
      try {
        bands.forEach((band, index) => {
          const worker = new Worker(
            new URL("./render-worker.ts", import.meta.url),
            { type: "module" },
          )
          worker.addEventListener("error", () => disableParallel(currentGeneration))
          const init: RenderWorkerInit = {
            colorSAB: shared(fb.color.buffer),
            depthSAB: shared(fb.depth.buffer),
            width,
            height,
            scene,
            band,
            config,
            skyStep: SKY_STEP,
            ctrlSAB: shared(ctrl.buffer),
            cameraSAB: shared(cam.buffer),
            viewProjectionSAB: shared(vp.buffer),
            visibleChunkSAB: shared(vis.buffer),
            instanceIdSAB: shared(instanceIds.buffer),
            instanceTransformSAB: shared(instanceTransforms.buffer),
            timesSAB: shared(times.buffer),
            workerIndex: index,
          }
          worker.postMessage(init)
          workers.push(worker)
        })
        Atomics.store(ctrl, RenderProtocol.DONE, workers.length)
      } catch {
        parallel = false
        stopWorkers()
      }
    }
    if (!parallel || workers.length === 0) {
      fb = Framebuffer.create(width, height)
    }
  }
  setup()

  return {
    get fb() {
      return fb
    },
    get parallel() {
      return parallel && workers.length > 0
    },
    reconfigure(next) {
      config = next
      setup()
    },
    dispatch(camera, viewProj, visible, time, instances) {
      pending = {
        camera,
        viewProjection: viewProj,
        visibleChunks: visible,
        instances,
        time,
      }
      if (parallel && workers.length > 0) {
        RenderProtocol.writeCamera(cam, camera, time)
        RenderProtocol.writeViewProjection(vp, viewProj)
        const count = Math.min(visible.length, vis.length)
        for (let i = 0; i < count; i++) {
          vis[i] = visible[i]
        }
        const instanceCount = RenderProtocol.writeInstances(
          instanceIds,
          instanceTransforms,
          instances,
        )
        Atomics.store(ctrl, RenderProtocol.VISIBLE_CHUNKS, count)
        Atomics.store(ctrl, RenderProtocol.VISIBLE_INSTANCES, instanceCount)
        Atomics.store(ctrl, RenderProtocol.DONE, 0)
        Atomics.add(ctrl, RenderProtocol.FRAME, 1)
        Atomics.notify(ctrl, RenderProtocol.FRAME, workers.length)
        return
      }
      renderInline(pending)
      pending = null
    },
    done() {
      const complete =
        !(parallel && workers.length > 0) ||
        Atomics.load(ctrl, RenderProtocol.DONE) >= workers.length
      if (complete) {
        pending = null
      }
      return complete
    },
    workMs() {
      if (parallel && workers.length > 0) {
        let m = 0
        for (let i = 0; i < workers.length; i++) {
          if (times[i] > m) {
            m = times[i]
          }
        }
        return m
      }
      return lastWork
    },
  }
}

/** Shared memory needs SharedArrayBuffer + a cross-origin-isolated page (the
 *  COOP/COEP headers Vite serves). Without it, render on the main thread. */
function canShare(): boolean {
  return (
    typeof SharedArrayBuffer !== "undefined" &&
    typeof Worker !== "undefined" &&
    (globalThis as { crossOriginIsolated?: boolean }).crossOriginIsolated ===
      true
  )
}

function shared(buffer: ArrayBufferLike): SharedArrayBuffer {
  if (!(buffer instanceof SharedArrayBuffer)) {
    throw new Error("render worker buffer is not shared")
  }
  return buffer
}

/** Split `height` rows into ~`count` bands. Interior boundaries snap up to a
 *  multiple of `step` so the sky's block grid stays aligned (no seam), while the
 *  bands stay disjoint so no two workers write the same pixel. */
function splitBands(
  height: number,
  count: number,
  step: number,
): [number, number][] {
  const bands: [number, number][] = []
  const per = Math.ceil(height / count)
  let y = 0
  while (y < height) {
    const raw = y + per
    const y1 =
      raw >= height ? height : Math.min(height, Math.ceil(raw / step) * step)
    bands.push([y, y1])
    y = y1
  }
  return bands
}
