///|
pub(all) struct CsvBundle {
  csv : String
  metadata : Json
} derive(@debug.Debug, ToJson)

///|
fn csv_quote(value : String) -> String {
  if value.contains(",") ||
    value.contains("\"") ||
    value.contains("\n") ||
    value.contains("\r") {
    "\"" + value.replace_all(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
pub fn export_csv(spectrum : Spectrum) -> CsvBundle raise JcampError {
  checked_spectrum(spectrum)
  let out = StringBuilder()
  out.write_string("x,y\r\n")
  for point in spectrum.points {
    out.write_string(csv_quote(decimal(point.x)) + ",")
    if point.y is Some(y) {
      out.write_string(csv_quote(decimal(y)))
    }
    out.write_string("\r\n")
  }
  {
    csv: out.to_string(),
    metadata: {
      "schema": "moonjcamp/csv/1",
      "title": spectrum.title,
      "data_type": spectrum.data_type,
      "x_units": spectrum.x_units,
      "y_units": spectrum.y_units,
      "records": Json(spectrum.records),
    },
  }
}

///|
pub fn parse_csv(text : String) -> Array[Array[String]] raise JcampError {
  guard text.length() <= Limits::default().max_chars && !text.contains("\u0000") else {
    reject("CSV_LIMIT", "CSV exceeds budget or contains NUL")
  }
  let text = if text.has_prefix("\uFEFF") { text[1:].to_owned() } else { text }
  let rows : Array[Array[String]] = []
  let mut row : Array[String] = []
  let mut field = StringBuilder()
  let mut quoted = false
  let mut closed = false
  let mut started = false
  let mut i = 0
  let mut line = 1
  fn append_field() -> Unit raise JcampError {
    guard row.length() < 16 else {
      reject("CSV_COLUMNS", "CSV column budget exceeded", line~)
    }
    row.push(field.to_string())
    field = StringBuilder()
    closed = false
    started = false
  }
  fn append_row() -> Unit raise JcampError {
    append_field()
    guard rows.length() <= Limits::default().max_points else {
      reject("CSV_ROWS", "CSV row budget exceeded", line~)
    }
    rows.push(row)
    row = []
  }
  while i < text.length() {
    let c = text[i]
    if quoted {
      if c == '"' {
        if i + 1 < text.length() && text[i + 1] == '"' {
          field.write_string("\"")
          i += 2
          continue
        }
        quoted = false
        closed = true
      } else {
        field.write_string(text[i:i + 1].to_owned())
        if c == '\n' {
          line += 1
        }
      }
    } else if c == ',' {
      append_field()
    } else if c == '\r' || c == '\n' {
      append_row()
      if c == '\r' && i + 1 < text.length() && text[i + 1] == '\n' {
        i += 1
      }
      line += 1
    } else if c == '"' {
      guard !started && !closed else {
        reject("CSV_QUOTE", "quote inside an unquoted field", line~)
      }
      quoted = true
      started = true
    } else {
      guard !closed else {
        reject("CSV_QUOTE", "characters after closing quote", line~)
      }
      field.write_string(text[i:i + 1].to_owned())
      started = true
    }
    i += 1
  }
  guard !quoted else { reject("CSV_QUOTE", "unterminated quoted field", line~) }
  if started || closed || !row.is_empty() {
    append_row()
  }
  rows
}

///|
pub fn import_csv(text : String, metadata : Json) -> Spectrum raise JcampError {
  let fields = json_object(
    metadata,
    ["schema", "title", "data_type", "x_units", "y_units", "records"],
    "CSV metadata",
  )
  guard json_string(fields, "schema") == "moonjcamp/csv/1" else {
    reject("CSV_SCHEMA", "unsupported CSV sidecar schema")
  }
  let rows = parse_csv(text)
  guard rows.length() >= 2 && rows[0] == ["x", "y"] else {
    reject(
      "CSV_HEADER", "CSV requires exact x,y header and at least one data row",
    )
  }
  let points : Array[Json] = []
  for i = 1; i < rows.length(); i = i + 1 {
    let row = rows[i]
    guard row.length() == 2 else {
      reject(
        "CSV_ARITY",
        "each row must contain exactly two fields",
        line=i + 1,
      )
    }
    let y : Json = if trim(row[1]) == "" {
      Json::null()
    } else {
      Json(number(row[1], line=i + 1, column=2))
    }
    points.push({ "x": number(row[0], line=i + 1), "y": y })
  }
  spectrum_from_json({
    "title": json_string(fields, "title"),
    "data_type": json_string(fields, "data_type"),
    "x_units": json_string(fields, "x_units"),
    "y_units": json_string(fields, "y_units"),
    "records": fields.get("records").unwrap_or([]),
    "points": Json(points),
  })
}