///|
/// MD5 message digest (RFC 1321). FLAC stores an MD5 signature of the raw
/// audio samples in its `STREAMINFO` block, so this module lets a decoder
/// check stream integrity end-to-end. The digest is computed with the
/// standard 64-round Merkle–Damgård construction over little-endian words.

/// The 64 per-round additive constants: `floor(abs(sin(i + 1)) * 2^32)`.
let k : Array[UInt] = [
  0xd76aa478U, 0xe8c7b756U, 0x242070dbU, 0xc1bdceeeU,
  0xf57c0fafU, 0x4787c62aU, 0xa8304613U, 0xfd469501U,
  0x698098d8U, 0x8b44f7afU, 0xffff5bb1U, 0x895cd7beU,
  0x6b901122U, 0xfd987193U, 0xa679438eU, 0x49b40821U,
  0xf61e2562U, 0xc040b340U, 0x265e5a51U, 0xe9b6c7aaU,
  0xd62f105dU, 0x02441453U, 0xd8a1e681U, 0xe7d3fbc8U,
  0x21e1cde6U, 0xc33707d6U, 0xf4d50d87U, 0x455a14edU,
  0xa9e3e905U, 0xfcefa3f8U, 0x676f02d9U, 0x8d2a4c8aU,
  0xfffa3942U, 0x8771f681U, 0x6d9d6122U, 0xfde5380cU,
  0xa4beea44U, 0x4bdecfa9U, 0xf6bb4b60U, 0xbebfbc70U,
  0x289b7ec6U, 0xeaa127faU, 0xd4ef3085U, 0x04881d05U,
  0xd9d4d039U, 0xe6db99e5U, 0x1fa27cf8U, 0xc4ac5665U,
  0xf4292244U, 0x432aff97U, 0xab9423a7U, 0xfc93a039U,
  0x655b59c3U, 0x8f0ccc92U, 0xffeff47dU, 0x85845dd1U,
  0x6fa87e4fU, 0xfe2ce6e0U, 0xa3014314U, 0x4e0811a1U,
  0xf7537e82U, 0xbd3af235U, 0x2ad7d2bbU, 0xeb86d391U,
]

/// The left-rotation amount for each of the 64 rounds.
let s : Array[Int] = [
  7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
  5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
  4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
  6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
]

/// Rotate `x` left by `n` bits (0 < n < 32).
fn rotl(x : UInt, n : Int) -> UInt {
  (x << n) | (x >> (32 - n))
}

/// Read one 32-bit little-endian word from `buf` at byte offset `pos`.
fn read_le32(buf : Array[Byte], pos : Int) -> UInt {
  let b0 = buf[pos].to_int().reinterpret_as_uint()
  let b1 = buf[pos + 1].to_int().reinterpret_as_uint()
  let b2 = buf[pos + 2].to_int().reinterpret_as_uint()
  let b3 = buf[pos + 3].to_int().reinterpret_as_uint()
  b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}

/// Write `v` to `out` as a 32-bit little-endian value.
fn write_le32(out : Array[Byte], pos : Int, v : UInt) -> Unit {
  for i in 0..<4 {
    out.set(pos + i, ((v >> (8 * i)) & 0xFFU).reinterpret_as_int().to_byte())
  }
}

/// Compress one 64-byte block into the running state.
fn block(a0 : UInt, b0 : UInt, c0 : UInt, d0 : UInt, m : Array[UInt]) -> (UInt, UInt, UInt, UInt) {
  let mut a = a0
  let mut b = b0
  let mut c = c0
  let mut d = d0
  for i in 0..<64 {
    let (f, g) = if i < 16 {
      ((b & c) | (b.lnot() & d), i)
    } else if i < 32 {
      ((d & b) | (d.lnot() & c), (5 * i + 1) % 16)
    } else if i < 48 {
      (b ^ c ^ d, (3 * i + 5) % 16)
    } else {
      (c ^ (b | d.lnot()), (7 * i) % 16)
    }
    let temp = d
    d = c
    c = b
    b = b + rotl(a + f + k[i] + m[g], s[i])
    a = temp
  }
  (a0 + a, b0 + b, c0 + c, d0 + d)
}

/// Compute the 16-byte MD5 digest of `data`.
pub fn md5(data : Bytes) -> Bytes {
  let len = data.length()
  // Append a 0x80 marker, pad with zeroes to 8 bytes short of a 64-byte
  // multiple, then append the original bit length as 8 little-endian bytes.
  let padded_len = ((len + 72) / 64) * 64
  let buf = Array::make(padded_len, b'\x00')
  for i in 0..> (8 * i)) & 0xFFL).to_int().to_byte())
  }
  let mut a = 0x67452301U
  let mut b = 0xefcdab89U
  let mut c = 0x98badcfeU
  let mut d = 0x10325476U
  let mut pos = 0
  while pos < padded_len {
    let m = Array::make(16, 0U)
    for j in 0..<16 {
      m.set(j, read_le32(buf, pos + j * 4))
    }
    let (a2, b2, c2, d2) = block(a, b, c, d, m)
    a = a2
    b = b2
    c = c2
    d = d2
    pos = pos + 64
  }
  let out = Array::make(16, b'\x00')
  write_le32(out, 0, a)
  write_le32(out, 4, b)
  write_le32(out, 8, c)
  write_le32(out, 12, d)
  Bytes::from_array(out)
}

fn hex_digit(v : Int) -> Byte {
  if v < 10 {
    (48 + v).to_byte()
  } else {
    (87 + v).to_byte()
  }
}

/// Render bytes as a lowercase hexadecimal string.
pub fn hex(data : Bytes) -> String {
  let out = Array::make(data.length() * 2, b'\x00')
  for i in 0..> 4))
    out.set(i * 2 + 1, hex_digit(b & 0x0F))
  }
  @utf8.decode_lossy(Bytes::from_array(out))
}

/// The MD5 digest of `data` as a 32-character lowercase hex string.
pub fn md5_hex(data : Bytes) -> String {
  hex(md5(data))
}

/// The MD5 signature stored in a `STREAMINFO` block, as hex. Pair it with
/// `md5_hex` over the decoded samples to verify a stream's integrity.
pub fn StreamInfo::md5_hex(self : StreamInfo) -> String {
  hex(Bytes::from_array(self.md5))
}