///|
pub fn parse_csv_text(source : String) -> Array[Map[String, String]] {
  let lines = normalized_lines(source)
  if lines.is_empty() {
    []
  } else {
    let headers = parse_csv_line(lines[0])
    let rows = []
    for line in lines[1:] {
      if line.trim().to_owned() != "" {
        let cells = parse_csv_line(line)
        rows.push(row_from_headers(headers, cells))
      }
    }
    rows
  }
}

///|
pub fn read_csv_file(path : String) -> Array[Map[String, String]] {
  let source = @fs.read_file_to_string(path) catch {
    _ => abort("failed to read csv file: \{path}")
  }
  parse_csv_text(source)
}

///|
fn parse_csv_line(line : String) -> Array[String] {
  let cells = []
  let mut current = ""
  let mut in_quotes = false
  let mut skip_next = false
  let chars = line.to_array()
  for index, ch in chars {
    if skip_next {
      skip_next = false
    } else if ch == '"' {
      if in_quotes && index + 1 < chars.length() && chars[index + 1] == '"' {
        current = current + "\""
        skip_next = true
      } else {
        in_quotes = !in_quotes
      }
    } else if ch == ',' && !in_quotes {
      cells.push(current)
      current = ""
    } else {
      current = current + "\{ch}"
    }
  }
  cells.push(current)
  cells.map(fn(cell) { cell.trim().to_owned() })
}

///|
fn row_from_headers(
  headers : Array[String],
  cells : Array[String],
) -> Map[String, String] {
  let row : Map[String, String] = Map([])
  for index, header in headers {
    row[header] = if index < cells.length() { cells[index] } else { "" }
  }
  row
}

///|
fn normalized_lines(source : String) -> Array[String] {
  source
  .split("\r\n")
  .to_array()
  .join("\n")
  .split("\n")
  .to_array()
  .filter(fn(line) { line.trim().to_owned() != "" })
  .map(fn(line) { line.to_owned() })
}