///|
/// Canonical CSC sparse double/logical matrix. Payload includes all stored slots, including unused capacity.
pub(all) struct SparseArray {
  name : String
  dtype : DType
  shape : Array[Int]
  row_indices : Array[Int]
  col_ptrs : Array[Int]
  real : Bytes
  imag : Bytes?
  nzmax : Int
  is_global : Bool
} derive(@debug.Debug)

///|
pub extend SparseArray with @debug.Debug::{to_repr}

///|
pub(all) enum MatVariable {
  Dense(NumericArray)
  Sparse(SparseArray)
} derive(@debug.Debug)

///|
pub extend MatVariable with @debug.Debug::{to_repr}

///|
pub(all) struct MatDocument {
  description : String
  little_endian : Bool
  variables : Array[MatVariable]
} derive(@debug.Debug)

///|
pub extend MatDocument with @debug.Debug::{to_repr}

///|
pub fn MatVariable::name(self : MatVariable) -> String {
  match self {
    Dense(a) => a.name
    Sparse(a) => a.name
  }
}

///|
pub fn MatVariable::stored_elements(self : MatVariable) -> Int {
  match self {
    Dense(a) => a.length()
    Sparse(a) => a.row_indices.length()
  }
}

///|
/// Number of used CSC entries, including explicit zeros; differs from allocated capacity.
pub fn SparseArray::nnz(self : SparseArray) -> Int {
  self.col_ptrs.get(self.col_ptrs.length() - 1).unwrap_or(0)
}

///|
pub fn MatDocument::get(self : MatDocument, name : String) -> MatVariable? {
  for item in self.variables {
    if item.name() == name {
      return Some(item)
    }
  }
  None
}

///|
pub fn MatDocument::select(
  self : MatDocument,
  names : Array[String],
) -> Array[MatVariable] raise MatError {
  let seen : Map[String, Bool] = Map([])
  let out = []
  for name in names {
    if seen.contains(name) {
      fail("duplicate-name", 0, "Repeated selection: " + name)
    }
    seen[name] = true
    match self.get(name) {
      Some(item) => out.push(item)
      None => fail("missing-variable", 0, "No variable named: " + name)
    }
  }
  out
}

///|
/// Replace one named variable with explicit dense/CSC storage. Other variables remain intact.
pub fn MatDocument::convert_storage(
  self : MatDocument,
  name : String,
  sparse : Bool,
  limits? : Limits = default_limits(),
) -> Array[MatVariable] raise MatError {
  let out = self.variables.copy()
  for i, item in self.variables {
    if item.name() == name {
      out[i] = match (item, sparse) {
        (Dense(a), true) => Sparse(a.to_sparse(limits~))
        (Sparse(a), false) => Dense(a.to_dense(limits~))
        _ => item
      }
      return out
    }
  }
  raise MatError("missing-variable", 0, "No variable named: " + name)
}

///|
fn validate_sparse(
  a : SparseArray,
  limits : Limits,
  allow_empty? : Bool = false,
) -> Int raise MatError {
  validate_name(a.name, limits, allow_empty~)
  if a.dtype != Float64 && a.dtype != Logical {
    fail(
      "unsupported-sparse-dtype", 0, "Sparse MAT supports float64 and logical",
    )
  }
  if a.shape.length() != 2 ||
    a.shape[0] < 0 ||
    a.shape[1] < 0 ||
    a.shape[1] >= limits.max_elements {
    fail(
      "invalid-shape", 0, "Sparse shape needs two nonnegative dimensions and bounded column pointers",
    )
  }
  let slots = a.row_indices.length()
  if slots > limits.max_elements ||
    a.nzmax < slots ||
    a.nzmax > limits.max_elements {
    fail(
      "element-limit", 0, "Sparse capacity exceeds configured limit or stored slots",
    )
  }
  if a.col_ptrs.length() != a.shape[1] + 1 ||
    a.col_ptrs[0] != 0 ||
    a.nnz() > slots {
    fail(
      "invalid-sparse", 0, "CSC pointers must start at zero and fit stored slots",
    )
  }
  if a.real.length() != slots * a.dtype.width() {
    fail("payload-size", 0, "Sparse values must match row-index slots")
  }
  if a.imag is Some(imag) &&
    (a.dtype != Float64 || imag.length() != a.real.length()) {
    fail(
      "payload-size", 0, "Sparse complex planes need float64 and equal storage",
    )
  }
  let index_bytes = (slots + a.col_ptrs.length()) * 4
  if index_bytes > limits.max_file_bytes ||
    a.real.length() >
    (limits.max_file_bytes - index_bytes) /
    (if a.imag is Some(_) { 2 } else { 1 }) {
    fail("file-limit", 0, "Sparse payload exceeds byte limit")
  }
  for col = 0; col < a.shape[1]; col = col + 1 {
    let start = a.col_ptrs[col]
    let end = a.col_ptrs[col + 1]
    if start < 0 || end < start || end > slots {
      fail(
        "invalid-sparse", col, "CSC column pointers must be monotone and bounded",
      )
    }
    let mut previous = -1
    for index = start; index < end; index = index + 1 {
      let row = a.row_indices[index]
      if row < 0 || row >= a.shape[0] {
        fail("invalid-sparse", index, "Sparse row is outside dimensions")
      }
      if row <= previous {
        fail(
          "noncanonical-sparse", index, "CSC rows must increase strictly within each column",
        )
      }
      previous = row
    }
  }
  if a.dtype == Logical {
    for value in a.real {
      if value != b'\x00' && value != b'\x01' {
        fail("invalid-logical", 0, "Logical sparse bytes must be 0 or 1")
      }
    }
  }
  slots
}

