// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A 2×2 matrix, stored as two column vectors.
///
/// Column-major, like every matrix here: `c0` is the first column, so the
/// matrix that maps `x` to `(a, c)` and `y` to `(b, d)` is `c0 = (a, c)`,
/// `c1 = (b, d)`, and its `x` and `y` are the first and second rows. WGSL writes
/// a `mat2x2` in the same order, so `to_array` goes straight into a buffer.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Mat2 {
  /// The first column.
  c0 : Vec2
  /// The second column.
  c1 : Vec2
}

///|
pub extend Mat2 with Eq::{equal, not_equal}

///|
/// The matrix with `c0` and `c1` as its columns.
pub fn Mat2::from_cols(c0 : Vec2, c1 : Vec2) -> Mat2 {
  { c0, c1, }
}

///|
/// The matrix with `r0` and `r1` as its rows.
pub fn Mat2::from_rows(r0 : Vec2, r1 : Vec2) -> Mat2 {
  { c0: { x: r0.x, y: r1.x, }, c1: { x: r0.y, y: r1.y, }, }
}

///|
/// The matrix from four values in column-major order: `[c0.x, c0.y, c1.x, c1.y]`.
///
/// This is the order a `mat2x2` sits in a GPU buffer in, so a matrix read
/// back out of one goes in here unchanged. Aborts if the array does not hold
/// four values.
pub fn Mat2::from_array(values : FixedArray[Float]) -> Mat2 {
  guard values.length() == 4 else {
    abort("Mat2::from_array wants 4 values, got \{values.length()}")
  }
  { c0: { x: values[0], y: values[1], }, c1: { x: values[2], y: values[3], }, }
}

///|
/// The identity: the matrix that changes nothing.
pub fn Mat2::identity() -> Mat2 {
  { c0: { x: 1.0, y: 0.0, }, c1: { x: 0.0, y: 1.0, }, }
}

///|
/// The zero matrix: the one that sends everything to the origin.
pub fn Mat2::zero() -> Mat2 {
  { c0: Vec2::zero(), c1: Vec2::zero(), }
}

///|
/// The matrix that scales `x` by `s.x` and `y` by `s.y`.
pub fn Mat2::from_scale(s : Vec2) -> Mat2 {
  { c0: { x: s.x, y: 0.0, }, c1: { x: 0.0, y: s.y, }, }
}

///|
/// The matrix that rotates counter-clockwise by `radians`.
pub fn Mat2::from_angle(radians : Float) -> Mat2 {
  let c = @math.cosf(radians)
  let s = @math.sinf(radians)
  { c0: { x: c, y: s, }, c1: { x: -s, y: c, }, }
}

///|
/// The angle, in radians, the matrix rotates `x` by.
///
/// Only meaningful for a rotation matrix — one built by `from_angle`, or a
/// linear part that happens to be one; on anything else it reads the direction
/// of the first column.
pub fn Mat2::to_angle(self : Mat2) -> Float {
  @math.atan2f(self.c0.y, self.c0.x)
}

///|
/// Column `index`, counted from zero.
pub fn Mat2::col(self : Mat2, index : Int32) -> Vec2 {
  if index == 0 {
    self.c0
  } else {
    self.c1
  }
}

///|
/// Row `index`, counted from zero.
pub fn Mat2::row(self : Mat2, index : Int32) -> Vec2 {
  if index == 0 {
    { x: self.c0.x, y: self.c1.x, }
  } else {
    { x: self.c0.y, y: self.c1.y, }
  }
}

///|
/// The element at `row` and `column`, both counted from zero.
pub fn Mat2::get(self : Mat2, row_index : Int32, column_index : Int32) -> Float {
  let column = if column_index == 0 { self.c0 } else { self.c1 }
  if row_index == 0 {
    column.x
  } else {
    column.y
  }
}

///|
/// The matrix with rows and columns exchanged.
pub fn Mat2::transpose(self : Mat2) -> Mat2 {
  { c0: self.row(0), c1: self.row(1), }
}

