import { Color } from "./Color"

/** A square-or-rectangular image of packed RGBA texels, row-major. */
export type Texture = {
  readonly width: number
  readonly height: number
  readonly data: Uint32Array
}

export namespace Texture {
  /** Generate a `size`x`size` checkerboard of `cells` squares per axis, using
   *  colors `a` (top-left) and `b`. A stand-in until real textures load. */
  export function checker(size: number, cells: number, a: Color, b: Color): Texture {
    const data = new Uint32Array(size * size)
    const cell = size / cells
    for (let y = 0; y < size; y++) {
      for (let x = 0; x < size; x++) {
        const on = (Math.floor(x / cell) + Math.floor(y / cell)) % 2 === 0
        data[y * size + x] = on ? a : b
      }
    }
    return { width: size, height: size, data }
  }

  /**
   * Sample a texel. Coordinates wrap (tile) outside 0..1. `nearest` point-
   * samples for crunchy PS1 texels; `linear` bilinearly blends the four
   * neighbors for a smooth (non-period) result.
   *
   * Note there are no mipmaps: when a textured surface is minified in the
   * distance, many texels fall inside one pixel and point sampling picks an
   * essentially random one, so the pattern aliases into a crawling moire as the
   * camera moves. That shimmer is itself part of the PS1 look here; the usual
   * cure (mipmaps) is a deliberate future step, not a bug.
   */
  export function sample(tex: Texture, u: number, v: number, filter: "nearest" | "linear"): Color {
    return filter === "linear" ? bilinear(tex, u, v) : nearest(tex, u, v)
  }

  function nearest(tex: Texture, u: number, v: number): Color {
    const x = wrap(Math.floor(frac(u) * tex.width), tex.width)
    const y = wrap(Math.floor(frac(v) * tex.height), tex.height)
    return tex.data[y * tex.width + x]
  }

  function bilinear(tex: Texture, u: number, v: number): Color {
    // -0.5 aligns the sample grid to texel centers before blending.
    const fx = frac(u) * tex.width - 0.5
    const fy = frac(v) * tex.height - 0.5
    const x0 = Math.floor(fx)
    const y0 = Math.floor(fy)
    const tx = fx - x0
    const ty = fy - y0
    const top = Color.lerp(texel(tex, x0, y0), texel(tex, x0 + 1, y0), tx)
    const bottom = Color.lerp(texel(tex, x0, y0 + 1), texel(tex, x0 + 1, y0 + 1), tx)
    return Color.lerp(top, bottom, ty)
  }

  function texel(tex: Texture, x: number, y: number): Color {
    return tex.data[wrap(y, tex.height) * tex.width + wrap(x, tex.width)]
  }

  /** Fractional part in 0..1 (handles negatives), for uv tiling. */
  function frac(n: number): number {
    return n - Math.floor(n)
  }

  /** Wrap an index into 0..size-1, staying non-negative for negative inputs. */
  function wrap(n: number, size: number): number {
    return ((n % size) + size) % size
  }
}
