///|
fn slice_model_with_ids(
  model : Model,
  root_ids : Array[Int],
  include_dependencies : Bool,
) -> ModelSlice {
  let graph = entity_graph(model)
  let dependency_ids = if include_dependencies {
    graph.reachable(root_ids)
  } else {
    root_ids.copy()
  }
  let selected_ids = dependency_ids.copy()
  let entities = model.entities.filter(entity => {
    selected_ids.contains(entity.id)
  })
  let omitted_ids = model.entities
    .filter(entity => !selected_ids.contains(entity.id))
    .map(entity => entity.id)
  {
    model: Model::new(header=model.header, entities~),
    selected_ids,
    dependency_ids,
    omitted_ids,
    root_ids,
  }
}

///|
/// Create a dependency-closed model slice rooted at raw entity IDs.
pub fn model_slice(
  model : Model,
  root_ids : Array[Int],
  include_dependencies? : Bool = true,
) -> ModelSlice {
  slice_model_with_ids(model, root_ids, include_dependencies)
}

///|
pub fn model_slice_for_products(
  model : Model,
  product_ids : Array[Int],
  include_dependencies? : Bool = true,
) -> ModelSlice {
  model_slice(model, product_ids, include_dependencies~)
}

///|
pub fn model_slice_for_query(
  model : Model,
  plan : ProductQueryPlan,
  include_dependencies? : Bool = true,
) -> ModelSlice raise ParseError {
  let products = plan.execute(model).products
  model_slice(model, products.map(product => product.id), include_dependencies~)
}

///|
pub fn model_slice_for_box(
  model : Model,
  bounds : BoundingBox,
  include_dependencies? : Bool = true,
) -> ModelSlice raise ParseError {
  let spatial = SpatialIndex::new(model)
  let products = spatial.query_box(bounds).map(hit => hit.product_id)
  model_slice(model, products, include_dependencies~)
}

///|
pub fn ModelSlice::entity(self : ModelSlice, id : Int) -> Entity? {
  self.model.entities.filter(entity => entity.id == id).get(0)
}

///|
pub fn ModelSlice::contains(self : ModelSlice, id : Int) -> Bool {
  self.selected_ids.contains(id)
}

///|
pub fn ModelSlice::root_count(self : ModelSlice) -> Int {
  self.root_ids.length()
}

///|
pub fn ModelSlice::dependency_count(self : ModelSlice) -> Int {
  self.dependency_ids.length()
}

///|
pub fn ModelSlice::omitted_count(self : ModelSlice) -> Int {
  self.omitted_ids.length()
}

///|
pub fn ModelSlice::product_count(self : ModelSlice) -> Int {
  self.model.products().length()
}

///|
pub fn ModelSlice::is_closed(self : ModelSlice) -> Bool {
  let ids = self.selected_ids
  self.model.entities.all(entity => {
    entity_references(entity).all(id => ids.contains(id))
  })
}

///|
pub fn ModelSlice::references_omitted(self : ModelSlice) -> Array[Int] {
  let ids = self.selected_ids
  let result : Array[Int] = []
  for entity in self.model.entities {
    for id in entity_references(entity) {
      if !ids.contains(id) && !result.contains(id) {
        result.push(id)
      }
    }
  }
  result
}

///|
pub fn ModelSlice::to_step(self : ModelSlice) -> String {
  self.model.to_step()
}

///|
pub fn ModelSlice::round_trip(self : ModelSlice) -> Model raise ParseError {
  parse(self.to_step())
}

///|
pub fn ModelSlice::headline(self : ModelSlice) -> String {
  "roots=" +
  self.root_ids.length().to_string() +
  " selected=" +
  self.selected_ids.length().to_string() +
  " dependencies=" +
  self.dependency_ids.length().to_string() +
  " omitted=" +
  self.omitted_ids.length().to_string() +
  " closed=" +
  self.is_closed().to_string()
}

///|
pub fn ModelSlice::summary(self : ModelSlice) -> Array[String] {
  [
    self.headline(),
    "schema=" + self.model.schema(),
    "products=" + self.model.products().length().to_string(),
    "references_omitted=" + self.references_omitted().length().to_string(),
  ]
}

///|
pub fn ModelSlice::to_json(self : ModelSlice) -> String {
  "{\"roots\":[" +
  self.root_ids.map(id => id.to_string()).join(",") +
  "],\"selected_ids\":[" +
  self.selected_ids.map(id => id.to_string()).join(",") +
  "],\"dependency_count\":" +
  self.dependency_ids.length().to_string() +
  ",\"omitted_count\":" +
  self.omitted_ids.length().to_string() +
  ",\"closed\":" +
  self.is_closed().to_string() +
  "}"
}

///|
pub fn ModelSlice::to_markdown(self : ModelSlice) -> String {
  let builder = StringBuilder()
  builder <+ "# Model slice\n\n"
  builder <+ "- Roots: \{self.root_ids.length()}\n"
  builder <+ "- Selected entities: \{self.selected_ids.length()}\n"
  builder <+ "- Dependencies: \{self.dependency_ids.length()}\n"
  builder <+ "- Omitted entities: \{self.omitted_ids.length()}\n"
  builder <+ "- Dependency closed: \{self.is_closed()}\n"
  builder.to_string()
}

///|
pub fn ModelSlice::ids_by_type(self : ModelSlice, name : String) -> Array[Int] {
  let wanted = name.to_upper()
  self.model.entities
  .filter(entity => entity.name.to_upper() == wanted)
  .map(entity => entity.id)
}

///|
pub fn ModelSlice::entities_by_type(
  self : ModelSlice,
  name : String,
) -> Array[Entity] {
  let wanted = name.to_upper()
  self.model.entities.filter(entity => entity.name.to_upper() == wanted)
}

///|
pub fn ModelSlice::products_by_kind(
  self : ModelSlice,
  kind : ProductKind,
) -> Array[Product] {
  self.model.products().filter(product => product.kind == kind)
}

///|
pub fn ModelSlice::with_additional_roots(
  self : ModelSlice,
  ids : Array[Int],
  include_dependencies? : Bool = true,
) -> ModelSlice {
  let roots = self.root_ids.copy()
  for id in ids {
    if !roots.contains(id) {
      roots.push(id)
    }
  }
  slice_model_with_ids(self.model, roots, include_dependencies)
}

///|
pub fn ModelSlice::without_roots(
  self : ModelSlice,
  ids : Array[Int],
) -> ModelSlice {
  let roots = self.root_ids.filter(id => !ids.contains(id))
  slice_model_with_ids(self.model, roots, true)
}

///|
pub fn ModelSlice::difference(
  self : ModelSlice,
  other : ModelSlice,
) -> Array[Int] {
  self.selected_ids.filter(id => !other.selected_ids.contains(id))
}

///|
pub fn slice_changed_entities(before : Model, after : Model) -> ModelSlice {
  let ids = diff_models(before, after).changed_ids()
  model_slice(after, ids, include_dependencies=true)
}