///|
pub(all) enum ReturnMode {
  Any
  First
  Last
  Single
} derive(Debug, Eq)

///|
/// Empty membership lists impose no restriction. All criteria are intersected.
pub(all) struct Selection {
  classifications : Array[Int32]
  source_ids : Array[Int32]
  min_intensity : Int32
  max_intensity : Int32
  return_mode : ReturnMode
  include_withheld : Bool
  gps_min : Double?
  gps_max : Double?
} derive(Debug)

///|
pub fn Selection::all() -> Selection {
  {
    classifications: [],
    source_ids: [],
    min_intensity: 0,
    max_intensity: 65535,
    return_mode: Any,
    include_withheld: true,
    gps_min: None,
    gps_max: None,
  }
}

///|
pub fn Selection::validate(self : Selection) -> Unit raise LasError {
  require(
    self.min_intensity >= 0 &&
    self.max_intensity <= 65535 &&
    self.min_intensity <= self.max_intensity,
    "invalid intensity interval",
  )
  for c in self.classifications {
    require(c >= 0 && c <= 255, "invalid selection class")
  }
  for s in self.source_ids {
    require(s >= 0 && s <= 65535, "invalid source selection")
  }
  if self.gps_min is Some(v) {
    require(finite(v), "nonfinite GPS minimum")
  }
  if self.gps_max is Some(v) {
    require(finite(v), "nonfinite GPS maximum")
  }
  if self.gps_min is Some(lo) {
    if self.gps_max is Some(hi) {
      require(lo <= hi, "reversed GPS interval")
    }
  }
}

///|
fn Selection::matches(self : Selection, p : Point) -> Bool {
  if !self.classifications.is_empty() &&
    !self.classifications.contains(p.classification) {
    return false
  }
  if !self.source_ids.is_empty() && !self.source_ids.contains(p.source_id) {
    return false
  }
  if p.intensity < self.min_intensity || p.intensity > self.max_intensity {
    return false
  }
  if !self.include_withheld && (p.flags & 4) != 0 {
    return false
  }
  let accepted = match self.return_mode {
    Any => true
    First => p.return_number == 1
    Last => p.return_number == p.returns
    Single => p.returns == 1
  }
  if !accepted {
    return false
  }
  if self.gps_min is Some(lo) {
    match p.gps_time {
      Some(t) => if t < lo { return false }
      None => return false
    }
  }
  if self.gps_max is Some(hi) {
    match p.gps_time {
      Some(t) => if t > hi { return false }
      None => return false
    }
  }
  true
}

///|
/// Stable-order selection retains every attribute and all opaque metadata.
pub fn Document::select(
  self : Document,
  selection : Selection,
  limits~ : Limits = Limits::default(),
) -> Document raise LasError {
  self.validate(limits~)
  selection.validate()
  self.with_points(self.points.filter(p => selection.matches(p)), limits~)
}

///|
pub extend ReturnMode with @debug.Debug::{to_repr}

///|
pub extend ReturnMode with Eq::{equal, not_equal}

///|
pub extend Selection with @debug.Debug::{to_repr}