///|
/// The signed area the matrix scales by: zero exactly when it is singular.
pub fn Mat2::determinant(self : Mat2) -> Float {
  self.c0.x * self.c1.y - self.c1.x * self.c0.y
}

///|
/// The matrix that undoes this one, or `None` when there is none.
///
/// A singular matrix — one with a zero determinant — flattens the plane onto a
/// line or a point, and nothing undoes that. `None` is the honest answer; the
/// caller decides whether that is an error or a case to handle.
pub fn Mat2::inverse(self : Mat2) -> Mat2? {
  let det = self.determinant()
  if det == 0.0 {
    None
  } else {
    let inv_det : Float = 1.0 / det
    Some({
      c0: { x: self.c1.y * inv_det, y: -self.c0.y * inv_det, },
      c1: { x: -self.c1.x * inv_det, y: self.c0.x * inv_det, },
    })
  }
}

///|
/// The vector the matrix takes `v` to.
///
/// Written as one expression per component rather than as the sum of two scaled
/// columns: a `Vec2` is a heap value in the native backend, so every intermediate
/// column is an allocation, and there are two of them here for each result.
pub fn Mat2::mul_vec2(self : Mat2, v : Vec2) -> Vec2 {
  {
    x: self.c0.x * v.x + self.c1.x * v.y,
    y: self.c0.y * v.x + self.c1.y * v.y,
  }
}

///|
/// The elements in column-major order, `[c0.x, c0.y, c1.x, c1.y]`, ready for a
/// buffer.
pub fn Mat2::to_array(self : Mat2) -> FixedArray[Float] {
  [self.c0.x, self.c0.y, self.c1.x, self.c1.y]
}

///|
/// Whether every element is finite.
pub fn Mat2::is_finite(self : Mat2) -> Bool {
  self.c0.is_finite() && self.c1.is_finite()
}

///|
/// Whether the two matrices agree to within `eps` element by element.
pub fn Mat2::approx(self : Mat2, other : Mat2, eps? : Float) -> Bool {
  self.c0.approx(other.c0, eps?) && self.c1.approx(other.c1, eps?)
}

///|
pub impl Add for Mat2 with fn add(self, other) {
  { c0: self.c0 + other.c0, c1: self.c1 + other.c1, }
}

///|
pub extend Mat2 with Add::{add}

///|
pub impl Sub for Mat2 with fn sub(self, other) {
  { c0: self.c0 - other.c0, c1: self.c1 - other.c1, }
}

///|
pub extend Mat2 with Sub::{sub}

///|
/// The matrix product: `(self * other) * v` is `self * (other * v)`.
pub impl Mul for Mat2 with fn mul(self, other) {
  { c0: self.mul_vec2(other.c0), c1: self.mul_vec2(other.c1), }
}

///|
pub extend Mat2 with Mul::{mul}

///|
/// The product with the inverse of the right-hand side; aborts when that side is
/// singular, since `Div` has nowhere to report it.
pub impl Div for Mat2 with fn div(self, other) {
  match other.inverse() {
    Some(inverse) => self * inverse
    None => abort("Mat2 division by a singular matrix")
  }
}

///|
pub extend Mat2 with Div::{div}

///|
pub impl Neg for Mat2 with fn neg(self) {
  { c0: -self.c0, c1: -self.c1, }
}

///|
pub extend Mat2 with Neg::{neg}

///|
pub impl Show for Mat2 with fn output(self, logger) {
  logger.write_string("Mat2[\{self.c0}, \{self.c1}]")
}

///|
pub extend Mat2 with Show::{output, to_string}

///|
pub impl Debug for Mat2 with fn to_repr(self) {
  @debug.Repr::ctor("Mat2", [
    (Some("c0"), self.c0.to_repr()),
    (Some("c1"), self.c1.to_repr()),
  ])
}

///|
pub extend Mat2 with Debug::{to_repr}