///|
/// Where a signature comes from and what it promises.
///
/// A signature describes **the old data**, block by block. The side that sends
/// an update computes it from what the receiver already has; the delta encoder
/// only needs the signature. Every block carries a weak checksum (a candidate
/// filter) and a truncated strong checksum (the decision).
///
/// The limits below are contract, not tuning: they bound every later allocation
/// so that a malformed signature cannot ask the delta encoder for more memory
/// than it is willing to give.
let signature_magic : Bytes = @utf8.encode("DSIG")

///|
let delta_magic : Bytes = @utf8.encode("DDLT")

///|
let manifest_magic : Bytes = @utf8.encode("DCHK")

///|
let format_version : Int32 = 1

///|
let min_block_size : Int32 = 256

///|
let max_block_size : Int32 = 1 << 20

///|
let max_block_count : Int32 = 1_000_000

///|
let max_supported_total : UInt64 = 1UL << 40

///|
/// Everything that can be wrong with a serialized signature or delta.
pub suberror FormatError {
  BadMagic(String)
  UnsupportedVersion(Int32)
  Truncated(Int32)
  BadField(String)
  TooLarge(String)
  BlockMismatch(Int32)
  UnsupportedOp(Int32)
  DigestMismatch
}

///|
pub fn FormatError::message(self : FormatError) -> String {
  match self {
    BadMagic(found) => "bad magic: expected DSIG, found " + found
    UnsupportedVersion(version) =>
      "unsupported format version: " + version.to_string()
    Truncated(offset) => "truncated input at byte " + offset.to_string()
    BadField(field) => "invalid field: " + field
    TooLarge(field) => "value out of range: " + field
    BlockMismatch(index) =>
      "block " + index.to_string() + " does not match the signature"
    UnsupportedOp(tag) => "unsupported delta op tag: " + tag.to_string()
    DigestMismatch =>
      "the rebuilt bytes do not match the digest the delta carries"
  }
}

///|

///|
/// One block of the old data: its position, its weak checksum and the truncated
/// SHA-256 of its bytes.
pub struct BlockSignature {
  index : Int32
  weak : UInt32
  strong : Bytes
}

///|
/// The signature of a whole byte string.
pub struct FileSignature {
  block_size : Int32
  strong_len : Int32
  total_length : Int32
  blocks : Array[BlockSignature]
}

///|
/// The block size used when the caller does not choose one.
///
/// Small inputs get small blocks so a one byte edit on a small file still
/// produces a small delta; large inputs get bigger blocks so that the signature
/// and the delta op list stay proportional to `length / 8192` rather than to
/// `length`. The result is always a power of two inside
/// `[min_block_size, max_block_size]`, which keeps the format predictable.
pub fn default_block_size(length : Int32) -> Int32 {
  let target = if length < 4096 { 512 } else { 8192 }
  let mut size = min_block_size
  while size < target && size * 2 <= max_block_size {
    size = size * 2
  }
  size
}

///|
/// Sign `input` block by block. The last block may be shorter than `block_size`.
pub fn sign_bytes(
  input : BytesView,
  block_size? : Int32,
  strong_len? : Int32,
) -> FileSignature raise FormatError {
  let size = match block_size {
    Some(value) => value
    None => default_block_size(input.length())
  }
  if size < min_block_size || size > max_block_size {
    raise TooLarge(
      "block_size must be between \{min_block_size} and \{max_block_size}, got \{size}",
    )
  }
  let strong = match strong_len {
    Some(value) => value
    None => 16
  }
  if strong != 8 && strong != 16 {
    raise BadField("strong_len must be 8 or 16, got \{strong}")
  }
  let count = block_count(input.length(), size)
  if count > max_block_count {
    raise TooLarge("block count \{count} exceeds \{max_block_count}")
  }
  let blocks : Array[BlockSignature] = []
  for index = 0; index < count; index = index + 1 {
    let start = index * size
    let end = if start + size < input.length() {
      start + size
    } else {
      input.length()
    }
    let block = input.clamped_view(start~, end~)
    blocks.push({
      index,
      weak: weak_checksum(block),
      strong: sha256_prefix(block, strong),
    })
  }
  {
    block_size: size,
    strong_len: strong,
    total_length: input.length(),
    blocks,
  }
}

///|
pub fn FileSignature::block_count(self : FileSignature) -> Int32 {
  self.blocks.length()
}

///|
/// The length of the block at `index`, which is `block_size` for every block but
/// the last one. Returns `None` when the index is outside the signature.
pub fn FileSignature::block_length(
  self : FileSignature,
  index : Int32,
) -> Int32? {
  if index < 0 || index >= self.blocks.length() {
    return None
  }
  let start = index * self.block_size
  let rest = self.total_length - start
  Some(if rest < self.block_size { rest } else { self.block_size })
}

