///|
/// Change class for a cache-aware WebBIM delivery.
pub(all) enum DeliveryChange {
  AddedAsset
  RemovedAsset
  InvalidatedAsset
  RetainedAsset
} derive(Debug, Eq)

///|
/// One manifest path and the revision impact that applies to it.
pub(all) struct DeliveryAssetChange {
  path : String
  change : DeliveryChange
  before_checksum : Int?
  after_checksum : Int?
  affected_products : Array[Int]
  byte_delta : Int
} derive(Debug)

///|
/// Cache invalidation plan generated from two model revisions.
pub(all) struct RevisionDelivery {
  before_hash : Int
  after_hash : Int
  changes : Array[DeliveryAssetChange]
  affected_products : Array[Int]
  invalidated_count : Int
  retained_count : Int
  added_count : Int
  removed_count : Int
  bytes_added : Int
  bytes_removed : Int
} derive(Debug)

///|
pub fn delivery_change_name(change : DeliveryChange) -> String {
  match change {
    AddedAsset => "added"
    RemovedAsset => "removed"
    InvalidatedAsset => "invalidated"
    RetainedAsset => "retained"
  }
}

///|
fn delivery_entry(
  manifest : AssetManifest,
  path : String,
) -> AssetManifestEntry? {
  manifest.entry(path)
}

///|
fn delivery_product_union(
  left : AssetManifestEntry?,
  right : AssetManifestEntry?,
) -> Array[Int] {
  let result : Array[Int] = []
  match left {
    Some(entry) => result.append(entry.product_ids)
    None => ()
  }
  match right {
    Some(entry) =>
      for id in entry.product_ids {
        if !result.contains(id) {
          result.push(id)
        }
      }
    None => ()
  }
  result
}

///|
fn delivery_change_for_entries(
  before : AssetManifestEntry?,
  after : AssetManifestEntry?,
) -> DeliveryChange {
  match (before, after) {
    (None, Some(_)) => AddedAsset
    (Some(_), None) => RemovedAsset
    (Some(left), Some(right)) if left.checksum != right.checksum =>
      InvalidatedAsset
    (Some(_), Some(_)) => RetainedAsset
    (None, None) => RetainedAsset
  }
}

///|
fn delivery_byte_delta(
  before : AssetManifestEntry?,
  after : AssetManifestEntry?,
) -> Int {
  let old_size = match before {
    Some(entry) => entry.byte_length
    None => 0
  }
  let new_size = match after {
    Some(entry) => entry.byte_length
    None => 0
  }
  new_size - old_size
}

///|
fn delivery_paths(
  before : AssetManifest,
  after : AssetManifest,
) -> Array[String] {
  let result = before.paths()
  for path in after.paths() {
    if !result.contains(path) {
      result.push(path)
    }
  }
  result
}

///|
/// Compare two generated manifests and classify every cache path.
pub fn revision_delivery(
  before : Model,
  after : Model,
  options? : AssetManifestOptions = default_asset_manifest_options(),
) -> RevisionDelivery raise ParseError {
  let old_manifest = asset_manifest(before, options~)
  let new_manifest = asset_manifest(after, options~)
  let changes : Array[DeliveryAssetChange] = []
  for path in delivery_paths(old_manifest, new_manifest) {
    let old_entry = delivery_entry(old_manifest, path)
    let new_entry = delivery_entry(new_manifest, path)
    let change = delivery_change_for_entries(old_entry, new_entry)
    changes.push({
      path,
      change,
      before_checksum: old_entry.map(entry => entry.checksum),
      after_checksum: new_entry.map(entry => entry.checksum),
      affected_products: delivery_product_union(old_entry, new_entry),
      byte_delta: delivery_byte_delta(old_entry, new_entry),
    })
  }
  let invalidated_count = changes.fold(init=0, (count, item) => {
    if item.change == InvalidatedAsset {
      count + 1
    } else {
      count
    }
  })
  let retained_count = changes.fold(init=0, (count, item) => {
    if item.change == RetainedAsset {
      count + 1
    } else {
      count
    }
  })
  let added_count = changes.fold(init=0, (count, item) => {
    if item.change == AddedAsset {
      count + 1
    } else {
      count
    }
  })
  let removed_count = changes.fold(init=0, (count, item) => {
    if item.change == RemovedAsset {
      count + 1
    } else {
      count
    }
  })
  let impact = revision_impact(before, after)
  {
    before_hash: old_manifest.model_hash,
    after_hash: new_manifest.model_hash,
    changes,
    affected_products: impact.affected_products,
    invalidated_count,
    retained_count,
    added_count,
    removed_count,
    bytes_added: changes.fold(init=0, (sum, item) => {
      if item.byte_delta > 0 {
        sum + item.byte_delta
      } else {
        sum
      }
    }),
    bytes_removed: changes.fold(init=0, (sum, item) => {
      if item.byte_delta < 0 {
        sum - item.byte_delta
      } else {
        sum
      }
    }),
  }
}

