///|
pub fn Grid::is_valid(self : Grid) -> Bool {
  self.rows > 0 &&
  self.cols > 0 &&
  self.values.length() == self.rows * self.cols &&
  self.lat_step != 0.0 &&
  self.lon_step != 0.0
}

///|
pub fn Grid::index(self : Grid, row : Int, col : Int) -> Int? {
  if row < 0 || col < 0 || row >= self.rows || col >= self.cols {
    None
  } else {
    Some(row * self.cols + col)
  }
}

///|
pub fn Grid::value_at(self : Grid, row : Int, col : Int) -> Double? {
  match self.index(row, col) {
    Some(i) => Some(self.values[i])
    None => None
  }
}

///|
fn grid_position(grid : Grid, point : GeoPoint) -> (Double, Double) {
  (
    (point.lat - grid.origin_lat) / grid.lat_step,
    (point.lon - grid.origin_lon) / grid.lon_step,
  )
}

///|
pub fn Grid::sample_nearest(self : Grid, point : GeoPoint) -> Double? {
  let (r, c) = grid_position(self, point)
  let row = r.round().to_int()
  let col = c.round().to_int()
  self.value_at(row, col)
}

///|
pub fn Grid::sample_bilinear(self : Grid, point : GeoPoint) -> Double? {
  let (r, c) = grid_position(self, point)
  let r0 = r.floor().to_int()
  let c0 = c.floor().to_int()
  if r0 < 0 || c0 < 0 || r0 >= self.rows || c0 >= self.cols {
    None
  } else if r0 == self.rows - 1 || c0 == self.cols - 1 {
    self.value_at(r0, c0)
  } else {
    let r1 = r0 + 1
    let c1 = c0 + 1
    match
      (
        self.value_at(r0, c0),
        self.value_at(r0, c1),
        self.value_at(r1, c0),
        self.value_at(r1, c1),
      ) {
      (Some(v00), Some(v01), Some(v10), Some(v11)) => {
        let tr = r - Double::from_int(r0)
        let tc = c - Double::from_int(c0)
        let a = lerp(v00, v01, tc)
        let b = lerp(v10, v11, tc)
        Some(lerp(a, b, tr))
      }
      _ => None
    }
  }
}

///|
pub fn make_grid(
  origin_lat~ : Double,
  origin_lon~ : Double,
  lat_step~ : Double,
  lon_step~ : Double,
  rows~ : Int,
  cols~ : Int,
  values~ : Array[Double],
) -> Grid {
  { origin_lat, origin_lon, lat_step, lon_step, rows, cols, values }
}

///|
pub fn profile_between(
  grid : Grid,
  start : GeoPoint,
  finish : GeoPoint,
  samples : Int,
) -> Array[ProfileSample] {
  if samples <= 0 {
    []
  } else if samples == 1 {
    match grid.sample_bilinear(start) {
      Some(value) => [{ point: start, distance_m: 0.0, value }]
      None => []
    }
  } else {
    let total_distance = haversine_distance_m(start, finish)
    let out : Array[ProfileSample] = []
    for i in 0..
          out.push({ point, distance_m: total_distance * t, value })
        None => ()
      }
    }
    out
  }
}