///|
/// The kind of payload described by a WebBIM delivery manifest.
pub(all) enum AssetKind {
  Manifest
  Geometry
  Properties
  Spatial
  Product
} derive(Debug, Eq)

///|
/// Options for producing a deterministic, cache-friendly asset manifest.
pub(all) struct AssetManifestOptions {
  base_uri : String
  chunk_size : Int
  include_properties : Bool
  include_spatial : Bool
  include_product_rows : Bool
  include_empty_chunks : Bool
} derive(Debug)

///|
/// One downloadable payload and the products it covers.
pub(all) struct AssetManifestEntry {
  path : String
  kind : AssetKind
  product_ids : Array[Int]
  byte_length : Int
  point_count : Int
  triangle_count : Int
  bounds : BoundingBox
  checksum : Int
  compressed : Bool
} derive(Debug)

///|
/// A complete delivery map for a parsed IFC model.
pub(all) struct AssetManifest {
  schema : String
  model_hash : Int
  entries : Array[AssetManifestEntry]
  product_count : Int
  chunk_count : Int
  point_count : Int
  triangle_count : Int
  bounds : BoundingBox
} derive(Debug)

///|
pub fn default_asset_manifest_options() -> AssetManifestOptions {
  {
    base_uri: "assets",
    chunk_size: 128,
    include_properties: true,
    include_spatial: true,
    include_product_rows: true,
    include_empty_chunks: false,
  }
}

///|
pub fn AssetManifestOptions::with_base_uri(
  self : AssetManifestOptions,
  base_uri : String,
) -> AssetManifestOptions {
  let trimmed = base_uri.trim().to_owned()
  { ..self, base_uri: if trimmed.length() == 0 { "assets" } else { trimmed } }
}

///|
pub fn AssetManifestOptions::with_chunk_size(
  self : AssetManifestOptions,
  chunk_size : Int,
) -> AssetManifestOptions {
  { ..self, chunk_size: if chunk_size <= 0 { 128 } else { chunk_size } }
}

///|
pub fn AssetManifestOptions::including_properties(
  self : AssetManifestOptions,
  value : Bool,
) -> AssetManifestOptions {
  { ..self, include_properties: value }
}

///|
pub fn AssetManifestOptions::including_spatial(
  self : AssetManifestOptions,
  value : Bool,
) -> AssetManifestOptions {
  { ..self, include_spatial: value }
}

///|
pub fn AssetManifestOptions::including_product_rows(
  self : AssetManifestOptions,
  value : Bool,
) -> AssetManifestOptions {
  { ..self, include_product_rows: value }
}

///|
pub fn AssetManifestOptions::including_empty_chunks(
  self : AssetManifestOptions,
  value : Bool,
) -> AssetManifestOptions {
  { ..self, include_empty_chunks: value }
}

///|
fn asset_kind_name(kind : AssetKind) -> String {
  match kind {
    Manifest => "manifest"
    Geometry => "geometry"
    Properties => "properties"
    Spatial => "spatial"
    Product => "product"
  }
}

///|
fn asset_schema_name(schema : SchemaVersion) -> String {
  match schema {
    IFC2X3 => "IFC2X3"
    IFC4 => "IFC4"
    IFC4X3 => "IFC4X3"
    UnknownSchema(name) => name
  }
}

///|
fn asset_path(
  options : AssetManifestOptions,
  kind : AssetKind,
  index : Int,
  suffix : String,
) -> String {
  options.base_uri +
  "/" +
  asset_kind_name(kind) +
  "/" +
  index.to_string() +
  suffix
}

///|
fn asset_checksum_text(value : String) -> Int {
  value.hash()
}

///|
fn asset_entry_json_text(
  ids : Array[Int],
  bounds : BoundingBox,
  point_count : Int,
  triangle_count : Int,
) -> String {
  "{\"product_ids\":[" +
  ids.map(id => id.to_string()).join(",") +
  "],\"bounds\":" +
  mesh_bounds_json({ points: [bounds.min, bounds.max], triangles: [] }) +
  ",\"point_count\":" +
  point_count.to_string() +
  ",\"triangle_count\":" +
  triangle_count.to_string() +
  "}"
}

