///|
fn parse_cells(line : String, separator : String) -> Array[String] {
  let output : Array[String] = []
  for cell in line.split(separator) {
    output.push(cell.trim().to_owned())
  }
  output
}

///|
pub fn parse_field_samples_tsv(text : String) -> Array[FieldSample] raise {
  let output : Array[FieldSample] = []
  for raw in text.split("\n") {
    let line = raw.trim()
    if line.is_empty() || line.has_prefix("lat") || line.has_prefix("latitude") {
      ()
    } else {
      let cells = parse_cells(line.to_owned(), "\t")
      if cells.length() >= 4 {
        let lat = parse_double_cell(cells[0])
        let lon = parse_double_cell(cells[1])
        let elevation_m = parse_double_cell(cells[2])
        let value = parse_double_cell(cells[3])
        let name = if cells.length() > 4 { cells[4] } else { "sample" }
        output.push({ point: { lat, lon, elevation_m }, value, name })
      }
    }
  }
  output
}

///|
/// Parse whitespace-separated XYZ rows: longitude latitude value, optionally elevation.
pub fn parse_xyz_samples(
  text : String,
  name? : String = "xyz",
) -> Array[FieldSample] raise {
  let output : Array[FieldSample] = []
  for raw in text.split("\n") {
    let line = raw.trim()
    if line.is_empty() || line.has_prefix("#") {
      ()
    } else {
      let cells : Array[String] = []
      for cell in line.split(" ") {
        let value = cell.trim()
        if !value.is_empty() {
          cells.push(value.to_owned())
        }
      }
      if cells.length() >= 3 {
        let lon = parse_double_cell(cells[0])
        let lat = parse_double_cell(cells[1])
        let elevation_m = if cells.length() >= 4 {
          parse_double_cell(cells[3])
        } else {
          0.0
        }
        let value = parse_double_cell(cells[2])
        output.push({ point: { lat, lon, elevation_m }, value, name })
      }
    }
  }
  output
}

///|
pub fn samples_to_xyz(samples : ArrayView[FieldSample]) -> String {
  let rows : Array[String] = []
  for sample in samples {
    rows.push(
      "\{sample.point.lon} \{sample.point.lat} \{sample.value} \{sample.point.elevation_m}",
    )
  }
  rows.join("\n")
}

///|
pub fn grid_to_json(grid : Grid, name : String) -> String {
  let values : Array[String] = []
  for value in grid.values {
    values.push(if is_finite(value) { "\{value}" } else { "null" })
  }
  let encoded_name = escape_json_string(name)
  let values_text = values.join(",")
  "{\"type\":\"regular-grid\",\"name\":\"\{encoded_name}\",\"origin_lat\":\{grid.origin_lat},\"origin_lon\":\{grid.origin_lon},\"lat_step\":\{grid.lat_step},\"lon_step\":\{grid.lon_step},\"rows\":\{grid.rows},\"cols\":\{grid.cols},\"values\":[\{values_text}]}"
}

///|
pub fn grid_metadata_csv(grid : Grid) -> String {
  "origin_lat,origin_lon,lat_step,lon_step,rows,cols\n\{grid.origin_lat},\{grid.origin_lon},\{grid.lat_step},\{grid.lon_step},\{grid.rows},\{grid.cols}"
}

///|
pub fn parse_grid_values_csv(
  text : String,
  origin_lat : Double,
  origin_lon : Double,
  lat_step : Double,
  lon_step : Double,
  rows : Int,
  cols : Int,
) -> Grid raise {
  let values : Array[Double] = []
  for raw in text.split("\n") {
    let line = raw.trim()
    if line.is_empty() || line.has_prefix("row") || line.has_prefix("lat") {
      ()
    } else {
      let cells = parse_cells(line.to_owned(), ",")
      let value_cell = if cells.length() >= 5 {
        cells[4]
      } else {
        cells.last().unwrap_or("")
      }
      if !value_cell.is_empty() {
        values.push(parse_double_cell(value_cell))
      }
    }
  }
  make_grid(
    origin_lat~,
    origin_lon~,
    lat_step~,
    lon_step~,
    rows~,
    cols~,
    values~,
  )
}

