///|
/// The three workflows the library is meant to be used through.
///
/// Each of them is a thin composition of the layers below, kept here so that a
/// caller does not have to remember which order to call `sign`, `compute_delta`
/// and `apply` in, and so that the numbers a report quotes come from one place.
///
/// 1. **Update a file**: `plan_transfer(old, new)` then send `delta`; the receiver
///    runs `verified_update(old, delta)` and gets the new bytes or an error.
/// 2. **Answer an update request**: the sender signs what the peer has, once, and
///    reuses that signature for every later version.
/// 3. **Deduplicate snapshots**: `ChunkStore::ingest` for every version, then
///    `assemble` to check that nothing was lost.

///|
/// The result of planning one transfer: the delta plus the numbers that make the
/// saving visible.
pub struct TransferPlan {
  signature : FileSignature
  delta : Delta
  new_length : Int32
  delta_bytes : Int32
}

///|
/// Sign `old` and compute the delta from it to `new_bytes`.
pub fn plan_transfer(
  old : BytesView,
  new_bytes : BytesView,
  block_size? : Int32,
  strong_len? : Int32,
) -> TransferPlan raise FormatError {
  let signature = sign_bytes(old, block_size?, strong_len?)
  let delta = compute_delta(new_bytes, signature)
  {
    signature,
    delta,
    new_length: new_bytes.length(),
    delta_bytes: delta.encode().length(),
  }
}

///|
/// Compute a delta against a signature that was produced earlier, which is what a
/// server does when it keeps the client's signature between requests.
pub fn plan_from_signature(
  signature : FileSignature,
  new_bytes : BytesView,
) -> TransferPlan raise FormatError {
  let delta = compute_delta(new_bytes, signature)
  {
    signature,
    delta,
    new_length: new_bytes.length(),
    delta_bytes: delta.encode().length(),
  }
}

///|
pub fn TransferPlan::delta(self : TransferPlan) -> Delta {
  self.delta
}

///|
pub fn TransferPlan::signature(self : TransferPlan) -> FileSignature {
  self.signature
}

///|
/// The encoded delta, ready to be stored or sent.
pub fn TransferPlan::delta_bytes(self : TransferPlan) -> Bytes {
  self.delta.encode()
}

///|
pub fn TransferPlan::new_length(self : TransferPlan) -> Int32 {
  self.new_length
}

///|
pub fn TransferPlan::literal_bytes(self : TransferPlan) -> Int32 {
  self.delta.literal_bytes()
}

///|
pub fn TransferPlan::copied_bytes(self : TransferPlan) -> Int32 {
  self.delta.copied_bytes()
}

///|
pub fn TransferPlan::copy_ops(self : TransferPlan) -> Int32 {
  self.delta.copy_ops()
}

///|
/// The saving as parts per million: `(new_length - delta_bytes) / new_length`.
///
/// Returned as an integer on purpose. A floating point ratio would print
/// differently on different backends in the last digit, and a report that cannot
/// be reproduced byte for byte is not much use as evidence.
pub fn TransferPlan::savings_ppm(self : TransferPlan) -> Int32 {
  if self.new_length <= 0 {
    return 0
  }
  let saved = self.new_length - self.delta_bytes
  if saved <= 0 {
    return 0
  }
  // 64 bit intermediate: `saved * 1_000_000` overflows a 32 bit Int for inputs
  // above a couple of kilobytes, which would silently turn the ratio into zero.
  (saved.to_int64() * 1_000_000L / self.new_length.to_int64()).to_int()
}

///|
/// Decode a delta, apply it and verify every copy. This is the only way a
/// receiver is expected to use a delta.
pub fn verified_update(
  old : BytesView,
  delta_bytes : BytesView,
) -> ApplyResult raise FormatError {
  let delta = decode_delta(delta_bytes)
  apply_delta(old, delta)
}

///|
/// Check that `old` really is the data the signature describes, block by block.
/// Useful when a signature is cached between requests.
pub fn signature_matches(signature : FileSignature, old : BytesView) -> Bool {
  if signature.total_length != old.length() {
    return false
  }
  for block in signature.blocks {
    let start = block.index * signature.block_size
    let end = if start + signature.block_size < old.length() {
      start + signature.block_size
    } else {
      old.length()
    }
    let actual = sha256_prefix(
      old.clamped_view(start~, end~),
      signature.strong_len,
    )
    if !constant_time_equal(actual[:], block.strong[:]) {
      return false
    }
  }
  true
}

///|
/// What one snapshot added to a store, in the shape a report wants it.
pub struct SnapshotReport {
  total_bytes : Int32
  chunk_count : Int32
  unique_chunks : Int32
  new_chunks : Int32
  reused_chunks : Int32
  stored_bytes : Int32
  saved_bytes : Int32
  manifest_bytes : Int32
  /// Saved bytes per million input bytes, so the ratio stays an integer.
  dedup_ppm : Int32
  root_hex : String
}

///|
/// Ingest one version into `store` and summarise what it cost.
pub fn ingest_snapshot(
  store : ChunkStore,
  input : BytesView,
  params? : ChunkParams,
) -> SnapshotReport {
  let ingest = store.ingest(input, params?)
  let saved = store.saved_bytes()
  let ingested = store.total_bytes_ingested()
  let dedup_ppm = if ingested <= 0 {
    0
  } else {
    (saved.to_int64() * 1_000_000L / ingested.to_int64()).to_int()
  }
  {
    total_bytes: ingest.total_bytes,
    chunk_count: ingest.chunk_count,
    unique_chunks: store.unique_chunks(),
    new_chunks: ingest.new_chunks,
    reused_chunks: ingest.reused_chunks,
    stored_bytes: store.stored_bytes(),
    saved_bytes: saved,
    manifest_bytes: ingest.manifest.encode().length(),
    dedup_ppm,
    root_hex: ingest.manifest.root_hex(),
  }
}