///|
pub fn Grid::downsample_mean(self : Grid, factor : Int) -> Grid {
  let safe = Int::max(1, factor)
  let rows = (self.rows + safe - 1) / safe
  let cols = (self.cols + safe - 1) / safe
  let values : Array[Double] = []
  for row in 0.. if is_finite(value) { window.push(value) }
            None => ()
          }
        }
      }
      values.push(summary(window).mean)
    }
  }
  {
    origin_lat: self.origin_lat,
    origin_lon: self.origin_lon,
    lat_step: self.lat_step * Double::from_int(safe),
    lon_step: self.lon_step * Double::from_int(safe),
    rows,
    cols,
    values,
  }
}

///|
pub fn Grid::downsample_min(self : Grid, factor : Int) -> Grid {
  self.downsample_with(factor, fn(values) { summary(values).minimum })
}

///|
pub fn Grid::downsample_max(self : Grid, factor : Int) -> Grid {
  self.downsample_with(factor, fn(values) { summary(values).maximum })
}

///|
pub fn Grid::downsample_with(
  self : Grid,
  factor : Int,
  reducer : (ArrayView[Double]) -> Double,
) -> Grid {
  let safe = Int::max(1, factor)
  let rows = (self.rows + safe - 1) / safe
  let cols = (self.cols + safe - 1) / safe
  let values : Array[Double] = []
  for row in 0.. if is_finite(value) { window.push(value) }
            None => ()
          }
        }
      }
      values.push(reducer(window))
    }
  }
  {
    origin_lat: self.origin_lat,
    origin_lon: self.origin_lon,
    lat_step: self.lat_step * Double::from_int(safe),
    lon_step: self.lon_step * Double::from_int(safe),
    rows,
    cols,
    values,
  }
}

///|
pub fn Grid::pyramid(self : Grid, levels : Int) -> Array[Grid] {
  let output : Array[Grid] = [self]
  let mut current = self
  for _ in 1.. Array[Grid] {
  let output : Array[Grid] = []
  let safe_rows = Int::max(1, tile_rows)
  let safe_cols = Int::max(1, tile_cols)
  for row in 0.. Int? {
  if row < 0 || col < 0 || row >= self.rows || col >= self.cols {
    None
  } else {
    Some(
      row /
      Int::max(1, tile_rows) *
      ((self.cols + Int::max(1, tile_cols) - 1) / Int::max(1, tile_cols)) +
      col / Int::max(1, tile_cols),
    )
  }
}

///|
pub fn Grid::interior(self : Grid, border : Int) -> Grid {
  self.crop(
    border,
    border,
    Int::max(0, self.rows - 2 * border),
    Int::max(0, self.cols - 2 * border),
  )
}

///|
pub fn Grid::boundary(self : Grid, border : Int) -> Grid {
  self.pad(border, 0.0).difference(self).unwrap_or(self)
}

///|
pub fn Grid::cell_centres(self : Grid) -> Array[GeoPoint] {
  let output : Array[GeoPoint] = []
  for row in 0.. output.push(point)
        None => ()
      }
    }
  }
  output
}

///|
pub fn grid_mosaic(grids : ArrayView[Grid]) -> Grid? {
  if grids.length() == 0 {
    None
  } else {
    let first = grids[0]
    let mut output = first
    for grid in grids[1:] {
      match
        output.combine(grid, fn(a, b) { if is_finite(b) { b } else { a } }) {
        Some(combined) => output = combined
        None => ()
      }
    }
    Some(output)
  }
}

///|
pub fn grid_match_extent(a : Grid, b : Grid) -> Bool {
  a.rows == b.rows &&
  a.cols == b.cols &&
  a.origin_lat == b.origin_lat &&
  a.origin_lon == b.origin_lon &&
  a.lat_step == b.lat_step &&
  a.lon_step == b.lon_step
}

///|
pub fn grid_residual(a : Grid, b : Grid) -> Grid? {
  if grid_match_extent(a, b) {
    a.difference(b)
  } else {
    None
  }
}

///|
pub fn grid_rmse(a : Grid, b : Grid) -> Double {
  match grid_residual(a, b) {
    Some(residual) => rms(residual.values)
    None => 0.0
  }
}

///|
pub fn grid_mae(a : Grid, b : Grid) -> Double {
  match grid_residual(a, b) {
    Some(residual) => residual.mean_absolute()
    None => 0.0
  }
}

///|
pub fn grid_correlation(a : Grid, b : Grid) -> Double {
  if grid_match_extent(a, b) {
    covariance_summary(a.values, b.values).correlation
  } else {
    0.0
  }
}

///|
pub fn grid_apply_mask(
  source : Grid,
  mask : Grid,
  accepted_value : Double,
) -> Grid {
  source
  .combine(mask, fn(value, flag) {
    if flag == accepted_value {
      value
    } else {
      0.0 / 0.0
    }
  })
  .unwrap_or(source)
}

///|
pub fn grid_percentile(grid : Grid, fraction : Double) -> Double {
  percentile(grid.values, fraction)
}

///|
pub fn grid_standardize(grid : Grid) -> Grid {
  { ..grid, values: normalize_values(grid.values) }
}