///|
pub fn raw_sparse_array(
  name : String,
  dtype : DType,
  shape : Array[Int],
  row_indices : Array[Int],
  col_ptrs : Array[Int],
  real : Bytes,
  imag? : Bytes,
  nzmax? : Int,
  is_global? : Bool = false,
  limits? : Limits = default_limits(),
) -> SparseArray raise MatError {
  validate_limits(limits)
  let a : SparseArray = {
    name,
    dtype,
    shape: shape.copy(),
    row_indices: row_indices.copy(),
    col_ptrs: col_ptrs.copy(),
    real,
    imag,
    nzmax: nzmax.unwrap_or(
      if row_indices.is_empty() {
        1
      } else {
        row_indices.length()
      },
    ),
    is_global,
  }
  ignore(validate_sparse(a, limits))
  a
}

///|
/// Explicit densification is bounded by shape product; huge sparse shapes never expand implicitly.
pub fn SparseArray::to_dense(
  self : SparseArray,
  limits? : Limits = default_limits(),
) -> NumericArray raise MatError {
  validate_limits(limits)
  ignore(validate_sparse(self, limits))
  let count = element_count(self.shape, limits, 0)
  let width = self.dtype.width()
  if count * width >
    limits.max_file_bytes / (if self.imag is Some(_) { 2 } else { 1 }) {
    fail("file-limit", 0, "Dense sparse expansion exceeds byte limit")
  }
  let expand = fn(bytes : Bytes) {
    let out = Array::make(count * width, b'\x00')
    for col = 0; col < self.shape[1]; col = col + 1 {
      for index = self.col_ptrs[col]
          index < self.col_ptrs[col + 1]
          index = index + 1 {
        let target = (col * self.shape[0] + self.row_indices[index]) * width
        for j = 0; j < width; j = j + 1 {
          out[target + j] = bytes[index * width + j]
        }
      }
    }
    Bytes::from_array(out)
  }
  {
    name: self.name,
    dtype: self.dtype,
    shape: self.shape.copy(),
    real: expand(self.real),
    imag: self.imag.map(expand),
    is_global: self.is_global,
  }
}

///|
/// Only all-zero byte patterns are omitted. Signed zero, NaN payloads and complex data survive.
pub fn NumericArray::to_sparse(
  self : NumericArray,
  limits? : Limits = default_limits(),
) -> SparseArray raise MatError {
  validate_limits(limits)
  ignore(validate_array(self, limits))
  if self.shape.length() != 2 ||
    (self.dtype != Float64 && self.dtype != Logical) {
    fail(
      "unsupported-sparse-dtype", 0, "Sparse conversion needs two-dimensional float64 or logical",
    )
  }
  if self.shape[1] >= limits.max_elements {
    fail("element-limit", 0, "Too many sparse columns")
  }
  let rows = []
  let cols = [0]
  let real = []
  let imaginary = []
  let width = self.dtype.width()
  for col = 0; col < self.shape[1]; col = col + 1 {
    for row = 0; row < self.shape[0]; row = row + 1 {
      let offset = (col * self.shape[0] + row) * width
      let mut present = false
      for j = 0; j < width; j = j + 1 {
        if self.real[offset + j] != b'\x00' {
          present = true
        }
        if self.imag is Some(bytes) && bytes[offset + j] != b'\x00' {
          present = true
        }
      }
      if present {
        rows.push(row)
        for j = 0; j < width; j = j + 1 {
          real.push(self.real[offset + j])
        }
        if self.imag is Some(bytes) {
          for j = 0; j < width; j = j + 1 {
            imaginary.push(bytes[offset + j])
          }
        }
      }
    }
    cols.push(rows.length())
  }
  let imag = if self.imag is Some(_) {
    Some(Bytes::from_array(imaginary))
  } else {
    None
  }
  raw_sparse_array(
    self.name,
    self.dtype,
    self.shape,
    rows,
    cols,
    Bytes::from_array(real),
    imag?,
    is_global=self.is_global,
    limits~,
  )
}