///|
pub fn ProductQueryPlan::new() -> ProductQueryPlan {
  {
    expression: MatchAll,
    sort: ById,
    descending: false,
    offset: 0,
    limit: 0,
    include_empty_geometry: false,
  }
}

///|
pub fn ProductQueryPlan::where_(
  self : ProductQueryPlan,
  expression : ProductExpression,
) -> ProductQueryPlan {
  { ..self, expression, }
}

///|
pub fn ProductQueryPlan::where_predicate(
  self : ProductQueryPlan,
  predicate : ProductPredicate,
) -> ProductQueryPlan {
  { ..self, expression: Predicate(predicate) }
}

///|
pub fn ProductQueryPlan::sorted_by(
  self : ProductQueryPlan,
  sort : ProductSort,
  descending? : Bool = false,
) -> ProductQueryPlan {
  { ..self, sort, descending }
}

///|
pub fn ProductQueryPlan::page(
  self : ProductQueryPlan,
  offset : Int,
  limit : Int,
) -> ProductQueryPlan {
  {
    ..self,
    offset: if offset < 0 {
      0
    } else {
      offset
    },
    limit: if limit < 0 {
      0
    } else {
      limit
    },
  }
}

///|
pub fn ProductQueryPlan::including_empty_geometry(
  self : ProductQueryPlan,
  value : Bool,
) -> ProductQueryPlan {
  { ..self, include_empty_geometry: value }
}

///|
pub fn ProductQueryPlan::expression(
  self : ProductQueryPlan,
) -> ProductExpression {
  self.expression
}

///|
pub fn ProductQueryPlan::sort_key(self : ProductQueryPlan) -> ProductSort {
  self.sort
}

///|
pub fn ProductQueryPlan::is_unbounded(self : ProductQueryPlan) -> Bool {
  self.limit == 0
}

///|
pub fn ProductExpression::all(
  expressions : Array[ProductExpression],
) -> ProductExpression {
  if expressions.length() == 0 {
    MatchAll
  } else {
    AllOf(expressions)
  }
}

///|
pub fn ProductExpression::any(
  expressions : Array[ProductExpression],
) -> ProductExpression {
  if expressions.length() == 0 {
    MatchNone
  } else {
    AnyOf(expressions)
  }
}

///|
pub fn ProductExpression::not(self : ProductExpression) -> ProductExpression {
  Not(self)
}

///|
pub fn ProductExpression::and_(
  self : ProductExpression,
  other : ProductExpression,
) -> ProductExpression {
  match (self, other) {
    (MatchNone, _) | (_, MatchNone) => MatchNone
    (MatchAll, right) => right
    (left, MatchAll) => left
    (AllOf(left), AllOf(right)) => AllOf(left + right)
    (AllOf(left), right) => AllOf(left + [right])
    (left, AllOf(right)) => AllOf([left] + right)
    (left, right) => AllOf([left, right])
  }
}

///|
pub fn ProductExpression::or_(
  self : ProductExpression,
  other : ProductExpression,
) -> ProductExpression {
  match (self, other) {
    (MatchAll, _) | (_, MatchAll) => MatchAll
    (MatchNone, right) => right
    (left, MatchNone) => left
    (AnyOf(left), AnyOf(right)) => AnyOf(left + right)
    (AnyOf(left), right) => AnyOf(left + [right])
    (left, AnyOf(right)) => AnyOf([left] + right)
    (left, right) => AnyOf([left, right])
  }
}

///|
fn query_contains(text : String, fragment : String) -> Bool {
  text.to_lower().contains(fragment.to_lower())
}

///|
fn query_product_properties(
  index : ModelIndex,
  product : Product,
) -> Array[PropertySet] raise ParseError {
  property_sets_for_product(index, product)
}

///|
fn query_product_has_property(
  index : ModelIndex,
  product : Product,
  name : String,
) -> Bool raise ParseError {
  property_named(query_product_properties(index, product), name) is Some(_)
}

///|
fn query_product_property_text(
  index : ModelIndex,
  product : Product,
  name : String,
  value : String,
) -> Bool raise ParseError {
  match property_string(query_product_properties(index, product), name) {
    Some(text) => query_contains(text, value)
    None => false
  }
}