///|
pub fn RevisionDelivery::is_empty(self : RevisionDelivery) -> Bool {
  self.before_hash == self.after_hash &&
  self.invalidated_count == 0 &&
  self.added_count == 0 &&
  self.removed_count == 0
}

///|
pub fn RevisionDelivery::change_count(self : RevisionDelivery) -> Int {
  self.changes.length()
}

///|
pub fn RevisionDelivery::paths_for(
  self : RevisionDelivery,
  change : DeliveryChange,
) -> Array[String] {
  self.changes.filter(item => item.change == change).map(item => item.path)
}

///|
pub fn RevisionDelivery::products_for_path(
  self : RevisionDelivery,
  path : String,
) -> Array[Int] {
  for item in self.changes {
    if item.path == path {
      return item.affected_products
    }
  }
  []
}

///|
pub fn RevisionDelivery::requires_geometry_refresh(
  self : RevisionDelivery,
) -> Bool {
  self.changes.any(item => {
    item.change == InvalidatedAsset &&
    (
      item.path.has_prefix("assets/geometry/") ||
      item.path.has_prefix("assets/spatial/")
    )
  })
}

///|
pub fn RevisionDelivery::requires_property_refresh(
  self : RevisionDelivery,
) -> Bool {
  self.changes.any(item => {
    item.change == InvalidatedAsset &&
    item.path.has_prefix("assets/properties/")
  })
}

///|
pub fn RevisionDelivery::byte_delta(self : RevisionDelivery) -> Int {
  self.bytes_added - self.bytes_removed
}

///|
pub fn RevisionDelivery::headline(self : RevisionDelivery) -> String {
  "changes=" +
  self.change_count().to_string() +
  " invalidated=" +
  self.invalidated_count.to_string() +
  " retained=" +
  self.retained_count.to_string() +
  " added=" +
  self.added_count.to_string() +
  " removed=" +
  self.removed_count.to_string() +
  " byte_delta=" +
  self.byte_delta().to_string()
}

///|
fn delivery_checksum_json(value : Int?) -> String {
  match value {
    Some(checksum) => checksum.to_string()
    None => "null"
  }
}

///|
pub fn RevisionDelivery::to_json(self : RevisionDelivery) -> String {
  let changes = self.changes
    .map(item => {
      "{\"path\":\"" +
      item.path +
      "\",\"change\":\"" +
      delivery_change_name(item.change) +
      "\",\"before_checksum\":" +
      delivery_checksum_json(item.before_checksum) +
      ",\"after_checksum\":" +
      delivery_checksum_json(item.after_checksum) +
      ",\"byte_delta\":" +
      item.byte_delta.to_string() +
      ",\"affected_products\":[" +
      item.affected_products.map(id => id.to_string()).join(",") +
      "]}"
    })
    .join(",")
  "{\"before_hash\":" +
  self.before_hash.to_string() +
  ",\"after_hash\":" +
  self.after_hash.to_string() +
  ",\"affected_products\":[" +
  self.affected_products.map(id => id.to_string()).join(",") +
  "],\"changes\":[" +
  changes +
  "]}"
}

///|
pub fn RevisionDelivery::to_markdown(self : RevisionDelivery) -> String {
  let builder = StringBuilder()
  builder <+ "# Revision delivery\n\n"
  builder <+ "- \{self.headline()}\n"
  builder <+ "- Geometry refresh: \{self.requires_geometry_refresh()}\n"
  builder <+ "- Property refresh: \{self.requires_property_refresh()}\n"
  builder <+ "- Affected products: \{self.affected_products.length()}\n\n"
  builder <+ "| Path | Change | Affected products | Byte delta |\n"
  builder <+ "| --- | --- | ---: | ---: |\n"
  for item in self.changes {
    builder <+
      "| \{item.path} | \{delivery_change_name(item.change)} | \{item.affected_products.length()} | \{item.byte_delta} |\n"
  }
  builder.to_string()
}

///|
pub fn revision_delivery_paths(
  before : Model,
  after : Model,
) -> Array[String] raise ParseError {
  revision_delivery(before, after).changes.map(item => item.path)
}

///|
pub fn revision_delivery_invalidated_paths(
  before : Model,
  after : Model,
) -> Array[String] raise ParseError {
  revision_delivery(before, after).paths_for(InvalidatedAsset)
}

///|
pub fn revision_delivery_summary(
  before : Model,
  after : Model,
) -> Array[String] raise ParseError {
  let delivery = revision_delivery(before, after)
  [
    delivery.headline(),
    "empty=" + delivery.is_empty().to_string(),
    "geometry_refresh=" + delivery.requires_geometry_refresh().to_string(),
    "property_refresh=" + delivery.requires_property_refresh().to_string(),
  ]
}