(* (c) Frédéric Bour
 * (c) Romain Calascibetta
 *)

module Tbl = struct
  (* XXX(dinosaure): [Tbl] is a small re-implementation
   * of [Hashtbl] where [find_all] is needed by [prj]. To
   * avoid an allocation of an intermediate list, we directly
   * use the underlying linked-list to do the projection.
   *
   * This implementation wants to be:
   * - deterministic (seed = 0)
   * - fast
   *
   * Memoization is done by [last_k]/[last_v] where the common use
   * of [Conduit] is a loop with multiple calls of [send]/[recv]
   * with the same [flow] value.
   *)

  type 'v t = {
    mutable size : int;
    mutable data : 'v lst array;
    mutable last_k : int;
    mutable last_v : 'v;
  }

  and 'v lst = Empty | Cons of { key : int; data : 'v; mutable next : 'v lst }

  let rec power_2_above x n =
    if x >= n then x
    else if x * 2 > Sys.max_array_length then x
    else power_2_above (x * 2) n

  let create ~epsilon size =
    let size = power_2_above 16 size in
    { size = 0; data = Array.make size Empty; last_k = 0; last_v = epsilon }

  external caml_hash : int -> int -> int -> 'a -> int = "caml_hash" [@@noalloc]

  let hash v = caml_hash 10 100 0 v

  let resize t =
    let old_data = t.data in
    let old_size = Array.length old_data in
    let new_size = old_size * 2 in
    if new_size < Sys.max_array_length then (
      let new_data = Array.make new_size Empty in
      let new_data_tail = Array.make new_size Empty in
      t.data <- new_data;
      let rec insert = function
        | Empty -> ()
        | Cons { key; next; _ } as cell ->
            let new_idx = hash key land (new_size - 1) in
            (match new_data_tail.(new_idx) with
            | Empty -> new_data.(new_idx) <- cell
            | Cons tail -> tail.next <- cell);
            new_data_tail.(new_idx) <- cell;
            insert next
      in
      for i = 0 to old_size - 1 do
        insert old_data.(i)
      done;
      for i = 0 to new_size - 1 do
        match new_data_tail.(i) with
        | Empty -> ()
        | Cons tail -> tail.next <- Empty
      done)

  let add t key data =
    let i = hash key land (Array.length t.data - 1) in
    let v = Cons { key; data; next = t.data.(i) } in
    t.data.(i) <- v;
    t.size <- t.size + 1;
    if t.size > Array.length t.data lsl 1 then resize t
end

module type KEY_INFO = sig
  type 'a t
end

module Make (Key_info : KEY_INFO) = struct
  type t = ..
  type 'a key = 'a Key_info.t

  module type WITNESS = sig
    type a
    type t += T of a

    val key : a key
  end

  type 'a witness = (module WITNESS with type a = 'a)
  type pack = Key : 'a key -> pack
  type value = Value : 'a * 'a key -> value

  let epsilon _ = raise_notrace Not_found
  let handlers = Tbl.create ~epsilon 0x10
  let keys = Hashtbl.create 0x10

  module Injection (M : sig
    type t

    val key : t key
  end) : WITNESS with type a = M.t = struct
    type a = M.t
    type t += T of a

    let key = M.key
    let handler = function T a -> Value (a, key) | _ -> raise Not_found

    let () =
      let[@warning "-3"] uid =
        Stdlib.Obj.Extension_constructor.id [%extension_constructor T]
      in
      Tbl.add handlers uid handler;
      Hashtbl.add keys uid (Key key)
  end

  let inj (type a) (key : a key) : a witness =
    (module Injection (struct
      type t = a

      let key = key
    end))

  (* XXX(dinosaure): we ensure that a value [t : t] must have an implementation
   * availble into [handlers]. By this way,
   * [let[@warning "-8"] Tbl.Cons _ = lst in] is safe where we must find an
   * implementation.
   *)

  let rec iter t uid lst =
    let[@warning "-8"] (Tbl.Cons { key = k; data = f; next = r; _ }) = lst in
    try
      if uid <> k then raise_notrace Not_found;
      handlers.Tbl.last_v <- f;
      f t
    with _ -> (iter [@tailcall]) t uid r

  let prj t =
    let arr = handlers.Tbl.data in
    let uid = Stdlib.Obj.Extension_constructor.(id (of_val t)) in
    if handlers.Tbl.last_k == uid then handlers.Tbl.last_v t
    else
      let res = iter t uid arr.(Tbl.hash uid land (Array.length arr - 1)) in
      handlers.Tbl.last_k <- uid;
      res

  let bindings () = Hashtbl.fold (fun _ v a -> v :: a) keys []
end