///|
fn asset_chunk_entry(
  options : AssetManifestOptions,
  chunk : SceneChunk,
) -> AssetManifestEntry {
  let payload = asset_entry_json_text(
    chunk.product_ids,
    chunk.bounds,
    chunk.point_count,
    chunk.triangle_count,
  )
  {
    path: asset_path(options, Geometry, chunk.index, ".json"),
    kind: Geometry,
    product_ids: chunk.product_ids,
    byte_length: payload.length(),
    point_count: chunk.point_count,
    triangle_count: chunk.triangle_count,
    bounds: chunk.bounds,
    checksum: asset_checksum_text(payload),
    compressed: false,
  }
}

///|
fn asset_product_entry(
  options : AssetManifestOptions,
  product : Product,
  index : Int,
) -> AssetManifestEntry {
  let payload = "{\"id\":" +
    product.id.to_string() +
    ",\"type\":\"" +
    product.ifc_type +
    "\",\"name\":\"" +
    product.name.unwrap_or("") +
    "\"}"
  {
    path: asset_path(options, Product, index, ".json"),
    kind: Product,
    product_ids: [product.id],
    byte_length: payload.length(),
    point_count: 0,
    triangle_count: 0,
    bounds: BoundingBox::empty(),
    checksum: asset_checksum_text(payload),
    compressed: false,
  }
}

///|
fn asset_property_entry(
  options : AssetManifestOptions,
  table : PropertyTable,
) -> AssetManifestEntry {
  let payload = table.to_json()
  {
    path: options.base_uri + "/properties/table.json",
    kind: Properties,
    product_ids: table.rows.map(row => row.product_id),
    byte_length: payload.length(),
    point_count: 0,
    triangle_count: 0,
    bounds: BoundingBox::empty(),
    checksum: asset_checksum_text(payload),
    compressed: false,
  }
}

///|
fn asset_spatial_entry(
  options : AssetManifestOptions,
  spatial : SpatialIndex,
) -> AssetManifestEntry {
  let payload = spatial.to_json()
  {
    path: options.base_uri + "/spatial/index.json",
    kind: Spatial,
    product_ids: spatial.product_ids(),
    byte_length: payload.length(),
    point_count: spatial
    .entries()
    .fold(init=0, (sum, entry) => sum + entry.point_count),
    triangle_count: spatial.total_triangles(),
    bounds: spatial.bounds(),
    checksum: asset_checksum_text(payload),
    compressed: false,
  }
}

///|
fn asset_manifest_entry(
  options : AssetManifestOptions,
  model : Model,
  entries : Array[AssetManifestEntry],
  spatial : SpatialIndex,
) -> AssetManifestEntry {
  let summary = "schema=" +
    asset_schema_name(schema_version(model)) +
    ";products=" +
    spatial.entries().length().to_string() +
    ";entries=" +
    entries.length().to_string()
  {
    path: options.base_uri + "/manifest.json",
    kind: Manifest,
    product_ids: spatial.product_ids(),
    byte_length: summary.length(),
    point_count: spatial
    .entries()
    .fold(init=0, (sum, entry) => sum + entry.point_count),
    triangle_count: spatial.total_triangles(),
    bounds: spatial.bounds(),
    checksum: asset_checksum_text(summary),
    compressed: false,
  }
}

