open Import

module Array = struct
  type nonrec t = Bytes.t

  let words = 8
  let[@inline] length t = Bytes.length t / words
  let[@inline] unsafe_get t i = Int64.to_int (Bytes.get_int64_ne t (i * words))
  let[@inline] unsafe_set t i x = Bytes.set_int64_ne t (i * words) (Int64.of_int x)

  let[@inline] make len x =
    let t = Bytes.create (len * words) in
    for i = 0 to length t - 1 do
      unsafe_set t i x
    done;
    t
  ;;

  let[@inline] make_absent len = Bytes.make (len * words) '\255'
  let clear t = Bytes.fill t 0 (Bytes.length t) '\255'

  let fold_left t ~init ~f =
    let init = ref init in
    for i = 0 to length t - 1 do
      init := f !init (unsafe_get t i)
    done;
    !init
  ;;
end

(* A specialized hash table that makes the following trade-offs:
   - Open addresing. Bucketing is quite memory intensive and dune is already
     a memory hog.
   - No boxing for empty slots. We make use of the fact that id's are never
     negative to achieve this.
   - No saving of the hash. Recomputing the hash for id's is a no-op.
*)

type nonrec table =
  { mutable table : Array.t
  ; mutable size : int
  }

type t = table Option.t ref

let init t =
  if Option.is_none !t then t := Option.some { size = 0; table = Array.make 0 (-1) };
  Option.get !t
;;

let[@inline] should_grow t =
  let slots = Array.length t.table in
  slots = 0 || (t.size > 0 && slots / t.size < 2)
;;

let absent = -1

let () =
  let x = Array.make_absent 1 in
  assert (Array.unsafe_get x 0 = absent)
;;

let create () = ref None

let[@inline] index_of_offset slots index i =
  let i = index + !i in
  if i >= slots then i - slots else i
;;

let clear t =
  match !t with
  | None -> ()
  | Some t ->
    t.size <- 0;
    Array.clear t.table
;;

let add t x =
  let hash = Int.hash x in
  let slots = Array.length t.table in
  let index = hash land (slots - 1) in
  let inserting = ref true in
  let i = ref 0 in
  while !inserting do
    let idx = index_of_offset slots index i in
    let elem = Array.unsafe_get t.table idx in
    if elem = absent
    then (
      Array.unsafe_set t.table idx x;
      inserting := false)
    else incr i
  done;
  t.size <- t.size + 1
;;

let resize t =
  let old_table = t.table in
  let slots = Array.length old_table in
  let table = Array.make_absent (if slots = 0 then 1 else slots lsl 1) in
  t.table <- table;
  for i = 0 to slots - 1 do
    let elem = Array.unsafe_get old_table i in
    if elem <> absent then add t elem
  done
;;

let add t x =
  let t = init t in
  if should_grow t then resize t;
  add t x
;;

let[@inline] is_empty t =
  let t = !t in
  if Option.is_none t
  then true
  else (
    let t = Option.get t in
    t.size = 0)
;;

let mem t x =
  let t = !t in
  if Option.is_none t || (Option.get t).size = 0
  then false
  else (
    let t = Option.get t in
    let hash = Int.hash x in
    let slots = Array.length t.table in
    let index = hash land (slots - 1) in
    let i = ref 0 in
    let found = ref false in
    while (not !found) && !i < slots do
      let idx = index_of_offset slots index i in
      let elem = Array.unsafe_get t.table idx in
      if Int.equal elem x
      then found := true
      else if Int.equal elem absent
      then i := slots
      else incr i
    done;
    !found)
;;

let pp fmt t =
  let { table; size } = init t in
  let table =
    Array.fold_left table ~init:[] ~f:(fun acc i -> if i = absent then acc else i :: acc)
    |> List.rev
    |> Stdlib.Array.of_list
  in
  let table fmt () = Fmt.sexp fmt "table" Fmt.(array int) table in
  let size fmt () = Fmt.sexp fmt "size" Fmt.int size in
  Format.fprintf fmt "%a@.%a@." table () size ()
;;
