///|
/// Color matrix transformations.
///
/// A ColorM is a 5x4 matrix that transforms RGBA color values.
/// The matrix layout follows Ebiten convention:
///   [r']   [m00 m01 m02 m03 m04] [r]
///   [g'] = [m10 m11 m12 m13 m14] [g]
///   [b']   [m20 m21 m22 m23 m24] [b]
///   [a']   [m30 m31 m32 m33 m34] [a]
///                                [1]
/// Stored as a flat array of 20 elements (row-major, 4 rows x 5 columns).
///
/// Reference:
/// - Ebiten colorm package

///|
pub struct ColorM {
  elements : FixedArray[Double]
} derive(Debug)

///|
pub impl Show for ColorM with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub fn ColorM::identity() -> ColorM {
  {
    elements: [
      1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
      0.0, 0.0, 0.0, 1.0, 0.0,
    ],
  }
}

///|
fn ColorM::element_at(self : ColorM, row : Int, col : Int) -> Double {
  self.elements[row * 5 + col]
}

///|
fn ColorM::set_element(
  self : ColorM,
  row : Int,
  col : Int,
  value : Double,
) -> Unit {
  self.elements[row * 5 + col] = value
}

///|
pub fn ColorM::is_identity(self : ColorM) -> Bool {
  for row in 0..<4 {
    for col in 0..<5 {
      let expected = if row == col { 1.0 } else { 0.0 }
      if self.element_at(row, col) != expected {
        return false
      }
    }
  }
  true
}

///|
/// Concatenate two color matrices (self * other).
pub fn ColorM::concat(self : ColorM, other : ColorM) -> ColorM {
  let result = ColorM::identity()
  for row in 0..<4 {
    for col in 0..<5 {
      let mut sum = 0.0
      for k in 0..<4 {
        sum = sum + self.element_at(row, k) * other.element_at(k, col)
      }
      if col == 4 {
        sum = sum + self.element_at(row, 4)
      }
      result.set_element(row, col, sum)
    }
  }
  result
}

///|
/// Scale RGBA channels.
pub fn ColorM::scale(
  self : ColorM,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> ColorM {
  let s = ColorM::identity()
  s.set_element(0, 0, r)
  s.set_element(1, 1, g)
  s.set_element(2, 2, b)
  s.set_element(3, 3, a)
  s.concat(self)
}

///|
/// Translate (offset) RGBA channels.
pub fn ColorM::translate(
  self : ColorM,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> ColorM {
  let t = ColorM::identity()
  t.set_element(0, 4, r)
  t.set_element(1, 4, g)
  t.set_element(2, 4, b)
  t.set_element(3, 4, a)
  t.concat(self)
}

///|
/// Rotate hue by angle in radians.
pub fn ColorM::rotate_hue(self : ColorM, theta : Double) -> ColorM {
  // Rotation in the (R-G, B) plane via Rodrigues-style decomposition.
  // Standard approach: decompose into luminance axis rotation.
  let sin = @math.sin(theta)
  let cos = @math.cos(theta)
  let m = ColorM::identity()
  // Matrix that rotates hue while preserving luminance.
  // Based on the NTSC luminance coefficients: R=0.213, G=0.715, B=0.072
  let lr = 0.213
  let lg = 0.715
  let lb = 0.072
  m.set_element(0, 0, lr + cos * (1.0 - lr) + sin * -lr)
  m.set_element(0, 1, lg + cos * -lg + sin * -lg)
  m.set_element(0, 2, lb + cos * -lb + sin * (1.0 - lb))
  m.set_element(1, 0, lr + cos * -lr + sin * 0.143)
  m.set_element(1, 1, lg + cos * (1.0 - lg) + sin * 0.140)
  m.set_element(1, 2, lb + cos * -lb + sin * -0.283)
  m.set_element(2, 0, lr + cos * -lr + sin * -(1.0 - lr))
  m.set_element(2, 1, lg + cos * -lg + sin * lg)
  m.set_element(2, 2, lb + cos * (1.0 - lb) + sin * lb)
  m.concat(self)
}

///|
/// Convert to monochrome (grayscale) using NTSC luminance coefficients.
pub fn ColorM::monochrome(self : ColorM) -> ColorM {
  let lr = 0.2126
  let lg = 0.7152
  let lb = 0.0722
  let m = ColorM::identity()
  m.set_element(0, 0, lr)
  m.set_element(0, 1, lg)
  m.set_element(0, 2, lb)
  m.set_element(1, 0, lr)
  m.set_element(1, 1, lg)
  m.set_element(1, 2, lb)
  m.set_element(2, 0, lr)
  m.set_element(2, 1, lg)
  m.set_element(2, 2, lb)
  m.concat(self)
}

///|
/// Invert colors.
pub fn ColorM::invert(self : ColorM) -> ColorM {
  self.scale(-1.0, -1.0, -1.0, 1.0).translate(1.0, 1.0, 1.0, 0.0)
}

///|
/// Apply color matrix to an RGBA color (all values 0.0-1.0).
pub fn ColorM::apply(
  self : ColorM,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> (Double, Double, Double, Double) {
  let nr = self.element_at(0, 0) * r +
    self.element_at(0, 1) * g +
    self.element_at(0, 2) * b +
    self.element_at(0, 3) * a +
    self.element_at(0, 4)
  let ng = self.element_at(1, 0) * r +
    self.element_at(1, 1) * g +
    self.element_at(1, 2) * b +
    self.element_at(1, 3) * a +
    self.element_at(1, 4)
  let nb = self.element_at(2, 0) * r +
    self.element_at(2, 1) * g +
    self.element_at(2, 2) * b +
    self.element_at(2, 3) * a +
    self.element_at(2, 4)
  let na = self.element_at(3, 0) * r +
    self.element_at(3, 1) * g +
    self.element_at(3, 2) * b +
    self.element_at(3, 3) * a +
    self.element_at(3, 4)
  (nr, ng, nb, na)
}

///|
/// Apply color matrix to a gfx.Color.
pub fn ColorM::apply_color(self : ColorM, color : @gfx.Color) -> @gfx.Color {
  let (r, g, b, a) = self.apply(color.r, color.g, color.b, color.a)
  @gfx.new_color(r, g, b, a)
}

///|
fn clamp01(v : Double) -> Double {
  if v < 0.0 {
    0.0
  } else if v > 1.0 {
    1.0
  } else {
    v
  }
}

///|
/// Apply color matrix and clamp result to [0, 1].
pub fn ColorM::apply_clamped(
  self : ColorM,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> (Double, Double, Double, Double) {
  let (nr, ng, nb, na) = self.apply(r, g, b, a)
  (clamp01(nr), clamp01(ng), clamp01(nb), clamp01(na))
}

///|
/// Change HSV: scale_hue rotates hue (radians), scale_saturation and scale_value
/// scale saturation and value.
pub fn ColorM::change_hsv(
  self : ColorM,
  scale_hue : Double,
  scale_saturation : Double,
  scale_value : Double,
) -> ColorM {
  self
  .rotate_hue(scale_hue)
  .scale(scale_value, scale_value, scale_value, 1.0)
  .scale(
    1.0 + (1.0 - scale_saturation) * (-1.0 + 0.213 / 1.0),
    1.0 + (1.0 - scale_saturation) * (-1.0 + 0.715 / 1.0),
    1.0 + (1.0 - scale_saturation) * (-1.0 + 0.072 / 1.0),
    1.0,
  )
}