///|
pub(all) struct FacetCount {
  value : String
  count : Int
} derive(Eq, @debug.Debug)

///|
pub struct RangeFacet {
  label : String
  lower : FieldValue?
  lower_inclusive : Bool
  upper : FieldValue?
  upper_inclusive : Bool
}

///|
pub fn RangeFacet::new(
  label : String,
  lower : FieldValue?,
  lower_inclusive : Bool,
  upper : FieldValue?,
  upper_inclusive : Bool,
) -> RangeFacet {
  { label, lower, lower_inclusive, upper, upper_inclusive }
}

///|
pub(all) struct RangeFacetCount {
  label : String
  count : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct HistogramBucket {
  key : Double
  count : Int
} derive(@debug.Debug)

///|
pub(all) struct NumericAggregation {
  count : Int
  min : Double?
  max : Double?
  sum : Double
  avg : Double?
  histogram : ReadOnlyArray[HistogramBucket]
} derive(@debug.Debug)

///|
fn facet_string(value : FieldValue) -> String? {
  match value {
    @core.Keyword(value) => Some(value)
    @core.Text(value) => Some(value)
    @core.Bool(value) => Some(if value { "true" } else { "false" })
    _ => None
  }
}

///|
pub fn Searcher::terms_facet(
  self : Searcher,
  query : &Query,
  field_id : FieldId,
) -> ReadOnlyArray[FacetCount] {
  let counts : Array[FacetCount] = []
  for hit in self.all_hits(query) {
    let segment = self.segments[hit.address.segment_ord].segment
    let seen : Array[String] = []
    for value in segment.fast_values(field_id, hit.address.doc_id) {
      match facet_string(value) {
        Some(value) if seen.search_by(item => item == value) is None => {
          seen.push(value)
          match counts.search_by(entry => entry.value == value) {
            Some(index) =>
              counts[index] = { value, count: counts[index].count + 1 }
            None => counts.push({ value, count: 1 })
          }
        }
        _ => ()
      }
    }
  }
  counts.sort_by((left, right) => {
    if left.count != right.count {
      right.count.compare(left.count)
    } else {
      left.value.compare(right.value)
    }
  })
  ReadOnlyArray::from_array(counts)
}

///|
fn range_contains(range : RangeFacet, value : FieldValue) -> Bool {
  let lower_matches = match range.lower {
    None => true
    Some(lower) =>
      match compare_field_values(value, lower) {
        Some(order) => order > 0 || (range.lower_inclusive && order == 0)
        None => false
      }
  }
  let upper_matches = match range.upper {
    None => true
    Some(upper) =>
      match compare_field_values(value, upper) {
        Some(order) => order < 0 || (range.upper_inclusive && order == 0)
        None => false
      }
  }
  lower_matches && upper_matches
}

///|
pub fn Searcher::range_facet(
  self : Searcher,
  query : &Query,
  field_id : FieldId,
  ranges : ReadOnlyArray[RangeFacet],
) -> ReadOnlyArray[RangeFacetCount] {
  let counts : Array[RangeFacetCount] = []
  for range in ranges {
    counts.push({ label: range.label, count: 0 })
  }
  for hit in self.all_hits(query) {
    let segment = self.segments[hit.address.segment_ord].segment
    let values = segment.fast_values(field_id, hit.address.doc_id)
    for range_index in 0.. range_contains(ranges[range_index], value))
        is Some(_) {
        counts[range_index] = {
          label: counts[range_index].label,
          count: counts[range_index].count + 1,
        }
      }
    }
  }
  ReadOnlyArray::from_array(counts)
}

///|
pub fn Searcher::aggregate(
  self : Searcher,
  query : &Query,
  field_id : FieldId,
  histogram_interval : Double?,
) -> NumericAggregation {
  match histogram_interval {
    Some(interval) => {
      guard interval > 0.0 else { abort("histogram interval must be positive") }
    }
    None => ()
  }
  let mut count = 0
  let mut minimum : Double? = None
  let mut maximum : Double? = None
  let mut sum = 0.0
  let histogram : Array[HistogramBucket] = []
  for hit in self.all_hits(query) {
    let segment = self.segments[hit.address.segment_ord].segment
    for value in segment.fast_values(field_id, hit.address.doc_id) {
      match field_value_as_double(value) {
        Some(number) => {
          count += 1
          sum += number
          minimum = match minimum {
            Some(current) =>
              Some(if number < current { number } else { current })
            None => Some(number)
          }
          maximum = match maximum {
            Some(current) =>
              Some(if number > current { number } else { current })
            None => Some(number)
          }
          match histogram_interval {
            Some(interval) => {
              let key = @math.floor(number / interval) * interval
              match histogram.search_by(bucket => bucket.key == key) {
                Some(index) =>
                  histogram[index] = { key, count: histogram[index].count + 1 }
                None => histogram.push({ key, count: 1 })
              }
            }
            None => ()
          }
        }
        None => ()
      }
    }
  }
  histogram.sort_by((left, right) => {
    if left.key < right.key {
      -1
    } else if left.key > right.key {
      1
    } else {
      0
    }
  })
  {
    count,
    min: minimum,
    max: maximum,
    sum,
    avg: if count == 0 {
      None
    } else {
      Some(sum / count.to_double())
    },
    histogram: ReadOnlyArray::from_array(histogram),
  }
}