///|
/// Observations are rows, features are columns. Public accessors return copies.
pub struct Dataset {
  features : Array[String]
  samples : Array[String]
  cells : Array[Array[Double]]
}

///|
fn labels(names : Array[String]) -> Unit raise CodaError {
  for i, name in names {
    if name.trim().length() == 0 || name.length() > 128 {
      raise Invalid("labels must be nonblank and at most 128 code units")
    }
    for j in 0.. Dataset raise CodaError {
  vector_size(features.length())
  if rows.length() < 1 || rows.length() > 1024 {
    raise Budget("dataset row count must be 1..1024")
  }
  if samples.length() != rows.length() {
    raise Shape("sample label count differs from rows")
  }
  labels(features)
  labels(samples)
  let cells = []
  for row in rows {
    if row.length() != features.length() {
      raise Shape("ragged dataset or incorrect feature count")
    }
    positive(row)
    cells.push(closure(row))
  }
  { features: features.copy(), samples: samples.copy(), cells, }
}

///|
pub fn Dataset::feature_names(self : Dataset) -> Array[String] {
  self.features.copy()
}

///|
pub fn Dataset::sample_names(self : Dataset) -> Array[String] {
  self.samples.copy()
}

///|
pub fn Dataset::rows(self : Dataset) -> Array[Array[Double]] {
  self.cells.map(r => r.copy())
}

///|
pub fn Dataset::row_count(self : Dataset) -> Int {
  self.cells.length()
}

///|
pub fn Dataset::part_count(self : Dataset) -> Int {
  self.features.length()
}

///|
pub fn Dataset::clr_rows(
  self : Dataset,
) -> Array[Array[Double]] raise CodaError {
  self.cells.map(r => clr(r))
}

///|
pub fn Dataset::ilr_rows(
  self : Dataset,
  b : Basis,
) -> Array[Array[Double]] raise CodaError {
  self.cells.map(r => ilr(r, b))
}

///|
/// Explicit name alignment prevents silently comparing different column orders.
pub fn Dataset::select_features(
  self : Dataset,
  order : Array[String],
) -> Dataset raise CodaError {
  labels(order)
  vector_size(order.length())
  let indices = []
  for name in order {
    let mut found = -1
    for i, existing in self.features {
      if existing == name {
        found = i
      }
    }
    if found < 0 {
      raise Invalid("requested feature name is absent")
    }
    indices.push(found)
  }
  dataset(order, self.samples, self.cells.map(r => subcomposition(r, indices)))
}

///|
pub fn Dataset::select_samples(
  self : Dataset,
  indices : Array[Int],
) -> Dataset raise CodaError {
  if indices.length() == 0 {
    raise Shape("empty sample selection")
  }
  unique_indices(indices, self.cells.length())
  dataset(
    self.features,
    indices.map(i => self.samples[i]),
    indices.map(i => self.cells[i]),
  )
}