///|
/// The `DSIG` encoding of this signature.
pub fn FileSignature::encode(self : FileSignature) -> Bytes {
  let sink = ByteSink::new()
  sink.write_bytes(signature_magic[:])
  sink.write_u8(format_version)
  sink.write_u32(self.block_size.reinterpret_as_uint())
  sink.write_u8(self.strong_len)
  sink.write_u64(self.total_length.to_uint64())
  sink.write_u32(self.blocks.length().reinterpret_as_uint())
  for block in self.blocks {
    sink.write_u32(block.weak)
    sink.write_bytes(block.strong[:])
  }
  sink.to_bytes()
}

///|
/// Parse a `DSIG` byte string.
///
/// Everything a sender could get wrong is checked here: the magic, the version,
/// the strong length, and the arithmetic that ties `block_count` to
/// `total_length` and `block_size`. A signature that parses is a signature whose
/// block boundaries the delta encoder can trust.
pub fn decode_signature(bytes : BytesView) -> FileSignature raise FormatError {
  if bytes.length() < 4 {
    raise Truncated(0)
  }
  let magic = bytes.clamped_view(start=0, end=4)
  if !constant_time_equal(magic, signature_magic[:]) {
    raise BadMagic(ascii_preview(magic))
  }
  let mut cursor = 4
  let version = match read_u8(bytes, cursor) {
    Some(value) => value
    None => raise Truncated(cursor)
  }
  cursor = cursor + 1
  if version != format_version {
    raise UnsupportedVersion(version)
  }
  let block_size = match read_u32(bytes, cursor) {
    Some(value) => value
    None => raise Truncated(cursor)
  }
  cursor = cursor + 4
  let size = block_size.reinterpret_as_int()
  if size < min_block_size || size > max_block_size {
    raise TooLarge("block_size \{size} outside the supported range")
  }
  let strong_len = match read_u8(bytes, cursor) {
    Some(value) => value
    None => raise Truncated(cursor)
  }
  cursor = cursor + 1
  if strong_len != 8 && strong_len != 16 {
    raise BadField("strong_len must be 8 or 16, got \{strong_len}")
  }
  let total = match read_u64(bytes, cursor) {
    Some(value) => value
    None => raise Truncated(cursor)
  }
  cursor = cursor + 8
  if total > max_supported_total {
    raise TooLarge("total_length \{total} exceeds the supported range")
  }
  let total_length = total.to_int()
  let count = match read_u32(bytes, cursor) {
    Some(value) => value
    None => raise Truncated(cursor)
  }
  cursor = cursor + 4
  let expected = block_count(total_length, size)
  if count.reinterpret_as_int() != expected {
    raise BadField(
      "block_count \{count.reinterpret_as_int()} does not match \{expected} blocks of \{size} bytes",
    )
  }
  if expected > max_block_count {
    raise TooLarge("block count \{expected} exceeds \{max_block_count}")
  }
  let blocks : Array[BlockSignature] = []
  for index = 0; index < expected; index = index + 1 {
    let weak = match read_u32(bytes, cursor) {
      Some(value) => value
      None => raise Truncated(cursor)
    }
    cursor = cursor + 4
    let strong = match sub_view(bytes, cursor, strong_len) {
      Some(view) => view
      None => raise Truncated(cursor)
    }
    cursor = cursor + strong_len
    blocks.push({
      index,
      weak,
      strong: Bytes::from_array(strong.to_array()[:]),
    })
  }
  if cursor != bytes.length() {
    raise BadField(
      "signature has \{bytes.length() - cursor} trailing bytes after \{expected} blocks",
    )
  }
  { block_size: size, strong_len, total_length, blocks, }
}

///|
/// The SHA-256 of the `DSIG` encoding, which is what the reports quote so two
/// signatures can be compared without comparing every block.
pub fn FileSignature::digest_hex(self : FileSignature) -> String {
  sha256_hex(self.encode())
}

///|
/// Four bytes as text when they are printable ASCII, and as hexadecimal
/// otherwise. Only used to make a "bad magic" message readable.
fn ascii_preview(view : BytesView) -> String {
  let mut printable = true
  for i = 0; i < view.length(); i = i + 1 {
    let byte = view[i].to_int()
    if byte < 0x20 || byte > 0x7E {
      printable = false
    }
  }
  if printable {
    let builder = StringBuilder(size_hint=view.length())
    for i = 0; i < view.length(); i = i + 1 {
      builder.write_char(view[i].to_int().unsafe_to_char())
    }
    builder.to_string()
  } else {
    "0x" + hex_encode(view)
  }
}