///|
fn safe_difference(a : Double, b : Double, distance : Double) -> Double {
  if distance == 0.0 {
    0.0
  } else {
    (a - b) / distance
  }
}

///|
pub fn Grid::gradient_at(self : Grid, row : Int, col : Int) -> Gradient? {
  match self.value_at(row, col) {
    None => None
    Some(center) => {
      let north = match self.value_at(row - 1, col) {
        Some(value) => value
        None => center
      }
      let south = match self.value_at(row + 1, col) {
        Some(value) => value
        None => center
      }
      let west = match self.value_at(row, col - 1) {
        Some(value) => value
        None => center
      }
      let east = match self.value_at(row, col + 1) {
        Some(value) => value
        None => center
      }
      let north_rate = safe_difference(south, north, 2.0 * self.lat_step)
      let east_rate = safe_difference(east, west, 2.0 * self.lon_step)
      let magnitude = (north_rate * north_rate + east_rate * east_rate).sqrt()
      let azimuth = rad_to_deg(@math.atan2(east_rate, north_rate))
      Some({
        north_per_degree: north_rate,
        east_per_degree: east_rate,
        magnitude,
        azimuth_deg: normalize_longitude(azimuth),
      })
    }
  }
}

///|
pub fn Grid::gradient(self : Grid) -> Grid {
  self.map_cells(fn(cell) {
    match self.gradient_at(cell.row, cell.col) {
      Some(value) => value.magnitude
      None => 0.0
    }
  })
}

///|
pub fn Grid::slope_degrees(self : Grid, row : Int, col : Int) -> Double? {
  match self.gradient_at(row, col) {
    Some(value) => Some(rad_to_deg(@math.atan(value.magnitude)))
    None => None
  }
}

///|
pub fn Grid::aspect_degrees(self : Grid, row : Int, col : Int) -> Double? {
  match self.gradient_at(row, col) {
    Some(value) => Some(value.azimuth_deg)
    None => None
  }
}

///|
pub fn Grid::laplacian_at(self : Grid, row : Int, col : Int) -> Double? {
  match self.value_at(row, col) {
    None => None
    Some(center) => {
      let north = match self.value_at(row - 1, col) {
        Some(v) => v
        None => center
      }
      let south = match self.value_at(row + 1, col) {
        Some(v) => v
        None => center
      }
      let west = match self.value_at(row, col - 1) {
        Some(v) => v
        None => center
      }
      let east = match self.value_at(row, col + 1) {
        Some(v) => v
        None => center
      }
      Some(north + south + west + east - 4.0 * center)
    }
  }
}

///|
pub fn Grid::laplacian(self : Grid) -> Grid {
  self.map_cells(fn(cell) {
    match self.laplacian_at(cell.row, cell.col) {
      Some(value) => value
      None => 0.0
    }
  })
}

///|
pub fn Grid::hillshade(self : Grid, illumination_deg : Double) -> Grid {
  self.map_cells(fn(cell) {
    match self.gradient_at(cell.row, cell.col) {
      None => 0.0
      Some(gradient) => {
        let difference = deg_to_rad(illumination_deg - gradient.azimuth_deg)
        @math.cos(deg_to_rad(45.0)) * @math.cos(deg_to_rad(45.0)) +
        @math.sin(deg_to_rad(45.0)) *
        @math.sin(deg_to_rad(45.0)) *
        @math.cos(difference)
      }
    }
  })
}