///|
priv struct CsvReader {
  rows : Array[Array[String]]
  mut fields : Array[String]
  mut field : StringBuilder
  mut field_units : Int32
  mut total_units : Int32
}

///|
fn CsvReader::character(self : CsvReader, c : Char) -> Unit raise OdsError {
  let units = if c.to_int() > 0xFFFF { 2 } else { 1 }
  self.field_units += units
  self.total_units += units
  if self.field_units > 32767 || self.total_units > 2097152 {
    raise Invalid("CSV text budget exceeded")
  }
  self.field.write_char(c)
}

///|
fn CsvReader::finish_field(self : CsvReader) -> Unit raise OdsError {
  if self.fields.length() >= 128 {
    raise Invalid("CSV exceeds 128 columns")
  }
  self.fields.push(self.field.to_string())
  self.field = StringBuilder()
  self.field_units = 0
}

///|
fn CsvReader::finish_row(self : CsvReader) -> Unit raise OdsError {
  self.finish_field()
  if self.rows.length() >= 1024 ||
    (self.rows.length() + 1) * self.fields.length() > 65536 {
    raise Invalid("CSV grid budget exceeded")
  }
  if !self.rows.is_empty() && self.fields.length() != self.rows[0].length() {
    raise Invalid("CSV rows must have equal column counts")
  }
  self.rows.push(self.fields)
  self.fields = []
}

///|
/// RFC4180 quoting with CRLF or LF record separators; bare CR is rejected.
/// Every field becomes Text (including empty fields and leading '='). No type
/// guessing or formula evaluation. Empty input imports one empty text cell.
/// Parsing/validation finishes before the workbook is mutated.
pub fn Workbook::add_csv_sheet(
  self : Workbook,
  name : String,
  text : String,
) -> Sheet raise OdsError {
  if text.length() > 4194304 {
    raise Invalid("CSV input exceeds 4194304 UTF-16 units")
  }
  let reader : CsvReader = {
    rows: [],
    fields: [],
    field: StringBuilder(),
    field_units: 0,
    total_units: 0,
  }
  let mut quoted = false
  let mut closed = false
  let mut cr = false
  let mut ended_row = false
  for c in text {
    if cr {
      if c != '\n' {
        raise Invalid("CSV bare CR record separator")
      }
      cr = false
      continue
    }
    if quoted {
      if c == '"' {
        quoted = false
        closed = true
      } else {
        reader.character(c)
      }
      continue
    }
    if closed && c == '"' {
      reader.character('"')
      quoted = true
      closed = false
      continue
    }
    if c == ',' {
      reader.finish_field()
      closed = false
      ended_row = false
    } else if c == '\r' || c == '\n' {
      reader.finish_row()
      closed = false
      cr = c == '\r'
      ended_row = true
    } else if c == '"' && !closed && reader.field_units == 0 {
      quoted = true
      ended_row = false
    } else {
      if closed || c == '"' {
        raise Invalid("CSV unexpected character around quote")
      }
      reader.character(c)
      ended_row = false
    }
  }
  if quoted || cr {
    raise Invalid("CSV truncated quote or CRLF")
  }
  if !ended_row {
    reader.finish_row()
  }
  // Validate XML-compatible strings and sheet budgets in an isolated sheet.
  let candidate = Workbook::new().add_sheet(name)
  for row, fields in reader.rows {
    candidate.set_row(row, 0, fields.map(s => Cell::new(Text(s))))
  }
  let sheet = self.add_sheet(name)
  for pos, cell in candidate.cells {
    sheet.cells[pos] = cell
  }
  sheet.rows = candidate.rows
  sheet.cols = candidate.cols
  sheet.text_units = candidate.text_units
  sheet
}

///|
fn csv_quote(text : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  for c in text {
    if c == '"' {
      out.write_char('"')
    }
    out.write_char(c)
  }
  out.write_char('"')
  out.to_string()
}

///|
fn csv_formula_like(text : String) -> Bool {
  // Ignore leading whitespace as importers may trim it before type inference.
  for c in text {
    if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
      continue
    }
    return c == '=' || c == '+' || c == '-' || c == '@'
  }
  false
}

///|
/// Deterministic comma CSV, quoted fields and CRLF records. Dates use ISO dates,
/// booleans true/false, numbers binary64 text. Empty and Text("") both emit "".
/// Merges are rejected. Formula caches require an explicit opt-in; formulas,
/// types, styles and sheet metadata cannot survive a CSV roundtrip.
/// External spreadsheet applications may infer types/formulas from CSV text.
/// use_formula_cache exports stored, possibly stale caches without recalculation.
/// reject_formula_like_text skips only leading ASCII space, TAB, CR and LF, then
/// rejects Text values starting =, +, - or @. It never changes data or rejects
/// typed negative numbers, and is a bounded policy rather than an application
/// sandbox. The default preserves the existing lossless text semantics.
pub fn Sheet::to_csv(
  self : Sheet,
  use_formula_cache~ : Bool = false,
  reject_formula_like_text~ : Bool = false,
) -> String raise OdsError {
  if !self.spans.is_empty() {
    raise Invalid("CSV export requires an unmerged sheet")
  }
  let out = XmlOutput::new()
  for row in 0.. 0 {
        out.write_string(",")
      }
      let value = match self.get(row, col) {
        Some(cell) => {
          if !use_formula_cache && cell.expression() is Some(_) {
            raise Invalid("CSV formula export requires use_formula_cache=true")
          }
          cell.value()
        }
        None => Empty
      }
      let text = match value {
        Empty => ""
        Text(s) => {
          if reject_formula_like_text && csv_formula_like(s) {
            raise Invalid("CSV formula-like text at row \{row} col \{col}")
          }
          s
        }
        Number(n) => finite_number_text(n)
        Boolean(b) => if b { "true" } else { "false" }
        Date(y, m, d) => "\{pad(y, 4)}-\{pad(m, 2)}-\{pad(d, 2)}"
      }
      out.write_string(csv_quote(text))
    }
    out.write_string("\r\n")
  }
  out.to_string()
}