///|
/// Parse once and prepare the indexes commonly needed by BIM applications.
pub fn ModelWorkspace::open(source : String) -> ModelWorkspace raise ParseError {
  let model = parse(source)
  let catalog = ModelCatalog::new(model)
  let spatial = SpatialIndex::new(model)
  let graph = analyze_entity_graph(entity_graph(model))
  {
    model,
    catalog,
    spatial,
    graph,
    health: source_health(source),
    quantities: None,
  }
}

///|
pub fn ModelWorkspace::from_model(
  model : Model,
  source? : String = "",
) -> ModelWorkspace raise ParseError {
  let catalog = ModelCatalog::new(model)
  let spatial = SpatialIndex::new(model)
  let graph = analyze_entity_graph(entity_graph(model))
  let actual_source = if source.length() == 0 {
    model.to_step()
  } else {
    source
  }
  {
    model,
    catalog,
    spatial,
    graph,
    health: source_health(actual_source),
    quantities: None,
  }
}

///|
pub fn workspace(source : String) -> ModelWorkspace raise ParseError {
  ModelWorkspace::open(source)
}

///|
pub fn ModelWorkspace::model(self : ModelWorkspace) -> Model {
  self.model
}

///|
pub fn ModelWorkspace::catalog(self : ModelWorkspace) -> ModelCatalog {
  self.catalog
}

///|
pub fn ModelWorkspace::spatial_index(self : ModelWorkspace) -> SpatialIndex {
  self.spatial
}

///|
pub fn ModelWorkspace::graph_analysis(self : ModelWorkspace) -> GraphAnalysis {
  self.graph
}

///|
pub fn ModelWorkspace::source_health(self : ModelWorkspace) -> SourceHealth {
  self.health
}

///|
pub fn ModelWorkspace::with_quantities(
  self : ModelWorkspace,
  options? : TakeoffOptions = default_takeoff_options(),
) -> ModelWorkspace raise ParseError {
  { ..self, quantities: Some(quantity_takeoff(self.model, options~)) }
}

///|
pub fn ModelWorkspace::has_quantities(self : ModelWorkspace) -> Bool {
  self.quantities is Some(_)
}

///|
pub fn ModelWorkspace::takeoff(
  self : ModelWorkspace,
  options? : TakeoffOptions = default_takeoff_options(),
) -> TakeoffReport raise ParseError {
  match self.quantities {
    Some(report) => report
    None => quantity_takeoff(self.model, options~)
  }
}

///|
pub fn ModelWorkspace::query(
  self : ModelWorkspace,
  plan : ProductQueryPlan,
) -> ProductQueryPage raise ParseError {
  plan.execute(self.model)
}

///|
pub fn ModelWorkspace::find(
  self : ModelWorkspace,
  product_id : Int,
) -> Product? {
  self.catalog.products.filter(product => product.id == product_id).get(0)
}

///|
pub fn ModelWorkspace::products(self : ModelWorkspace) -> Array[Product] {
  self.catalog.products.copy()
}

///|
pub fn ModelWorkspace::clashes(
  self : ModelWorkspace,
  options? : ClashOptions = default_clash_options(),
) -> ClashReport {
  detect_clashes(self.spatial, options~)
}

///|
pub fn ModelWorkspace::rules(
  self : ModelWorkspace,
  rules : Array[ModelRule],
) -> RuleReport raise ParseError {
  evaluate_rules(self.model, rules)
}

///|
pub fn ModelWorkspace::delivery_report(
  self : ModelWorkspace,
) -> RuleReport raise ParseError {
  delivery_rule_report(self.model)
}

///|
pub fn ModelWorkspace::snapshot(self : ModelWorkspace) -> ModelSnapshot {
  model_snapshot(self.model)
}

///|
pub fn ModelWorkspace::summary(
  self : ModelWorkspace,
) -> WorkspaceSummary raise ParseError {
  let report = delivery_rule_report(self.model)
  {
    schema: self.model.schema(),
    source_length: self.health.byte_length,
    entity_count: self.model.entities.length(),
    product_count: self.catalog.products.length(),
    indexed_products: self.spatial.entries().length(),
    graph_nodes: self.graph.nodes,
    graph_edges: self.graph.edges,
    rule_errors: report.error_count,
    rule_warnings: report.warning_count,
  }
}

///|
pub fn WorkspaceSummary::headline(self : WorkspaceSummary) -> String {
  "schema=" +
  self.schema +
  " entities=" +
  self.entity_count.to_string() +
  " products=" +
  self.product_count.to_string() +
  " indexed=" +
  self.indexed_products.to_string() +
  " graph=" +
  self.graph_nodes.to_string() +
  "/" +
  self.graph_edges.to_string() +
  " rules=" +
  self.rule_errors.to_string() +
  "/" +
  self.rule_warnings.to_string()
}

///|
pub fn WorkspaceSummary::is_healthy(self : WorkspaceSummary) -> Bool {
  self.rule_errors == 0
}

///|
pub fn WorkspaceSummary::to_json(self : WorkspaceSummary) -> String {
  "{\"schema\":\"" +
  self.schema +
  "\",\"source_length\":" +
  self.source_length.to_string() +
  ",\"entity_count\":" +
  self.entity_count.to_string() +
  ",\"product_count\":" +
  self.product_count.to_string() +
  ",\"indexed_products\":" +
  self.indexed_products.to_string() +
  ",\"graph_nodes\":" +
  self.graph_nodes.to_string() +
  ",\"graph_edges\":" +
  self.graph_edges.to_string() +
  ",\"rule_errors\":" +
  self.rule_errors.to_string() +
  ",\"rule_warnings\":" +
  self.rule_warnings.to_string() +
  "}"
}

