import { Vec3 } from "./Vec3"

/** 4x4 matrix in column-major storage: index = col * 4 + row, matching OpenGL
 *  conventions so the standard perspective/lookAt formulas apply directly. */
export type Mat4 = Float32Array

export namespace Mat4 {
  /** Matrix product A * B (apply B first, then A). */
  export function multiply(a: Mat4, b: Mat4): Mat4 {
    const out = new Float32Array(16)
    for (let col = 0; col < 4; col++) {
      for (let row = 0; row < 4; row++) {
        let sum = 0
        for (let k = 0; k < 4; k++) {
          sum += a[k * 4 + row] * b[col * 4 + k]
        }
        out[col * 4 + row] = sum
      }
    }
    return out
  }

  /** Model transform T * Ry * S: uniform `scale`, then a yaw rotation about Y,
   *  then a translation. Built directly in column-major storage (no intermediate
   *  matmuls) since it runs per instance per frame. A vertex at local +Z ends up
   *  pointing along world (sin yaw, 0, cos yaw), i.e. the object faces `yaw`. */
  export function compose(tx: number, ty: number, tz: number, yaw: number, scale: number): Mat4 {
    const c = Math.cos(yaw)
    const s = Math.sin(yaw)
    const out = new Float32Array(16)
    out[0] = scale * c
    out[2] = scale * -s
    out[5] = scale
    out[8] = scale * s
    out[10] = scale * c
    out[12] = tx
    out[13] = ty
    out[14] = tz
    out[15] = 1
    return out
  }

  /** Right-handed perspective projection (camera looks down -Z). Maps the view
   *  frustum to clip space; the -1 in row 3 copies -z into w, so the later
   *  divide by w is what produces foreshortening. */
  export function perspective(fovY: number, aspect: number, near: number, far: number): Mat4 {
    const f = 1 / Math.tan(fovY / 2)
    const out = new Float32Array(16)
    out[0] = f / aspect
    out[5] = f
    out[10] = (far + near) / (near - far)
    out[11] = -1
    out[14] = (2 * far * near) / (near - far)
    return out
  }

  /** View matrix looking from `eye` toward `center`, with `up` roughly up.
   *  Builds an orthonormal camera basis (s = right, u = up, f = forward) and
   *  packs it as the inverse camera transform. */
  export function lookAt(eye: Vec3, center: Vec3, up: Vec3): Mat4 {
    const f = Vec3.normalize(Vec3.sub(center, eye))
    const s = Vec3.normalize(Vec3.cross(f, up))
    const u = Vec3.cross(s, f)
    const out = new Float32Array(16)
    out[0] = s.x
    out[1] = u.x
    out[2] = -f.x
    out[4] = s.y
    out[5] = u.y
    out[6] = -f.y
    out[8] = s.z
    out[9] = u.z
    out[10] = -f.z
    out[12] = -Vec3.dot(s, eye)
    out[13] = -Vec3.dot(u, eye)
    out[14] = Vec3.dot(f, eye)
    out[15] = 1
    return out
  }

  /** Transform a point, returning homogeneous coords. `w` is kept (not divided
   *  out) because the rasterizer needs it for near-clipping and the perspective
   *  divide/depth; for a perspective matrix w equals the view-space distance. */
  export function transform(m: Mat4, v: Vec3): { x: number; y: number; z: number; w: number } {
    return {
      x: m[0] * v.x + m[4] * v.y + m[8] * v.z + m[12],
      y: m[1] * v.x + m[5] * v.y + m[9] * v.z + m[13],
      z: m[2] * v.x + m[6] * v.y + m[10] * v.z + m[14],
      w: m[3] * v.x + m[7] * v.y + m[11] * v.z + m[15],
    }
  }
}