///|
/// Build paths and resource statistics without serializing model geometry twice.
pub fn asset_manifest(
  model : Model,
  options? : AssetManifestOptions = default_asset_manifest_options(),
) -> AssetManifest raise ParseError {
  let spatial = SpatialIndex::new(model)
  let partition = scene_partition_for_products(
    spatial,
    spatial.product_ids(),
    chunk_size=options.chunk_size,
  )
  let entries : Array[AssetManifestEntry] = []
  for chunk in partition.chunks {
    if options.include_empty_chunks || !chunk.is_empty() {
      entries.push(asset_chunk_entry(options, chunk))
    }
  }
  if options.include_spatial {
    entries.push(asset_spatial_entry(options, spatial))
  }
  if options.include_product_rows {
    let products = model.products()
    for index in 0.. sum + entry.point_count),
    triangle_count: spatial.total_triangles(),
    bounds: spatial.bounds(),
  }
}

///|
pub fn AssetManifest::entry_count(self : AssetManifest) -> Int {
  self.entries.length()
}

///|
pub fn AssetManifest::entry(
  self : AssetManifest,
  path : String,
) -> AssetManifestEntry? {
  for entry in self.entries {
    if entry.path == path {
      return Some(entry)
    }
  }
  None
}

///|
pub fn AssetManifest::entries_of_kind(
  self : AssetManifest,
  kind : AssetKind,
) -> Array[AssetManifestEntry] {
  self.entries.filter(entry => entry.kind == kind)
}

///|
pub fn AssetManifest::paths(self : AssetManifest) -> Array[String] {
  self.entries.map(entry => entry.path)
}

///|
pub fn AssetManifest::total_bytes(self : AssetManifest) -> Int {
  self.entries.fold(init=0, (sum, entry) => sum + entry.byte_length)
}

///|
pub fn AssetManifest::total_points(self : AssetManifest) -> Int {
  self.point_count
}

///|
pub fn AssetManifest::total_triangles(self : AssetManifest) -> Int {
  self.triangle_count
}

///|
pub fn AssetManifest::is_cacheable(self : AssetManifest) -> Bool {
  self.entries.all(entry => entry.checksum != 0 && !entry.compressed)
}

///|
pub fn AssetManifest::geometry_entries(
  self : AssetManifest,
) -> Array[AssetManifestEntry] {
  self.entries_of_kind(Geometry)
}

///|
pub fn AssetManifest::product_entries(
  self : AssetManifest,
) -> Array[AssetManifestEntry] {
  self.entries_of_kind(Product)
}

///|
pub fn AssetManifest::spatial_entry(
  self : AssetManifest,
) -> AssetManifestEntry? {
  self.entries_of_kind(Spatial).get(0)
}

///|
pub fn AssetManifest::headline(self : AssetManifest) -> String {
  "schema=" +
  self.schema +
  " products=" +
  self.product_count.to_string() +
  " chunks=" +
  self.chunk_count.to_string() +
  " assets=" +
  self.entry_count().to_string() +
  " bytes=" +
  self.total_bytes().to_string()
}

///|
fn asset_bounds_json(bounds : BoundingBox) -> String {
  if bounds.empty {
    "null"
  } else {
    "{\"min\":[" +
    bounds.min.x.to_string() +
    "," +
    bounds.min.y.to_string() +
    "," +
    bounds.min.z.to_string() +
    "],\"max\":[" +
    bounds.max.x.to_string() +
    "," +
    bounds.max.y.to_string() +
    "," +
    bounds.max.z.to_string() +
    "]}"
  }
}

///|
pub fn AssetManifest::to_json(self : AssetManifest) -> String {
  let entries = self.entries
    .map(entry => {
      "{\"path\":\"" +
      entry.path +
      "\",\"kind\":\"" +
      asset_kind_name(entry.kind) +
      "\",\"product_count\":" +
      entry.product_ids.length().to_string() +
      ",\"byte_length\":" +
      entry.byte_length.to_string() +
      ",\"point_count\":" +
      entry.point_count.to_string() +
      ",\"triangle_count\":" +
      entry.triangle_count.to_string() +
      ",\"checksum\":" +
      entry.checksum.to_string() +
      ",\"bounds\":" +
      asset_bounds_json(entry.bounds) +
      "}"
    })
    .join(",")
  "{\"schema\":\"" +
  self.schema +
  "\",\"model_hash\":" +
  self.model_hash.to_string() +
  ",\"product_count\":" +
  self.product_count.to_string() +
  ",\"chunk_count\":" +
  self.chunk_count.to_string() +
  ",\"point_count\":" +
  self.point_count.to_string() +
  ",\"triangle_count\":" +
  self.triangle_count.to_string() +
  ",\"entries\":[" +
  entries +
  "]}"
}

