///|
pub fn ThermalMatrix::new(
  width~ : Int,
  height~ : Int,
  values~ : Array[Double],
) -> ThermalMatrix raise ThermalError {
  if width <= 0 || height <= 0 {
    raise ThermalError::InvalidDimensions(
      width~,
      height~,
      values=values.length(),
    )
  }
  let expected = width * height
  if values.length() != expected {
    raise ThermalError::InvalidDimensions(
      width~,
      height~,
      values=values.length(),
    )
  }
  { width, height, values: values.copy() }
}

///|
pub fn ThermalMatrix::from_rows(
  rows : Array[Array[Double]],
) -> ThermalMatrix raise ThermalError {
  if rows.is_empty() {
    raise ThermalError::EmptyMatrix
  }
  let width = rows[0].length()
  if width == 0 {
    raise ThermalError::EmptyMatrix
  }
  let values : Array[Double] = []
  for row_index, row in rows {
    if row.length() != width {
      raise ThermalError::RaggedRows(
        expected=width,
        actual=row.length(),
        row=row_index,
      )
    }
    values.append(row)
  }
  ThermalMatrix::new(width~, height=rows.length(), values~)
}

///|
pub fn ThermalMatrix::index(
  matrix : ThermalMatrix,
  point : ThermalPoint,
) -> Int raise ThermalError {
  if point.x < 0 ||
    point.x >= matrix.width ||
    point.y < 0 ||
    point.y >= matrix.height {
    raise ThermalError::OutOfBounds(
      x=point.x,
      y=point.y,
      width=matrix.width,
      height=matrix.height,
    )
  }
  point.y * matrix.width + point.x
}

///|
pub fn ThermalMatrix::get(
  matrix : ThermalMatrix,
  point : ThermalPoint,
) -> Double raise ThermalError {
  matrix.values[matrix.index(point)]
}

///|
pub fn ThermalMatrix::unsafe_get(
  matrix : ThermalMatrix,
  x~ : Int,
  y~ : Int,
) -> Double {
  matrix.values[y * matrix.width + x]
}

///|
pub fn ThermalMatrix::row(
  matrix : ThermalMatrix,
  y : Int,
) -> Array[Double] raise ThermalError {
  if y < 0 || y >= matrix.height {
    raise ThermalError::OutOfBounds(
      x=0,
      y~,
      width=matrix.width,
      height=matrix.height,
    )
  }
  let out : Array[Double] = []
  for x in 0.. Double,
) -> ThermalMatrix {
  { width: matrix.width, height: matrix.height, values: matrix.values.map(f) }
}

///|
pub fn ThermalMatrix::crop(
  matrix : ThermalMatrix,
  origin~ : ThermalPoint,
  width~ : Int,
  height~ : Int,
) -> ThermalMatrix raise ThermalError {
  if width <= 0 || height <= 0 {
    raise ThermalError::InvalidDimensions(
      width~,
      height~,
      values=width * height,
    )
  }
  let last = ThermalPoint::new(x=origin.x + width - 1, y=origin.y + height - 1)
  ignore(matrix.index(origin))
  ignore(matrix.index(last))
  let values : Array[Double] = []
  for y in origin.y..<(origin.y + height) {
    for x in origin.x..<(origin.x + width) {
      values.push(matrix.unsafe_get(x~, y~))
    }
  }
  ThermalMatrix::new(width~, height~, values~)
}

///|
pub fn ThermalMatrix::range(
  matrix : ThermalMatrix,
) -> TemperatureRange raise ThermalError {
  if matrix.values.is_empty() {
    raise ThermalError::EmptyMatrix
  }
  let first = matrix.values[0]
  for min = first, max = first, i = 1; i < matrix.values.length(); {
    let value = matrix.values[i]
    continue Double::min(min, value), Double::max(max, value), i + 1
  } nobreak {
    { min, max }
  }
}