import { Color } from "./Color"
import type { RenderConfig } from "./RenderConfig"

/** CPU color + depth buffer the renderer writes into before it is blitted to a
 *  canvas. Kept as flat typed arrays so it needs no DOM and can also run
 *  headless (server-side rendering, tests, baking). */
export type Framebuffer = {
  readonly width: number
  readonly height: number
  /** Packed RGBA pixels; see Color. Aliased as an ImageData at blit time. */
  readonly color: Uint32Array
  /** Per-pixel depth stored as 1/w. 1/w (unlike w) interpolates linearly in
   *  screen space, so it is both cheap and correct to compare. Larger = nearer;
   *  cleared to 0 = infinitely far. */
  readonly depth: Float32Array
}

export namespace Framebuffer {
  export function create(width: number, height: number): Framebuffer {
    return {
      width,
      height,
      color: new Uint32Array(width * height),
      depth: new Float32Array(width * height),
    }
  }

  /** Reset every pixel to `color` and depth to 0 (farthest). Call once a frame
   *  before drawing; `color` should match the fog color so uncovered pixels
   *  (gaps past the geometry) blend seamlessly. */
  export function clear(fb: Framebuffer, color: Color): void {
    fb.color.fill(color)
    fb.depth.fill(0)
  }

  /**
   * Posterize the color buffer to `config.colorDepth` bits per channel with a
   * Bayer 4x4 ordered dither, in place. This is a post-process over the whole
   * frame (run after all geometry), reproducing the PS1's banded-yet-dithered
   * 15-bit output. Skipped entirely when it would be a no-op (full depth, no
   * dither).
   */
  export function quantize(fb: Framebuffer, config: RenderConfig, y0 = 0, y1 = fb.height): void {
    const levels = (1 << config.colorDepth) - 1
    if (levels >= 255 && config.dither === 0) {
      return
    }
    const { width, color } = fb
    for (let y = y0; y < y1; y++) {
      for (let x = 0; x < width; x++) {
        // Per-pixel threshold from the tiled Bayer matrix, centered on 0 and
        // scaled by strength, nudges each channel before it snaps to a level.
        const threshold = (BAYER4[(y & 3) * 4 + (x & 3)] / 16 - 0.5) * config.dither
        const i = y * width + x
        const c = color[i]
        color[i] = Color.rgb(
          channel(Color.r(c), threshold, levels),
          channel(Color.g(c), threshold, levels),
          channel(Color.b(c), threshold, levels),
        )
      }
    }
  }

  /** Bayer 4x4 threshold map (values 0..15), read modulo 4 in x and y. */
  const BAYER4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]

  /** Snap one 0..255 channel to `levels` steps after applying the dither
   *  threshold, then expand back to 0..255. */
  function channel(value: number, threshold: number, levels: number): number {
    const n = value / 255 + threshold / levels
    const q = Math.min(levels, Math.max(0, Math.round(n * levels)))
    return Math.round((q / levels) * 255)
  }
}
