///|
pub(all) enum PatchKind {
  PatchAdd
  PatchRemove
  PatchReplace
} derive(Eq, Debug)

///|
pub struct PatchHunk {
  kind_value : PatchKind
  address_value : UInt64
  before_value : Bytes
  after_value : Bytes
} derive(Eq, Debug)

///|
pub struct ImagePatch {
  hunk_values : Array[PatchHunk]
  old_entry_value : UInt64?
  new_entry_value : UInt64?
  added_bytes_value : Int
  removed_bytes_value : Int
  changed_bytes_value : Int
} derive(Eq, Debug)

///|
fn patch_error(message : String, address : UInt64) -> FirmwareError {
  FirmwareError::new(
    IntegrityViolation,
    message + " at address " + address.to_string(radix=16),
    SourcePosition::line(0),
  )
}

///|
fn difference_patch_kind(kind : ByteDifferenceKind) -> PatchKind {
  match kind {
    ByteAdded => PatchAdd
    ByteRemoved => PatchRemove
    ByteChanged => PatchReplace
  }
}

///|
fn optional_bytes(values : Array[Byte]) -> Bytes {
  Bytes::makei(values.length(), index => values[index])
}

///|
fn make_patch_hunk(
  kind : PatchKind,
  address : UInt64,
  before : Array[Byte],
  after : Array[Byte],
) -> PatchHunk {
  {
    kind_value: kind,
    address_value: address,
    before_value: optional_bytes(before),
    after_value: optional_bytes(after),
  }
}

///|
fn append_difference_bytes(
  difference : ByteDifference,
  before : Array[Byte],
  after : Array[Byte],
) -> Unit {
  match difference.left() {
    Some(value) => before.push(value)
    None => ()
  }
  match difference.right() {
    Some(value) => after.push(value)
    None => ()
  }
}

///|
/// Build ordered contiguous patch hunks from exact sparse-byte differences.
pub fn create_image_patch(
  source : FirmwareImage,
  target : FirmwareImage,
  max_differences? : Int = 10000,
) -> Result[ImagePatch, FirmwareError] {
  let comparison = match compare_images(source, target, max_differences~) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let differences = comparison.differences()
  let hunks : Array[PatchHunk] = []
  if differences.length() > 0 {
    let mut current_kind = difference_patch_kind(differences[0].kind())
    let mut current_address = differences[0].address()
    let mut previous_address = current_address
    let mut before : Array[Byte] = []
    let mut after : Array[Byte] = []
    append_difference_bytes(differences[0], before, after)
    for index = 1; index < differences.length(); index = index + 1 {
      let difference = differences[index]
      let kind = difference_patch_kind(difference.kind())
      if kind != current_kind || difference.address() != previous_address + 1UL {
        hunks.push(
          make_patch_hunk(current_kind, current_address, before, after),
        )
        current_kind = kind
        current_address = difference.address()
        before = []
        after = []
      }
      append_difference_bytes(difference, before, after)
      previous_address = difference.address()
    }
    hunks.push(make_patch_hunk(current_kind, current_address, before, after))
  }
  Ok({
    hunk_values: hunks,
    old_entry_value: source.entry_point(),
    new_entry_value: target.entry_point(),
    added_bytes_value: comparison.added_count(),
    removed_bytes_value: comparison.removed_count(),
    changed_bytes_value: comparison.changed_count(),
  })
}

///|
pub fn PatchHunk::kind(self : PatchHunk) -> PatchKind {
  self.kind_value
}

///|
pub fn PatchHunk::address(self : PatchHunk) -> UInt64 {
  self.address_value
}

///|
pub fn PatchHunk::before(self : PatchHunk) -> Bytes {
  Bytes::makei(self.before_value.length(), index => self.before_value[index])
}

///|
pub fn PatchHunk::after(self : PatchHunk) -> Bytes {
  Bytes::makei(self.after_value.length(), index => self.after_value[index])
}

///|
pub fn PatchHunk::length(self : PatchHunk) -> Int {
  match self.kind_value {
    PatchAdd => self.after_value.length()
    PatchRemove | PatchReplace => self.before_value.length()
  }
}

///|
pub fn ImagePatch::hunks(self : ImagePatch) -> Array[PatchHunk] {
  self.hunk_values.copy()
}

///|
pub fn ImagePatch::old_entry(self : ImagePatch) -> UInt64? {
  self.old_entry_value
}

///|
pub fn ImagePatch::new_entry(self : ImagePatch) -> UInt64? {
  self.new_entry_value
}

///|
pub fn ImagePatch::added_bytes(self : ImagePatch) -> Int {
  self.added_bytes_value
}

///|
pub fn ImagePatch::removed_bytes(self : ImagePatch) -> Int {
  self.removed_bytes_value
}

///|
pub fn ImagePatch::changed_bytes(self : ImagePatch) -> Int {
  self.changed_bytes_value
}

///|
fn verify_patch_bytes(
  image : FirmwareImage,
  hunk : PatchHunk,
) -> Result[Unit, FirmwareError] {
  match hunk.kind() {
    PatchAdd =>
      for offset = 0; offset < hunk.after_value.length(); offset = offset + 1 {
        let address = hunk.address() + offset.to_uint64()
        if image.byte_at(address) is Some(_) {
          return Err(
            patch_error("patch expected an absent source byte", address),
          )
        }
      }
    PatchRemove | PatchReplace =>
      for offset = 0; offset < hunk.before_value.length(); offset = offset + 1 {
        let address = hunk.address() + offset.to_uint64()
        if image.byte_at(address) != Some(hunk.before_value[offset]) {
          return Err(
            patch_error("patch source byte does not match evidence", address),
          )
        }
      }
  }
  Ok(())
}

///|
/// Apply a patch only when all recorded source bytes and entry metadata match.
pub fn ImagePatch::apply(
  self : ImagePatch,
  source : FirmwareImage,
) -> Result[FirmwareImage, FirmwareError] {
  if source.entry_point() != self.old_entry_value {
    return Err(
      patch_error("patch source entry point does not match evidence", 0UL),
    )
  }
  let mut current = source
  for hunk in self.hunk_values {
    match verify_patch_bytes(current, hunk) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    match hunk.kind() {
      PatchAdd | PatchReplace =>
        match current.write(hunk.address(), hunk.after()) {
          Ok(value) => current = value
          Err(error) => return Err(error)
        }
      PatchRemove => {
        let end = hunk.address() + hunk.before_value.length().to_uint64()
        match current.erase(hunk.address(), end) {
          Ok(value) => current = value
          Err(error) => return Err(error)
        }
      }
    }
  }
  FirmwareImage::from_chunks(
    image_as_chunks(current),
    entry_point=self.new_entry_value,
  )
}