// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// A RIPEMD-160 message-digest algorithm implementation based on
// [The RIPEMD-160 page] https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
// Note that RIPEMD-160 is no longer considered a strong hash;
// unless mandated, more secure alternatives should be preferred.

///|
struct RIPEMD160 {
  reg : FixedArray[UInt] // state h0 h1 h2 h3 h4
  mut len : UInt64
  buf : FixedArray[Byte]
  mut buf_index : Int
}

///|
pub impl CryptoHasher for RIPEMD160 with fn size(_self : RIPEMD160) -> Int {
  20
}

///|
pub impl CryptoHasher for RIPEMD160 with fn block_size(_self : RIPEMD160) -> Int {
  64
}

///|
pub impl CryptoHasher for RIPEMD160 with fn reset(self : RIPEMD160) -> Unit {
  self.reg[0] = 0x67452301
  self.reg[1] = 0xefcdab89
  self.reg[2] = 0x98badcfe
  self.reg[3] = 0x10325476
  self.reg[4] = 0xc3d2e1f0
  self.len = 0
  self.buf.fill(0)
  self.buf_index = 0
}

///|
/// Instantiate a RIPEMD-160 context
pub fn RIPEMD160::new() -> RIPEMD160 {
  {
    reg: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0],
    len: 0,
    buf: FixedArray::make(64, Byte::default()),
    buf_index: 0,
  }
}

// auxiliary functions of the left (f) and right (g) lines

///|
fn ripemd_f1(x : UInt, y : UInt, z : UInt) -> UInt {
  x ^ y ^ z
}

///|
fn ripemd_f2(x : UInt, y : UInt, z : UInt) -> UInt {
  (x & y) | (x.lnot() & z)
}

///|
fn ripemd_f3(x : UInt, y : UInt, z : UInt) -> UInt {
  (x | y.lnot()) ^ z
}

///|
fn ripemd_f4(x : UInt, y : UInt, z : UInt) -> UInt {
  (x & z) | (y & z.lnot())
}

///|
fn ripemd_f5(x : UInt, y : UInt, z : UInt) -> UInt {
  x ^ (y | z.lnot())
}

// message word selection tables of the left (r1) and right (r2) lines

///|
let ripemd_r1 : FixedArray[Int] = [
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3,
  12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
  1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10,
  14, 1, 3, 8, 11, 6, 15, 13,
]

///|
let ripemd_r2 : FixedArray[Int] = [
  5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10,
  14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
  8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7,
  6, 2, 13, 14, 0, 3, 9, 11,
]

// rotation tables of the left (s1) and right (s2) lines

///|
let ripemd_s1 : FixedArray[Int] = [
  11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7,
  15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5,
  12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11,
  6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6,
]

///|
let ripemd_s2 : FixedArray[Int] = [
  8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9,
  11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13,
  7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12,
  5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11,
]

// round constants of the left (k1) and right (k2) lines

///|
let ripemd_k1 : FixedArray[UInt] = [
  0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e,
]

///|
let ripemd_k2 : FixedArray[UInt] = [
  0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000,
]

// the left line uses f1..f5 in rounds 0..4, the right line uses them in reverse

///|
fn ripemd_f_left(round : Int, x : UInt, y : UInt, z : UInt) -> UInt {
  if round == 0 {
    ripemd_f1(x, y, z)
  } else if round == 1 {
    ripemd_f2(x, y, z)
  } else if round == 2 {
    ripemd_f3(x, y, z)
  } else if round == 3 {
    ripemd_f4(x, y, z)
  } else {
    ripemd_f5(x, y, z)
  }
}

///|
fn ripemd_f_right(round : Int, x : UInt, y : UInt, z : UInt) -> UInt {
  if round == 0 {
    ripemd_f5(x, y, z)
  } else if round == 1 {
    ripemd_f4(x, y, z)
  } else if round == 2 {
    ripemd_f3(x, y, z)
  } else if round == 3 {
    ripemd_f2(x, y, z)
  } else {
    ripemd_f1(x, y, z)
  }
}

///|
#inline
fn RIPEMD160::transform(
  state : FixedArray[UInt],
  input : FixedArray[UInt],
) -> Unit {
  // parse_le_u32x16_into always supplies exactly 16 words; every table
  // word index used below is therefore in bounds.
  let mut a1 = state[0]
  let mut b1 = state[1]
  let mut c1 = state[2]
  let mut d1 = state[3]
  let mut e1 = state[4]
  let mut a2 = state[0]
  let mut b2 = state[1]
  let mut c2 = state[2]
  let mut d2 = state[3]
  let mut e2 = state[4]
  for round in 0..<5 {
    let k1 = ripemd_k1.unsafe_get(round)
    let k2 = ripemd_k2.unsafe_get(round)
    for i in 0..<16 {
      let idx = round * 16 + i
      let t1 = rotate_left_u(
          a1 +
          ripemd_f_left(round, b1, c1, d1) +
          input.unsafe_get(ripemd_r1.unsafe_get(idx)) +
          k1,
          ripemd_s1.unsafe_get(idx),
        ) +
        e1
      a1 = e1
      e1 = d1
      d1 = rotate_left_u(c1, 10)
      c1 = b1
      b1 = t1
      let t2 = rotate_left_u(
          a2 +
          ripemd_f_right(round, b2, c2, d2) +
          input.unsafe_get(ripemd_r2.unsafe_get(idx)) +
          k2,
          ripemd_s2.unsafe_get(idx),
        ) +
        e2
      a2 = e2
      e2 = d2
      d2 = rotate_left_u(c2, 10)
      c2 = b2
      b2 = t2
    }
  }
  let t = state[1] + c1 + d2
  state[1] = state[2] + d1 + e2
  state[2] = state[3] + e1 + a2
  state[3] = state[4] + a1 + b2
  state[4] = state[0] + b1 + c2
  state[0] = t
}