///|
pub fn AssetManifest::to_csv(self : AssetManifest) -> String {
  let builder = StringBuilder()
  builder <+
    "path,kind,product_count,byte_length,point_count,triangle_count,checksum\n"
  for entry in self.entries {
    builder <+
      "\{entry.path},\{asset_kind_name(entry.kind)},\{entry.product_ids.length()},\{entry.byte_length},\{entry.point_count},\{entry.triangle_count},\{entry.checksum}\n"
  }
  builder.to_string()
}

///|
pub fn AssetManifest::to_markdown(self : AssetManifest) -> String {
  let builder = StringBuilder()
  builder <+ "# WebBIM asset manifest\n\n"
  builder <+ "- \{self.headline()}\n"
  builder <+ "- Model hash: \{self.model_hash}\n"
  builder <+ "- Points: \{self.point_count}\n"
  builder <+ "- Triangles: \{self.triangle_count}\n"
  builder <+ "- Cacheable: \{self.is_cacheable()}\n\n"
  builder <+ "| Path | Kind | Products | Bytes | Points | Triangles |\n"
  builder <+ "| --- | --- | ---: | ---: | ---: | ---: |\n"
  for entry in self.entries {
    builder <+
      "| \{entry.path} | \{asset_kind_name(entry.kind)} | \{entry.product_ids.length()} | \{entry.byte_length} | \{entry.point_count} | \{entry.triangle_count} |\n"
  }
  builder.to_string()
}

///|
pub fn asset_manifest_paths(
  manifest : AssetManifest,
  kind : AssetKind,
) -> Array[String] {
  manifest.entries_of_kind(kind).map(entry => entry.path)
}

///|
pub fn asset_manifest_product_paths(
  manifest : AssetManifest,
  product_id : Int,
) -> Array[String] {
  manifest.entries
  .filter(entry => entry.product_ids.contains(product_id))
  .map(entry => entry.path)
}

///|
pub fn asset_manifest_checksums(manifest : AssetManifest) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for entry in manifest.entries {
    result[entry.path] = entry.checksum
  }
  result
}

///|
pub fn asset_manifest_kind_histogram(
  manifest : AssetManifest,
) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for entry in manifest.entries {
    result.update_or_default(asset_kind_name(entry.kind), 1, count => count + 1)
  }
  result
}

///|
pub fn asset_manifest_is_complete(manifest : AssetManifest) -> Bool {
  manifest.entry_count() > 0 &&
  manifest.entries_of_kind(Manifest).length() == 1 &&
  manifest.entries_of_kind(Geometry).length() > 0
}

///|
pub fn asset_manifest_summary(manifest : AssetManifest) -> Array[String] {
  [
    manifest.headline(),
    "cacheable=" + manifest.is_cacheable().to_string(),
    "complete=" + asset_manifest_is_complete(manifest).to_string(),
    "geometry_assets=" + manifest.geometry_entries().length().to_string(),
    "product_assets=" + manifest.product_entries().length().to_string(),
  ]
}

///|
pub fn asset_manifest_entry_json(
  manifest : AssetManifest,
  path : String,
) -> String? {
  match manifest.entry(path) {
    Some(entry) =>
      Some(
        "{\"path\":\"" +
        entry.path +
        "\",\"kind\":\"" +
        asset_kind_name(entry.kind) +
        "\",\"bytes\":" +
        entry.byte_length.to_string() +
        ",\"checksum\":" +
        entry.checksum.to_string() +
        "}",
      )
    None => None
  }
}