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

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

///|
pub fn profile_to_geojson(
  profile : ArrayView[ProfileSample],
  name? : String = "profile",
) -> String {
  let coordinates : Array[String] = []
  for sample in profile {
    coordinates.push(
      "[\{sample.point.lon},\{sample.point.lat},\{sample.point.elevation_m}]",
    )
  }
  "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"geometry\":{\"type\":\"LineString\",\"coordinates\":[\{coordinates.join(",")}]},\"properties\":{\"name\":\"\{escape_json_string(name)}\",\"samples\":\{profile.length()}}}]}"
}

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

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

///|
pub fn samples_to_geojson_lines(
  features : ArrayView[PolylineFeature],
) -> String {
  let encoded : Array[String] = []
  for feature in features {
    let coordinates : Array[String] = []
    for point in feature.points {
      coordinates.push("[\{point.lon},\{point.lat},\{point.elevation_m}]")
    }
    encoded.push(
      "{\"type\":\"Feature\",\"geometry\":{\"type\":\"LineString\",\"coordinates\":[\{coordinates.join(",")}]},\"properties\":{\"name\":\"\{escape_json_string(feature.name)}\"}}",
    )
  }
  "{\"type\":\"FeatureCollection\",\"features\":[\{encoded.join(",")}] }".replace_all(
    old="] }",
    new="]}",
  )
}

///|
pub fn samples_filter_name(
  samples : ArrayView[FieldSample],
  name : String,
) -> Array[FieldSample] {
  samples.filter(fn(sample) { sample.name == name })
}

///|
pub fn samples_map_value(
  samples : ArrayView[FieldSample],
  transform : (Double) -> Double,
) -> Array[FieldSample] {
  let output : Array[FieldSample] = []
  for sample in samples {
    output.push({ ..sample, value: transform(sample.value) })
  }
  output
}

///|
pub fn samples_translate(
  samples : ArrayView[FieldSample],
  north_m : Double,
  east_m : Double,
) -> Array[FieldSample] {
  let output : Array[FieldSample] = []
  for sample in samples {
    output.push({
      ..sample,
      point: from_local_enu(sample.point, { x: east_m, y: north_m, z: 0.0 }),
    })
  }
  output
}

///|
pub fn samples_sort_by_distance(
  samples : ArrayView[FieldSample],
  origin : GeoPoint,
) -> Array[FieldSample] {
  let output = samples.to_owned()
  output.sort_by(fn(a, b) {
    haversine_distance_m(origin, a.point).compare(
      haversine_distance_m(origin, b.point),
    )
  })
  output
}

///|
pub fn samples_deduplicate(
  samples : ArrayView[FieldSample],
  tolerance_m : Double,
) -> Array[FieldSample] {
  let output : Array[FieldSample] = []
  for sample in samples {
    let mut duplicate = false
    for existing in output {
      if point_approximately_equal(sample.point, existing.point, tolerance_m) {
        duplicate = true
      }
    }
    if !duplicate {
      output.push(sample)
    }
  }
  output
}

///|
pub fn samples_bounds(samples : ArrayView[FieldSample]) -> GridExtent {
  let mut min_lat = 0.0
  let mut max_lat = 0.0
  let mut min_lon = 0.0
  let mut max_lon = 0.0
  let mut found = false
  for sample in samples {
    if valid_point(sample.point) {
      if !found {
        min_lat = sample.point.lat
        max_lat = sample.point.lat
        min_lon = sample.point.lon
        max_lon = sample.point.lon
        found = true
      } else {
        min_lat = Double::min(min_lat, sample.point.lat)
        max_lat = Double::max(max_lat, sample.point.lat)
        min_lon = Double::min(min_lon, sample.point.lon)
        max_lon = Double::max(max_lon, sample.point.lon)
      }
    }
  }
  { min_lat, max_lat, min_lon, max_lon }
}

///|
pub fn samples_to_field_series(
  samples : ArrayView[FieldSample],
  origin : GeoPoint,
  name? : String = "samples",
) -> FieldSeries {
  let ordered = samples_sort_by_distance(samples, origin)
  let points : Array[SeriesPoint] = []
  for sample in ordered {
    points.push({
      coordinate: haversine_distance_m(origin, sample.point),
      value: sample.value,
      flag: if is_finite(sample.value) {
        Good
      } else {
        Missing
      },
    })
  }
  { name, points }
}

///|
pub fn field_series_to_csv(series : FieldSeries) -> String {
  let rows : Array[String] = ["coordinate,value,flag"]
  for point in series.points {
    rows.push("\{point.coordinate},\{point.value},\{quality_name(point.flag)}")
  }
  rows.join("\n")
}