/// Planning and applying incremental synchronization.

fn index_of_digest(digests : Array[String], digest : String) -> Int? {
  for i, candidate in digests {
    if digest_equal(candidate, digest) { return Some(i) }
  }
  None
}

pub fn diff_manifests(old : Manifest, next : Manifest) -> SyncPlan {
  let operations = []
  let old_digests = manifest_digests(old)
  let old_refs = old.references()
  let next_refs = next.references()
  for reference in next_refs {
    match index_of_digest(old_digests, reference.id()) {
      Some(_) => operations.push(Reuse(digest=reference.id(), size=reference.length()))
      None => operations.push(Upload(digest=reference.id(), size=reference.length(), source_index=reference.at()))
    }
  }
  let retained = []
  for reference in next_refs { retained.push(reference.id()) }
  for reference in old_refs {
    if index_of_digest(retained, reference.id()) is None {
      operations.push(Remove(digest=reference.id()))
    }
  }
  SyncPlan::new(operations)
}

pub fn sync_savings(plan : SyncPlan, original_size : Int) -> Int {
  if original_size <= 0 { return 0 }
  original_size - plan.uploaded()
}

pub fn sync_ratio(plan : SyncPlan, original_size : Int) -> Double {
  if original_size <= 0 { return 0.0 }
  plan.uploaded().to_double() / original_size.to_double()
}

pub(all) struct ChunkStore {
  mut digests : Array[String]
  mut blocks : Array[Array[Byte]]
  mut inserted : Int
  mut hits : Int
}

pub fn ChunkStore::new() -> ChunkStore {
  { digests: [], blocks: [], inserted: 0, hits: 0 }
}

pub fn ChunkStore::len(self : ChunkStore) -> Int { self.digests.length() }
pub fn ChunkStore::insertions(self : ChunkStore) -> Int { self.inserted }
pub fn ChunkStore::hits(self : ChunkStore) -> Int { self.hits }

pub fn ChunkStore::has(self : ChunkStore, digest : String) -> Bool {
  index_of_digest(self.digests, digest) is Some(_)
}

pub fn ChunkStore::put(self : ChunkStore, chunk : Chunk) -> Bool {
  match index_of_digest(self.digests, chunk.id()) {
    Some(index) => {
      self.hits = self.hits + 1
      self.blocks[index] = chunk.bytes()
      false
    }
    None => {
      self.digests.push(chunk.id())
      self.blocks.push(chunk.bytes())
      self.inserted = self.inserted + 1
      true
    }
  }
}

pub fn ChunkStore::get(self : ChunkStore, digest : String) -> Array[Byte]? {
  match index_of_digest(self.digests, digest) {
    Some(index) => Some(self.blocks[index].copy())
    None => None
  }
}

pub fn ChunkStore::remove(self : ChunkStore, digest : String) -> Bool {
  match index_of_digest(self.digests, digest) {
    Some(index) => {
      let _ = self.digests.remove(index)
      let _ = self.blocks.remove(index)
      true
    }
    None => false
  }
}

pub fn ChunkStore::clear(self : ChunkStore) -> Unit {
  self.digests.clear()
  self.blocks.clear()
  self.inserted = 0
  self.hits = 0
}

pub fn ChunkStore::all_digests(self : ChunkStore) -> Array[String] {
  self.digests.copy()
}

pub fn store_chunks(store : ChunkStore, chunks : Array[Chunk]) -> Int {
  let mut added = 0
  for chunk in chunks { if store.put(chunk) { added = added + 1 } }
  added
}

pub fn restore_manifest(store : ChunkStore, manifest : Manifest) -> Result[Array[Byte], String] {
  let result = []
  for reference in manifest.references() {
    match store.get(reference.id()) {
      Some(bytes) => {
        if bytes.length() != reference.length() { return Err("stored chunk size mismatch") }
        if !digest_equal(digest_bytes(bytes), reference.id()) { return Err("stored chunk digest mismatch") }
        for byte in bytes { result.push(byte) }
      }
      None => return Err("required chunk is missing from store")
    }
  }
  if result.length() != manifest.size() { return Err("restored file size mismatch") }
  Ok(result)
}

pub fn apply_plan(
  old_store : ChunkStore,
  new_store : ChunkStore,
  old_manifest : Manifest,
  new_manifest : Manifest,
) -> Result[Array[Byte], String] {
  let _ = diff_manifests(old_manifest, new_manifest)
  for reference in new_manifest.references() {
    if !new_store.has(reference.id()) {
      match old_store.get(reference.id()) {
        Some(bytes) => {
          new_store.digests.push(reference.id())
          new_store.blocks.push(bytes)
        }
        None => return Err("plan refers to a chunk unavailable in both stores")
      }
    }
  }
  restore_manifest(new_store, new_manifest)
}

pub fn plan_reuses(plan : SyncPlan) -> Int {
  let mut count = 0
  for op in plan.operations() {
    match op {
      Reuse(..) => count = count + 1
      _ => ()
    }
  }
  count
}

pub fn plan_uploads(plan : SyncPlan) -> Int {
  let mut count = 0
  for op in plan.operations() {
    match op {
      Upload(..) => count = count + 1
      _ => ()
    }
  }
  count
}

pub fn plan_removals(plan : SyncPlan) -> Int {
  let mut count = 0
  for op in plan.operations() {
    match op {
      Remove(..) => count = count + 1
      _ => ()
    }
  }
  count
}