///|
fn csv_rows(text : String) -> Array[Array[String]] raise CodecError {
  if text.length() > 65536 {
    raise Invalid("CSV exceeds 65536 code units")
  }
  let chars = text.iter().to_array()
  let rows : Array[Array[String]] = []
  let mut row : Array[String] = []
  let mut cell = StringBuilder()
  let mut cell_size = 0
  let mut mode = 0 // 0=start, 1=unquoted, 2=quoted, 3=after closing quote
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if mode == 2 {
      if c == '"' {
        mode = 3
      } else {
        cell.write_char(c)
        cell_size += if c.to_int() > 65535 { 2 } else { 1 }
      }
    } else if mode == 3 && c == '"' {
      cell.write_char('"')
      cell_size += 1
      mode = 2
    } else if c == ',' || c == '\n' || c == '\r' {
      row.push(cell.to_string())
      cell = StringBuilder()
      cell_size = 0
      mode = 0
      if row.length() > 8 {
        raise Invalid("CSV has more than eight columns")
      }
      if c != ',' {
        rows.push(row)
        row = []
        if rows.length() > 29 {
          raise Invalid("CSV has more than 28 data rows")
        }
        if c == '\r' && i + 1 < chars.length() && chars[i + 1] == '\n' {
          i += 1
        }
      }
    } else if mode == 3 {
      raise Invalid("unexpected text after CSV closing quote")
    } else if c == '"' {
      if mode != 0 {
        raise Invalid("quote inside unquoted CSV field")
      }
      mode = 2
    } else {
      cell.write_char(c)
      cell_size += if c.to_int() > 65535 { 2 } else { 1 }
      mode = 1
    }
    if cell_size > 256 {
      raise Invalid("CSV field exceeds 256 code units")
    }
    i += 1
  }
  if mode == 2 {
    raise Invalid("unterminated CSV quote")
  }
  if mode != 0 || cell_size > 0 || row.length() > 0 {
    row.push(cell.to_string())
    rows.push(row)
  }
  if rows.length() > 29 {
    raise Invalid("CSV row bound exceeded")
  }
  rows
}

///|
fn csv_int(text : String) -> Int32 raise CodecError {
  if text == "-1" {
    return -1
  }
  if text.length() == 0 || text.length() > 7 {
    raise Invalid("invalid CSV integer")
  }
  let mut n = 0
  for c in text.iter() {
    if c < '0' || c > '9' {
      raise Invalid("invalid CSV decimal integer")
    }
    n = n * 10 + c.to_int() - 48
  }
  if n > 1000000 {
    raise Invalid("CSV integer outside bounded domain")
  }
  n
}

///|
fn csv_problem(
  text : String,
  kerf : Int32,
  min_remnant : Int32,
) -> Problem raise CodecError {
  let rows = csv_rows(text)
  let header = [
    "kind", "id", "material", "length", "quantity", "cost", "trim_start", "trim_end",
  ]
  if rows.length() == 0 || rows[0] != header {
    raise Invalid(
      "CSV header must be kind,id,material,length,quantity,cost,trim_start,trim_end",
    )
  }
  let ds : Array[Demand] = []
  let ss : Array[Stock] = []
  for i in 1.. p
    Err(e) => raise Invalid(e)
  }
}

///|
pub fn decode_csv(
  text : String,
  kerf~ : Int32 = 0,
  min_remnant~ : Int32 = 1,
) -> Result[Problem, String] {
  Ok(csv_problem(text, kerf, min_remnant)) catch {
    Invalid(e) => Err(e)
  }
}

///|
fn csv_quote(s : String) -> String {
  "\"" + s.replace(old="\"", new="\"\"") + "\""
}

///|
pub fn encode_csv(p : Problem) -> String {
  let b = StringBuilder()
  b.write_string(
    "kind,id,material,length,quantity,cost,trim_start,trim_end\r\n",
  )
  for d in p.demands {
    b.write_string(
      "demand," +
      csv_quote(d.id) +
      "," +
      csv_quote(d.material) +
      "," +
      d.length.to_string() +
      "," +
      d.quantity.to_string() +
      ",0,0,0\r\n",
    )
  }
  for s in p.stocks {
    b.write_string(
      "stock," +
      csv_quote(s.id) +
      "," +
      csv_quote(s.material) +
      "," +
      s.length.to_string() +
      "," +
      s.quantity.to_string() +
      "," +
      s.cost.to_string() +
      "," +
      s.trim_start.to_string() +
      "," +
      s.trim_end.to_string() +
      "\r\n",
    )
  }
  b.to_string()
}