///|
/// Strict comma-delimited CSV, with LF/CRLF and doubled quotes.
/// A leading BOM is accepted; no spaces, values or unknown columns are discarded.
pub fn parse_csv(source : String) -> Table raise MergeError {
  parse_delimited(source, ',')
}

///|
/// Quoted tab-delimited text; tabs/newlines use doubled-quote CSV rules.
pub fn parse_tsv(source : String) -> Table raise MergeError {
  parse_delimited(source, '\t')
}

///|
fn parse_delimited(source : String, delimiter : Char) -> Table raise MergeError {
  if source.length() > max_csv_chars {
    raise LimitExceeded("CSV exceeds 40000000 UTF-16 code units")
  }
  validate_unicode(source)
  let chars = source.to_array()
  let rows : Array[Array[String]] = []
  let mut row : Array[String] = []
  let field = StringBuilder()
  // 0: start, 1: unquoted, 2: quoted, 3: closed quote.
  let mut state = 0
  let mut i = if chars.get(0) == Some('\uFEFF') { 1 } else { 0 }
  let mut active = false
  let mut cells = 0
  while i < chars.length() {
    let c = chars[i]
    active = true
    if state == 2 {
      if c == '"' {
        if chars.get(i + 1) == Some('"') {
          field.write_char('"')
          i += 1
        } else {
          state = 3
        }
      } else {
        field.write_char(c)
      }
    } else if c == delimiter || c == '\n' || c == '\r' {
      if c == '\r' {
        if chars.get(i + 1) != Some('\n') {
          raise InvalidCsv(
            record=rows.length() + 1,
            column=row.length() + 1,
            reason="bare CR outside quotes; use LF or CRLF",
          )
        }
        i += 1
      }
      row.push(field.to_string())
      cells += 1
      if cells > max_cells + max_columns {
        raise LimitExceeded("too many CSV fields")
      }
      field.reset()
      state = 0
      if row.length() > max_columns {
        raise LimitExceeded("CSV exceeds 512 columns")
      }
      if c != delimiter {
        rows.push(row)
        row = []
        active = false
        if rows.length() > max_rows + 1 {
          raise LimitExceeded("CSV exceeds 100000 data rows")
        }
      }
    } else if state == 3 {
      raise InvalidCsv(
        record=rows.length() + 1,
        column=row.length() + 1,
        reason="characters after a closing quote; remove them or quote the entire field",
      )
    } else if c == '"' {
      if state != 0 {
        raise InvalidCsv(
          record=rows.length() + 1,
          column=row.length() + 1,
          reason="quote inside an unquoted field; quote the entire field",
        )
      }
      state = 2
    } else {
      field.write_char(c)
      state = 1
    }
    i += 1
  }
  if state == 2 {
    raise InvalidCsv(
      record=rows.length() + 1,
      column=row.length() + 1,
      reason="unterminated quoted field; add the closing quote",
    )
  }
  if active {
    row.push(field.to_string())
    rows.push(row)
  }
  guard rows.get(0) is Some(header) else {
    raise InvalidTable("empty CSV; a header is required")
  }
  Table::from_rows(header, rows[1:].to_owned())
}

///|
/// Deterministic CSV. Cell strings round-trip; original formatting does not.
pub fn Table::to_csv(self : Table) -> String {
  self.to_delimited(',')
}

///|
pub fn Table::to_tsv(self : Table) -> String {
  self.to_delimited('\t')
}

///|
fn Table::to_delimited(self : Table, delimiter : Char) -> String {
  let out = StringBuilder()
  write_record(out, self.columns, delimiter)
  for row in self.records {
    write_record(out, row, delimiter)
  }
  out.to_string()
}

///|
fn write_record(
  out : StringBuilder,
  values : Array[String],
  delimiter : Char,
) -> Unit {
  for i, value in values {
    if i != 0 {
      out.write_char(delimiter)
    }
    if value.has_prefix("\uFEFF") ||
      value.contains(delimiter.to_string()) ||
      value.contains("\"") ||
      value.contains("\n") ||
      value.contains("\r") {
      out.write_char('"')
      for c in value {
        out.write_char(c)
        if c == '"' {
          out.write_char('"')
        }
      }
      out.write_char('"')
    } else {
      out.write_string(value)
    }
  }
  out.write_char('\n')
}