///|
pub fn model_snapshot(model : Model) -> ModelSnapshot {
  {
    schema: model.schema(),
    source_length: model.to_step().length(),
    entity_count: model.entities.length(),
    product_count: model.products().length(),
    shape_signatures: model_shape_signatures(model),
    reference_targets: model_reference_targets(model),
  }
}

///|
pub fn ModelSnapshot::same_shape(
  self : ModelSnapshot,
  other : ModelSnapshot,
) -> Bool {
  self.shape_signatures == other.shape_signatures
}

///|
pub fn ModelSnapshot::same_references(
  self : ModelSnapshot,
  other : ModelSnapshot,
) -> Bool {
  self.reference_targets == other.reference_targets
}

///|
pub fn ModelSnapshot::entity_delta(
  self : ModelSnapshot,
  other : ModelSnapshot,
) -> Int {
  other.entity_count - self.entity_count
}

///|
pub fn ModelSnapshot::product_delta(
  self : ModelSnapshot,
  other : ModelSnapshot,
) -> Int {
  other.product_count - self.product_count
}

///|
pub fn ModelSnapshot::headline(self : ModelSnapshot) -> String {
  self.schema +
  " entities=" +
  self.entity_count.to_string() +
  " products=" +
  self.product_count.to_string() +
  " source=" +
  self.source_length.to_string()
}

///|
pub fn ModelSnapshot::to_json(self : ModelSnapshot) -> String {
  "{\"schema\":\"" +
  self.schema +
  "\",\"source_length\":" +
  self.source_length.to_string() +
  ",\"entity_count\":" +
  self.entity_count.to_string() +
  ",\"product_count\":" +
  self.product_count.to_string() +
  ",\"shape_count\":" +
  self.shape_signatures.length().to_string() +
  ",\"reference_target_count\":" +
  self.reference_targets.length().to_string() +
  "}"
}

///|
pub fn snapshot_diff(
  left : ModelSnapshot,
  right : ModelSnapshot,
) -> Array[String] {
  let result : Array[String] = []
  if left.schema != right.schema {
    result.push("schema changed: " + left.schema + " -> " + right.schema)
  }
  if left.entity_count != right.entity_count {
    result.push("entities delta: " + left.entity_delta(right).to_string())
  }
  if left.product_count != right.product_count {
    result.push("products delta: " + left.product_delta(right).to_string())
  }
  if !left.same_shape(right) {
    result.push("shape signatures changed")
  }
  if !left.same_references(right) {
    result.push("reference targets changed")
  }
  result
}

///|
pub fn workspace_snapshot_diff(
  left : ModelWorkspace,
  right : ModelWorkspace,
) -> Array[String] {
  snapshot_diff(left.snapshot(), right.snapshot())
}

///|
pub fn workspace_export(
  workspace : ModelWorkspace,
  options? : ExportOptions = default_export_options(),
) -> ExportedAsset raise ParseError {
  export_model_asset(workspace.model, options)
}

///|
pub fn workspace_inspection(workspace : ModelWorkspace) -> Inspection {
  {
    model: workspace.model,
    summary: workspace.catalog.summary(),
    metrics: workspace.catalog.metrics,
    validation: workspace.model.validate(),
    capabilities: capability_report(workspace.model),
  }
}

///|
pub fn workspace_health_score(workspace : ModelWorkspace) -> Int {
  source_health_score(workspace.health_to_source())
}

///|
fn ModelWorkspace::health_to_source(self : ModelWorkspace) -> String {
  self.model.to_step()
}

///|
pub fn ModelWorkspace::headline(
  self : ModelWorkspace,
) -> String raise ParseError {
  self.summary().headline()
}

///|
pub fn ModelWorkspace::to_json(
  self : ModelWorkspace,
) -> String raise ParseError {
  let summary = self.summary()
  "{\"summary\":" +
  summary.to_json() +
  ",\"health\":\"" +
  self.health.summary() +
  "\"" +
  ",\"graph\":" +
  self.graph.to_json() +
  ",\"spatial\":" +
  self.spatial.to_json() +
  "}"
}

///|
pub fn workspace_query_by_kind(
  workspace : ModelWorkspace,
  kind : ProductKind,
) -> Array[Product] raise ParseError {
  workspace.query(ProductQueryPlan::new().where_predicate(ProductKind(kind))).products
}

///|
pub fn workspace_query_by_type(
  workspace : ModelWorkspace,
  ifc_type : String,
) -> Array[Product] raise ParseError {
  workspace.query(ProductQueryPlan::new().where_predicate(EntityType(ifc_type))).products
}

///|
pub fn workspace_products_at_point(
  workspace : ModelWorkspace,
  point : Point3,
  tolerance? : Double = 0.0,
) -> Array[SpatialHit] {
  workspace.spatial.query_point(point, tolerance~)
}

///|
pub fn workspace_nearest_product(
  workspace : ModelWorkspace,
  point : Point3,
) -> SpatialHit? {
  workspace.spatial.nearest(point)
}