///|
priv enum CsvState {
  Start
  Bare
  Quoted
  Closed
}

///|
priv struct Parser {
  side : String
  limits : Limits
  records : Array[Array[String]]
  mut fields : Array[String]
  field : StringBuilder
  mut field_units : Int
  mut state : CsvState
  mut line : Int
  mut column : Int
  mut previous_cr : Bool
  mut pending_cr : Bool
  mut touched : Bool
}

///|
fn Parser::error(
  self : Parser,
  code : String,
  message : String,
) -> Unit raise MoonRowError {
  fail(
    code,
    self.side,
    message,
    record=self.records.length() + 1,
    line=self.line,
    column=self.column,
  )
}

///|
fn Parser::append(self : Parser, ch : Char) -> Unit raise MoonRowError {
  let units = if ch.to_int() > 0xFFFF { 2 } else { 1 }
  if self.field_units + units > self.limits.max_field_units {
    self.error("LIMIT_FIELD", "field exceeds UTF-16 unit limit")
  }
  self.field.write_char(ch)
  self.field_units += units
}

///|
fn Parser::end_field(self : Parser) -> Unit raise MoonRowError {
  if self.fields.length() >= self.limits.max_columns {
    self.error("LIMIT_COLUMNS", "record exceeds column limit")
  }
  self.fields.push(self.field.to_string())
  self.field.reset()
  self.field_units = 0
  self.state = Start
}

///|
fn Parser::end_record(self : Parser) -> Unit raise MoonRowError {
  self.end_field()
  if self.records.length() > 0 &&
    self.fields.length() != self.records[0].length() {
    self.error(
      "ROW_WIDTH",
      "record has \{self.fields.length()} fields; expected \{self.records[0].length()}",
    )
  }
  if self.records.length() > self.limits.max_rows {
    self.error("LIMIT_ROWS", "input exceeds data record limit")
  }
  self.records.push(self.fields)
  self.fields = []
  self.touched = false
}

///|
/// Parses the documented UTF-8 CSV text dialect. The host must decode bytes strictly.
/// Header validation and record-width validation happen here, not only in the CLI.
pub fn parse_csv(
  text : String,
  side? : String = "input",
  limits? : Limits = default_limits(),
) -> Table raise MoonRowError {
  if limits.max_rows < 0 || limits.max_columns < 1 || limits.max_field_units < 1 {
    fail(
      "INVALID_LIMITS", side, "limits require rows >= 0, columns >= 1, field units >= 1",
    )
  }
  let p : Parser = {
    side,
    limits,
    records: [],
    fields: [],
    field: StringBuilder(),
    field_units: 0,
    state: Start,
    line: 1,
    column: 1,
    previous_cr: false,
    pending_cr: false,
    touched: false,
  }
  let mut first = true
  for ch in text {
    if first {
      first = false
      if ch == '\uFEFF' {
        continue
      }
    }
    if p.pending_cr {
      if ch != '\n' {
        p.error("CSV_BARE_CR", "CR outside quotes must be followed by LF")
      }
      p.pending_cr = false
      p.end_record()
    } else {
      p.touched = true
      match p.state {
        Quoted => if ch == '"' { p.state = Closed } else { p.append(ch) }
        Start | Bare | Closed =>
          match ch {
            ',' => p.end_field()
            '\n' => p.end_record()
            '\r' => p.pending_cr = true
            '"' =>
              match p.state {
                Start => p.state = Quoted
                Closed => {
                  p.append('"')
                  p.state = Quoted
                }
                _ => p.error("CSV_QUOTE", "quote inside an unquoted field")
              }
            _ =>
              match p.state {
                Closed =>
                  p.error(
                    "CSV_AFTER_QUOTE", "unexpected character after closing quote",
                  )
                _ => {
                  p.append(ch)
                  p.state = Bare
                }
              }
          }
      }
    }
    if ch == '\r' {
      p.line += 1
      p.column = 1
      p.previous_cr = true
    } else if ch == '\n' {
      if !p.previous_cr {
        p.line += 1
      }
      p.column = 1
      p.previous_cr = false
    } else {
      p.column += 1
      p.previous_cr = false
    }
  }
  if p.pending_cr {
    p.error("CSV_BARE_CR", "CR outside quotes must be followed by LF")
  }
  if p.state is Quoted {
    p.error("CSV_UNCLOSED", "unterminated quoted field")
  }
  if p.touched {
    p.end_record()
  }
  if p.records.is_empty() {
    fail("EMPTY_INPUT", side, "CSV needs a header record")
  }
  let headers = p.records[0]
  let seen : Map[String, Bool] = Map([])
  for name in headers {
    if name.is_empty() {
      fail("EMPTY_HEADER", side, "header names must not be empty", record=1)
    }
    if seen.contains(name) {
      fail("DUPLICATE_HEADER", side, "duplicate header: \{name}", record=1)
    }
    seen[name] = true
  }
  let rows : Array[Array[String]] = []
  for i = 1; i < p.records.length(); i = i + 1 {
    rows.push(p.records[i])
  }
  { headers, rows, }
}