///|
fn query_compare_number(
  actual : Double,
  comparison : NumberComparison,
  expected : Double,
  upper : Double?,
) -> Bool {
  match comparison {
    LessThan => actual < expected
    LessOrEqual => actual <= expected
    EqualTo => (actual - expected).abs() <= 0.0000001
    GreaterOrEqual => actual >= expected
    GreaterThan => actual > expected
    Between => actual >= expected && actual <= upper.unwrap_or(expected)
  }
}

///|
fn query_product_property_number(
  index : ModelIndex,
  product : Product,
  name : String,
  comparison : NumberComparison,
  expected : Double,
  upper : Double?,
) -> Bool raise ParseError {
  match property_number(query_product_properties(index, product), name) {
    Some(actual) => query_compare_number(actual, comparison, expected, upper)
    None => false
  }
}

///|
fn query_product_geometry_kind(
  index : ModelIndex,
  product : Product,
  wanted : GeometryKind,
) -> Bool {
  geometries_for_product(index, product).any(geometry => geometry.kind == wanted)
}

///|
fn query_product_quantity(
  index : ModelIndex,
  product : Product,
  kind : QuantityKind,
) -> Double raise ParseError {
  let row = quantity_for_product(index, product, default_takeoff_options())
  row.value(kind)
}

///|
fn query_predicate(
  index : ModelIndex,
  product : Product,
  predicate : ProductPredicate,
  include_empty_geometry : Bool,
) -> Bool raise ParseError {
  match predicate {
    ProductKind(kind) => product.kind == kind
    EntityType(name) => product.ifc_type.to_upper() == name.to_upper()
    EntityTypePrefix(prefix) =>
      product.ifc_type.to_upper().has_prefix(prefix.to_upper())
    NameContains(fragment) =>
      query_contains(product.name.unwrap_or(""), fragment)
    DescriptionContains(fragment) =>
      query_contains(product.description.unwrap_or(""), fragment)
    GlobalId(value) => query_contains(product.global_id.unwrap_or(""), value)
    Tag(value) => query_contains(product.tag.unwrap_or(""), value)
    InContainer(id) => product.container == Some(id)
    HasProperties => product.property_sets.length() > 0
    HasGeometry => {
      let geometries = geometries_for_product(index, product)
      include_empty_geometry ||
      geometries.any(geometry => {
        geometry.points.length() > 0 ||
        geometry.triangle_count > 0 ||
        geometry.depth is Some(_)
      })
    }
    PropertyExists(name) => query_product_has_property(index, product, name)
    PropertyText(name, value) =>
      query_product_property_text(index, product, name, value)
    PropertyNumber(name, comparison, expected, upper) =>
      query_product_property_number(
        index, product, name, comparison, expected, upper,
      )
    IdRange(first, last) => product.id >= first && product.id <= last
    GeometryKind(kind) => query_product_geometry_kind(index, product, kind)
    Quantity(kind, comparison, expected, upper) =>
      query_compare_number(
        query_product_quantity(index, product, kind),
        comparison,
        expected,
        upper,
      )
  }
}

///|
fn query_expression(
  index : ModelIndex,
  product : Product,
  expression : ProductExpression,
  include_empty_geometry : Bool,
) -> Bool raise ParseError {
  match expression {
    MatchAll => true
    MatchNone => false
    Predicate(predicate) =>
      query_predicate(index, product, predicate, include_empty_geometry)
    Not(inner) =>
      !query_expression(index, product, inner, include_empty_geometry)
    AllOf(expressions) => {
      for expression in expressions {
        if !query_expression(index, product, expression, include_empty_geometry) {
          return false
        }
      }
      true
    }
    AnyOf(expressions) => {
      for expression in expressions {
        if query_expression(index, product, expression, include_empty_geometry) {
          return true
        }
      }
      false
    }
  }
}

///|
fn query_kind_sort(product : Product) -> String {
  product_kind_name(product.kind)
}

