///|
pub(all) struct CsvOptions {
  delimiter : String
  has_header : Bool
  comment_prefix : String
} derive(Debug, Eq)

///|
pub fn CsvOptions::default() -> CsvOptions {
  { delimiter: ",", has_header: true, comment_prefix: "#" }
}

///|
fn meaningful_lines(text : String, comment_prefix : String) -> Array[String] {
  let lines : Array[String] = []
  for raw in text.split("\n") {
    let line = raw.trim().to_owned()
    if line != "" && !line.has_prefix(comment_prefix) {
      lines.push(line)
    }
  }
  lines
}

///|
fn parse_number(
  text : StringView,
  row : Int,
  column : Int,
) -> Double raise @domain.ExtractionError {
  @strconv.from_str(text.trim()) catch {
    _ =>
      raise @domain.ExtractionError::InvalidArgument(
        message="invalid number at row \{row}, column \{column}: \{text}",
      )
  }
}

///|
/// Parse a two-column equilibrium table (`x,y`) from CSV text.
pub fn parse_equilibrium_csv(
  text : String,
  options? : CsvOptions = CsvOptions::default(),
  extrapolation? : @interpolation.Extrapolation = @interpolation.Extrapolation::Reject,
) -> @interpolation.EquilibriumTable raise @domain.ExtractionError {
  if options.delimiter == "" {
    raise @domain.ExtractionError::InvalidArgument(
      message="CSV delimiter cannot be empty",
    )
  }
  let lines = meaningful_lines(text, options.comment_prefix)
  let first_data_row = if options.has_header { 1 } else { 0 }
  if lines.length() <= first_data_row {
    raise @domain.ExtractionError::InsufficientData(
      required=first_data_row + 2,
      actual=lines.length(),
    )
  }
  let points : Array[@domain.EquilibriumPoint] = []
  for index in first_data_row.. Array[@domain.TieLine] raise @domain.ExtractionError {
  let lines = meaningful_lines(text, options.comment_prefix)
  let first_data_row = if options.has_header { 1 } else { 0 }
  if lines.length() <= first_data_row {
    raise @domain.ExtractionError::InsufficientData(
      required=first_data_row + 1,
      actual=lines.length(),
    )
  }
  let tie_lines : Array[@domain.TieLine] = []
  for index in first_data_row.. parse_number(cell, index + 1, column + 1)
    ]
    tie_lines.push(
      @domain.TieLine::new(
        raffinate=@domain.TernaryComposition::new(
          carrier=values[0],
          solute=values[1],
          solvent=values[2],
        ),
        extract=@domain.TernaryComposition::new(
          carrier=values[3],
          solute=values[4],
          solvent=values[5],
        ),
      ),
    )
  }
  tie_lines
}