///|
pub fn RIPEMD160::update_from_iter(self : RIPEMD160, data : Iter[Byte]) -> Unit {
  let input = FixedArray::make(16, 0U)
  data.each(fn(b) {
    self.buf[self.buf_index] = b
    self.buf_index += 1
    if self.buf_index == 64 {
      self.buf_index = 0
      self.len += 512UL
      parse_le_u32x16_into(self.buf, 0, input)
      RIPEMD160::transform(self.reg, input)
    }
  })
}

///|
pub impl CryptoHasher for RIPEMD160 with fn update(
  self : RIPEMD160,
  data : BytesView,
) -> Unit {
  self.update(data)
}

///|
/// update the state of given context from new `data`
pub fn[Data : ByteSource] RIPEMD160::update(
  self : RIPEMD160,
  data : Data,
) -> Unit {
  let input = FixedArray::make(16, 0U)
  let data_len = data.length()
  let mut offset = 0
  while offset < data_len {
    let min_len = @cmp.minimum(64 - self.buf_index, data_len - offset)
    data.blit_to(
      self.buf,
      len=min_len,
      src_offset=offset,
      dst_offset=self.buf_index,
    )
    self.buf_index += min_len
    if self.buf_index == 64 {
      self.len += 512UL
      self.buf_index = 0
      parse_le_u32x16_into(self.buf, 0, input)
      RIPEMD160::transform(self.reg, input)
    }
    offset += min_len
  }
}

///|
pub fn RIPEMD160::finalize(self : RIPEMD160) -> FixedArray[Byte] {
  let ret = FixedArray::make(20, Byte::default())
  self._finalize_into(ret)
  ret
}

///|
fn RIPEMD160::_finalize_into(
  self : RIPEMD160,
  buffer : FixedArray[Byte],
  offset? : Int = 0,
) -> Unit {
  // Copy data
  let data = FixedArray::make(64, Byte::default())
  let input = FixedArray::make(16, 0U)
  let mut cnt = self.buf_index
  let len = self.len + 8 * cnt.to_uint64()
  self.buf.blit_to(data, len=cnt)
  let reg = self.reg.copy()

  // Padding
  data[cnt] = b'\x80'
  cnt += 1
  if cnt > 56 {
    parse_le_u32x16_into(data, 0, input)
    RIPEMD160::transform(reg, input)
    data.fill(0)
  }
  // little-endian 64-bit bit length at bytes 56..64
  data.unsafe_set(56, len.to_byte())
  data.unsafe_set(57, (len >> 8).to_byte())
  data.unsafe_set(58, (len >> 16).to_byte())
  data.unsafe_set(59, (len >> 24).to_byte())
  data.unsafe_set(60, (len >> 32).to_byte())
  data.unsafe_set(61, (len >> 40).to_byte())
  data.unsafe_set(62, (len >> 48).to_byte())
  data.unsafe_set(63, (len >> 56).to_byte())
  parse_le_u32x16_into(data, 0, input)
  RIPEMD160::transform(reg, input)

  // Write result to buffer
  arr_u32_to_u8le_into(reg.iter(), buffer, offset)
}

///|
pub impl CryptoHasher for RIPEMD160 with fn finalize_into(
  self : RIPEMD160,
  buffer : FixedArray[Byte],
  offset~ : Int,
) -> Unit {
  self._finalize_into(buffer, offset~)
}

///|
/// Compute the RIPEMD-160 digest of some `data`.
/// - Note that RIPEMD-160 is no longer considered a strong hash;
/// unless mandated, more secure alternatives should be preferred.
pub fn[Data : ByteSource] ripemd160(data : Data) -> FixedArray[Byte] {
  RIPEMD160::new()..update(data).finalize()
}

///|
pub fn ripemd160_from_iter(data : Iter[Byte]) -> FixedArray[Byte] {
  RIPEMD160::new()..update_from_iter(data).finalize()
}

///|
test {
  inspect(
    bytes_to_hex_string(
      ripemd160(
        b"\x61\x62\x63", // abc in utf-8
      ),
    ),
    content="8eb208f7e05d987a9b044a8e98c6b087f15a0bfc",
  )
  inspect(
    bytes_to_hex_string(ripemd160(b"")),
    content="9c1185a5c5e9fc54612808977ee8f548b2258d31",
  )
  let hash1 = "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"
  let ctx = RIPEMD160::new()
  ctx.update(b"\x61".to_fixedarray())
  ctx.update(b"\x62".to_fixedarray())
  ctx.update(b"\x63".to_fixedarray())
  assert_eq(hash1, bytes_to_hex_string(ctx.finalize()))
  let ctx = RIPEMD160::new()
  for i = 0; i < 3; i = i + 1 {
    ctx.update_from_iter(b"\x61\x62\x63".iter())
  }
  inspect(
    bytes_to_hex_string(ctx.finalize()),
    content="357caa7408a576d25c8f06853ebb1746565573b7",
  )
}

///|
test "ripemd160 reentry" {
  let string = b"abcd"
  let ctx = RIPEMD160::new()
  ctx.update(string)
  inspect(
    bytes_to_hex_string(ctx.finalize()),
    content="2e7e536fd487deaa943fda5522d917bdb9011b7a",
  )
  ctx.update(string)
  inspect(
    bytes_to_hex_string(ctx.finalize()),
    content="f169f36789db8960d94bbced93290098f04b10eb",
  )
  ctx.update(string)
  inspect(
    bytes_to_hex_string(ctx.finalize()),
    content="4bcf93042b478aa5c2f41be1d179814b35a88e00",
  )
}

///|
pub extend RIPEMD160 with CryptoHasher::{reset, finalize_into, size, block_size}