///|
pub fn grid_to_matrix_rows(grid : Grid) -> Array[Array[Double]] {
  let output : Array[Array[Double]] = []
  for row in 0.. Grid {
  let values : Array[Double] = []
  let cols = if rows.length() == 0 { 0 } else { rows[0].length() }
  for row in rows {
    for col in 0.. String {
  let rows : Array[String] = ["distance_m\tlat\tlon\televation_m\tvalue"]
  for sample in profile {
    rows.push(
      "\{sample.distance_m}\t\{sample.point.lat}\t\{sample.point.lon}\t\{sample.point.elevation_m}\t\{sample.value}",
    )
  }
  rows.join("\n")
}

///|
pub fn series_to_json(series : FieldSeries) -> String {
  let points : Array[String] = []
  for point in series.points {
    let value = if is_finite(point.value) { "\{point.value}" } else { "null" }
    points.push(
      "{\"coordinate\":\{point.coordinate},\"value\":\{value},\"flag\":\"\{quality_name(point.flag)}\"}",
    )
  }
  let series_name = escape_json_string(series.name)
  let point_text = points.join(",")
  "{\"name\":\"\{series_name}\",\"points\":[\{point_text}]}"
}

///|
pub fn parse_series_csv(
  text : String,
  name? : String = "series",
) -> FieldSeries raise {
  let points : Array[SeriesPoint] = []
  for raw in text.split("\n") {
    let line = raw.trim()
    if line.is_empty() || line.has_prefix("coordinate") {
      ()
    } else {
      let cells = parse_cells(line.to_owned(), ",")
      if cells.length() >= 2 {
        let coordinate = parse_double_cell(cells[0])
        let value = parse_double_cell(cells[1])
        points.push({
          coordinate,
          value,
          flag: if is_finite(value) {
            Good
          } else {
            Missing
          },
        })
      }
    }
  }
  { name, points }
}

///|
pub fn parse_series_tsv(
  text : String,
  name? : String = "series",
) -> FieldSeries raise {
  let points : Array[SeriesPoint] = []
  for raw in text.split("\n") {
    let line = raw.trim()
    if line.is_empty() || line.has_prefix("coordinate") {
      ()
    } else {
      let cells = parse_cells(line.to_owned(), "\t")
      if cells.length() >= 2 {
        let coordinate = parse_double_cell(cells[0])
        let value = parse_double_cell(cells[1])
        points.push({ coordinate, value, flag: Good })
      }
    }
  }
  { name, points }
}

///|
pub fn format_double_table(
  values : ArrayView[Double],
  separator : String,
) -> String {
  let cells : Array[String] = []
  for value in values {
    cells.push(if is_finite(value) { "\{value}" } else { "NaN" })
  }
  cells.join(separator)
}

///|
pub fn csv_header(columns : ArrayView[String]) -> String {
  let output : Array[String] = []
  for column in columns {
    output.push(csv_escape(column))
  }
  output.join(",")
}

///|
pub fn csv_row(values : ArrayView[String]) -> String {
  let output : Array[String] = []
  for value in values {
    output.push(csv_escape(value))
  }
  output.join(",")
}

///|
pub fn samples_to_csv_safe(samples : ArrayView[FieldSample]) -> String {
  let rows : Array[String] = [
    csv_header(["lat", "lon", "elevation_m", "value", "name"]),
  ]
  for sample in samples {
    rows.push(
      csv_row([
        "\{sample.point.lat}",
        "\{sample.point.lon}",
        "\{sample.point.elevation_m}",
        "\{sample.value}",
        sample.name,
      ]),
    )
  }
  rows.join("\n")
}

///|
pub fn grid_to_csv_safe(grid : Grid) -> String {
  let rows : Array[String] = [csv_header(["row", "col", "lat", "lon", "value"])]
  for cell in grid.cells() {
    rows.push(
      csv_row([
        "\{cell.row}",
        "\{cell.col}",
        "\{cell.point.lat}",
        "\{cell.point.lon}",
        "\{cell.value}",
      ]),
    )
  }
  rows.join("\n")
}

///|
pub fn export_samples_bundle(
  samples : ArrayView[FieldSample],
  name : String,
) -> String {
  let encoded_name = escape_json_string(name)
  let encoded_csv = escape_json_string(samples_to_csv_safe(samples))
  let geojson = samples_to_geojson(samples)
  "{\"name\":\"" +
  encoded_name +
  "\",\"count\":" +
  "\{samples.length()}" +
  ",\"csv\":\"" +
  encoded_csv +
  "\",\"geojson\":" +
  geojson +
  "}"
}