///|
pub(all) struct CsvOptions {
  delimiter : Char
  trim_fields : Bool
  allow_comments : Bool
} derive(Debug, Eq)

///|
pub fn default_csv_options() -> CsvOptions {
  { delimiter: ',', trim_fields: true, allow_comments: true }
}

///|
pub(all) suberror CsvError {
  EmptyRow
  UnterminatedQuote(Int)
  UnexpectedQuote(Int)
  InvalidColumnCount(Int, Int, Int)
} derive(Debug, Eq)

///|
pub fn parse_csv_row(
  line : String,
  options : CsvOptions,
) -> Array[String] raise CsvError {
  let result = Array::new()
  let mut field = StringBuilder()
  let mut quoted = false
  let mut quote_closed = false
  let mut column = 0
  for character in line {
    if quoted {
      if character == '"' {
        quoted = false
        quote_closed = true
      } else {
        field.write_char(character)
      }
    } else if character == '"' {
      if field.to_string().length() > 0 {
        raise CsvError::UnexpectedQuote(column)
      }
      quoted = true
      quote_closed = false
    } else if character == options.delimiter {
      result.push(normalize_csv_field(field.to_string(), options))
      field = StringBuilder()
      column += 1
      quote_closed = false
    } else {
      if quote_closed && !character.is_whitespace() {
        raise CsvError::UnexpectedQuote(column)
      }
      field.write_char(character)
    }
  }
  if quoted {
    raise CsvError::UnterminatedQuote(column)
  }
  result.push(normalize_csv_field(field.to_string(), options))
  result
}

///|
fn normalize_csv_field(value : String, options : CsvOptions) -> String {
  if options.trim_fields {
    value.trim().to_owned()
  } else {
    value
  }
}

///|
pub fn parse_csv_table(
  text : String,
  options : CsvOptions,
) -> Array[Array[String]] raise CsvError {
  let rows = Array::new()
  for line in text.split("\n") {
    let clean = line.trim().to_owned()
    if clean.is_empty() || (options.allow_comments && clean.has_prefix("#")) {
      continue
    }
    rows.push(parse_csv_row(clean, options))
  }
  rows
}

///|
pub fn require_rectangular(
  rows : ArrayView[Array[String]],
) -> Int raise CsvError {
  if rows.is_empty() {
    raise CsvError::EmptyRow
  }
  let columns = rows[0].length()
  for i in 1.. String {
  let needs_quote = value.contains(delimiter.to_string()) ||
    value.contains("\"") ||
    value.contains("\n")
  if needs_quote {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
pub fn csv_join_row(values : ArrayView[String], delimiter : Char) -> String {
  values
  .map(fn(value) { csv_escape(value, delimiter) })
  .join(delimiter.to_string())
}

///|
pub fn csv_table_to_text(
  rows : ArrayView[Array[String]],
  delimiter : Char,
) -> String {
  rows.map(fn(row) { csv_join_row(row, delimiter) }).join("\n")
}

///|
pub fn column_index(header : ArrayView[String], name : String) -> Int? {
  for i, value in header {
    if value.to_lower() == name.to_lower() {
      return Some(i)
    }
  }
  None
}

///|
pub fn select_columns(
  row : ArrayView[String],
  indexes : ArrayView[Int],
) -> Array[String] {
  let result = Array::new()
  for index in indexes {
    if index >= 0 && index < row.length() {
      result.push(row[index])
    }
  }
  result
}

///|
pub fn rows_with_column(
  rows : ArrayView[Array[String]],
  index : Int,
  expected : String,
) -> Array[Array[String]] {
  let result = Array::new()
  for row in rows {
    if index >= 0 && index < row.length() && row[index] == expected {
      result.push(row)
    }
  }
  result
}

///|
pub fn unique_column_values(
  rows : ArrayView[Array[String]],
  index : Int,
) -> Array[String] {
  let result = Array::new()
  for row in rows {
    if index >= 0 && index < row.length() && !result.contains(row[index]) {
      result.push(row[index])
    }
  }
  result
}

///|
pub fn csv_numeric_column(
  rows : ArrayView[Array[String]],
  index : Int,
) -> Array[Double] raise VisionFormatError {
  let result = Array::new()
  for row in rows {
    if index < 0 || index >= row.length() {
      raise VisionFormatError::InvalidRow("numeric column out of range")
    }
    result.push(parse_double_field(row[index][:], "csv numeric column"))
  }
  result
}