let nybble_to_hex_nybble n =
  if n >= 0 && n < 10
  then Char.chr (n + 48)
  else if n < 16
  then Char.chr (n + 97 - 10)
  else failwith (Printf.sprintf "Illegal hex nybble value: %d" n)
;;

let byte_to_hex_bytes b =
  let l = b lsr 4 in
  let r = b land 15 in
  nybble_to_hex_nybble l, nybble_to_hex_nybble r
;;

let encode_hex (src : bytes) : bytes =
  let open Bytes in
  let dst = create (length src * 2) in
  for i = 0 to length src - 1 do
    let l, r = byte_to_hex_bytes (Char.code (get src i)) in
    set dst (i * 2) l;
    set dst ((i * 2) + 1) r
  done;
  dst
;;

(* Left-rotate for 32-bit words using Int32 *)
let rotl32 (v : Int32.t) (bits : int) : Int32.t =
  Int32.logor (Int32.shift_left v bits) (Int32.shift_right_logical v (32 - bits))
;;

(* Convert a 32-bit Int32.t to 4 bytes (big-endian) *)
let number_to_32_bit_be (n : Int32.t) : bytes =
  let open Bytes in
  let result = create 4 in
  set result 0 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical n 24) 0xffl)));
  set result 1 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical n 16) 0xffl)));
  set result 2 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical n 8) 0xffl)));
  set result 3 (Char.chr (Int32.to_int (Int32.logand n 0xffl)));
  result
;;

(* Convert 4 big-endian bytes to a 32-bit Int32.t *)
let number_of_32_bit_be (bts : bytes) : Int32.t =
  let open Bytes in
  let open Int32 in
  let b0 = of_int (Char.code (get bts 0)) in
  let b1 = of_int (Char.code (get bts 1)) in
  let b2 = of_int (Char.code (get bts 2)) in
  let b3 = of_int (Char.code (get bts 3)) in
  logor (shift_left b0 24)
    (logor (shift_left b1 16) (logor (shift_left b2 8) b3))
;;

(* SHA-1 digest *)
let digest (bytes : bytes) : bytes =
  let open Int32 in
  let h0 = ref 0x67452301l in
  let h1 = ref 0xEFCDAB89l in
  let h2 = ref 0x98BADCFEl in
  let h3 = ref 0x10325476l in
  let h4 = ref 0xC3D2E1F0l in

  let preprocess _a =
    let length_mod_512 = (Bytes.length _a * 8) mod 512 in
    let length_to_add =
      if 512 - length_mod_512 >= 65
      then 512 - length_mod_512
      else 512 - length_mod_512 + 512
    in
    let to_append = Bytes.make (length_to_add / 8) '\x00' in
    Bytes.set to_append 0 '\x80';
    Bytes.blit
      (number_to_32_bit_be (of_int (Bytes.length _a * 8)))
      0
      to_append
      (Bytes.length to_append - 4)
      4;
    Bytes.concat (Bytes.of_string "") [ _a; to_append ]
  in

  let preprocessed = preprocess bytes in
  let preprocessed_length = Bytes.length preprocessed in
  assert (preprocessed_length mod 64 = 0);
  for i = 0 to (preprocessed_length / 64) - 1 do
    let chunk = Bytes.sub preprocessed (i * 64) 64 in
    let w =
      Array.init 80 (fun k ->
          if k < 16
          then number_of_32_bit_be (Bytes.sub chunk (k * 4) 4)
          else 0l)
    in
    for j = 16 to 79 do
      w.(j) <- rotl32 (logxor (logxor w.(j - 3) w.(j - 8)) (logxor w.(j - 14) w.(j - 16))) 1
    done;
    let a = ref !h0 in
    let b = ref !h1 in
    let c = ref !h2 in
    let d = ref !h3 in
    let e = ref !h4 in
    let temp = ref 0l in
    let k = ref 0l in
    let f = ref 0l in
    (* main loop *)
    for j = 0 to 79 do
      if 0 <= j && j < 20
      then (
        f := logxor (logand !b !c) (logand (lognot !b) !d);
        k := 0x5A827999l)
      else if 20 <= j && j < 40
      then (
        f := logxor !b (logxor !c !d);
        k := 0x6ED9EBA1l)
      else if 40 <= j && j < 60
      then (
        f := logor (logor (logand !b !c) (logand !b !d)) (logand !c !d);
        k := 0x8F1BBCDCl)
      else (
        f := logxor !b (logxor !c !d);
        k := 0xCA62C1D6l);
      temp := add (add (add (add (rotl32 !a 5) !f) !e) !k) w.(j);
      e := !d;
      d := !c;
      c := rotl32 !b 30;
      b := !a;
      a := !temp
    done;
    h0 := add !h0 !a;
    h1 := add !h1 !b;
    h2 := add !h2 !c;
    h3 := add !h3 !d;
    h4 := add !h4 !e
  done;
  Bytes.concat
    (Bytes.of_string "")
    (List.map number_to_32_bit_be [ !h0; !h1; !h2; !h3; !h4 ])
;;