///|
fn query_product_name_sort(product : Product) -> String {
  product.name.unwrap_or("").to_lower()
}

///|
fn query_product_sort_value(
  index : ModelIndex,
  product : Product,
  sort : ProductSort,
) -> String raise ParseError {
  match sort {
    ById => product.id.to_string()
    ByType => product.ifc_type.to_upper()
    ByName => query_product_name_sort(product)
    ByKind => query_kind_sort(product)
    ByQuantity(kind) => query_product_quantity(index, product, kind).to_string()
    ByDistance(point) => {
      let bounds = model_bounds(index, [product])
      box_distance(bounds, { min: point, max: point, empty: false }).to_string()
    }
  }
}

///|
fn query_product_precedes(
  index : ModelIndex,
  left : Product,
  right : Product,
  sort : ProductSort,
  descending : Bool,
) -> Bool raise ParseError {
  let left_value = query_product_sort_value(index, left, sort)
  let right_value = query_product_sort_value(index, right, sort)
  if left_value == right_value {
    left.id < right.id
  } else if descending {
    left_value > right_value
  } else {
    left_value < right_value
  }
}

///|
fn query_sort_products(
  index : ModelIndex,
  products : Array[Product],
  plan : ProductQueryPlan,
) -> Array[Product] raise ParseError {
  let result = products.copy()
  for i in 0.. ProductQueryPage raise ParseError {
  let index = model.index()
  let matched : Array[Product] = []
  let mut examined = 0
  for product in model.products() {
    examined += 1
    if query_expression(
        index,
        product,
        plan.expression,
        plan.include_empty_geometry,
      ) {
      matched.push(product)
    }
  }
  let sorted = query_sort_products(index, matched, plan)
  let total = sorted.length()
  let offset = if plan.offset > total { total } else { plan.offset }
  let limit = if plan.limit == 0 { total - offset } else { plan.limit }
  let end = if offset + limit > total { total } else { offset + limit }
  let products = if end <= offset { [] } else { sorted[offset:end].to_owned() }
  {
    products,
    total_matches: total,
    offset,
    limit,
    has_next: end < total,
    examined,
  }
}

///|
pub fn query_plan(model : Model) -> ProductQueryPlan {
  ignore(model)
  ProductQueryPlan::new()
}

///|
pub fn ProductQueryPage::is_empty(self : ProductQueryPage) -> Bool {
  self.products.length() == 0
}

///|
pub fn ProductQueryPage::count(self : ProductQueryPage) -> Int {
  self.products.length()
}

///|
pub fn ProductQueryPage::ids(self : ProductQueryPage) -> Array[Int] {
  self.products.map(product => product.id)
}

///|
pub fn ProductQueryPage::next_offset(self : ProductQueryPage) -> Int? {
  if self.has_next {
    Some(self.offset + self.limit)
  } else {
    None
  }
}

///|
pub fn ProductQueryPage::headline(self : ProductQueryPage) -> String {
  "matches=" +
  self.total_matches.to_string() +
  " returned=" +
  self.products.length().to_string() +
  " offset=" +
  self.offset.to_string() +
  " has_next=" +
  self.has_next.to_string()
}

///|
pub fn ProductQueryPage::to_json(self : ProductQueryPage) -> String {
  "{\"total_matches\":" +
  self.total_matches.to_string() +
  ",\"offset\":" +
  self.offset.to_string() +
  ",\"limit\":" +
  self.limit.to_string() +
  ",\"has_next\":" +
  self.has_next.to_string() +
  ",\"examined\":" +
  self.examined.to_string() +
  ",\"products\":[" +
  self.products.map(product => product_to_json(product)).join(",") +
  "]}"
}

///|
fn query_predicate_text(predicate : ProductPredicate) -> String {
  match predicate {
    ProductKind(kind) => "kind=" + product_kind_name(kind)
    EntityType(name) => "type=" + name
    EntityTypePrefix(prefix) => "type_prefix=" + prefix
    NameContains(value) => "name~" + value
    DescriptionContains(value) => "description~" + value
    GlobalId(value) => "global_id=" + value
    Tag(value) => "tag=" + value
    InContainer(id) => "container=#" + id.to_string()
    HasProperties => "has_properties"
    HasGeometry => "has_geometry"
    PropertyExists(name) => "property=" + name
    PropertyText(name, value) => "property_text=" + name + "~" + value
    PropertyNumber(name, comparison, expected, upper) => {
      let upper_text = match upper {
        Some(value) => ".." + value.to_string()
        None => ""
      }
      "property_number=" +
      name +
      " " +
      query_number_comparison_text(comparison) +
      " " +
      expected.to_string() +
      upper_text
    }
    IdRange(first, last) => "id=" + first.to_string() + ".." + last.to_string()
    GeometryKind(kind) => "geometry=" + geometry_kind_text(kind)
    Quantity(kind, comparison, expected, upper) => {
      let upper_text = match upper {
        Some(value) => ".." + value.to_string()
        None => ""
      }
      "quantity=" +
      quantity_kind_name(kind) +
      " " +
      query_number_comparison_text(comparison) +
      " " +
      expected.to_string() +
      upper_text
    }
  }
}

///|
fn query_number_comparison_text(comparison : NumberComparison) -> String {
  match comparison {
    LessThan => "<"
    LessOrEqual => "<="
    EqualTo => "="
    GreaterOrEqual => ">="
    GreaterThan => ">"
    Between => "between"
  }
}

///|
fn geometry_kind_text(kind : GeometryKind) -> String {
  match kind {
    Extrusion => "extrusion"
    FacetedBrep => "faceted_brep"
    TriangulatedMesh => "triangulated_mesh"
    CsgSolid => "csg_solid"
    Profile => "profile"
    Polyline => "polyline"
    PointCloud => "point_cloud"
    Unknown => "unknown"
  }
}

///|
fn query_expression_text(expression : ProductExpression) -> String {
  match expression {
    MatchAll => "all"
    MatchNone => "none"
    Predicate(predicate) => query_predicate_text(predicate)
    Not(inner) => "not(" + query_expression_text(inner) + ")"
    AllOf(values) =>
      "all(" +
      values.map(value => query_expression_text(value)).join(", ") +
      ")"
    AnyOf(values) =>
      "any(" +
      values.map(value => query_expression_text(value)).join(", ") +
      ")"
  }
}

///|
pub fn ProductQueryPlan::explain(self : ProductQueryPlan) -> String {
  "filter=" +
  query_expression_text(self.expression) +
  " offset=" +
  self.offset.to_string() +
  " limit=" +
  self.limit.to_string() +
  " descending=" +
  self.descending.to_string()
}

///|
pub fn ProductExpression::describe(self : ProductExpression) -> String {
  query_expression_text(self)
}

///|
pub fn ProductPredicate::describe(self : ProductPredicate) -> String {
  query_predicate_text(self)
}

///|
pub fn ProductQueryPlan::execute(
  self : ProductQueryPlan,
  model : Model,
) -> ProductQueryPage raise ParseError {
  execute_query_plan(model, self)
}

///|
pub fn query_kind(
  model : Model,
  kind : ProductKind,
) -> Array[Product] raise ParseError {
  ProductQueryPlan::new().where_predicate(ProductKind(kind)).execute(model).products
}

///|
pub fn query_by_name(
  model : Model,
  name : String,
) -> Array[Product] raise ParseError {
  ProductQueryPlan::new().where_predicate(NameContains(name)).execute(model).products
}

///|
pub fn query_property(
  model : Model,
  name : String,
  value : String,
) -> Array[Product] raise ParseError {
  ProductQueryPlan::new()
  .where_predicate(PropertyText(name, value))
  .execute(model).products
}

///|
pub fn query_geometry(
  model : Model,
  kind : GeometryKind,
) -> Array[Product] raise ParseError {
  ProductQueryPlan::new().where_predicate(GeometryKind(kind)).execute(model).products
}

///|
pub fn query_plan_report(
  model : Model,
  plan : ProductQueryPlan,
) -> Array[String] raise ParseError {
  let page = plan.execute(model)
  [plan.explain(), page.headline()]
}