open StdLabels

open Bigarray

type 'a typ =
  | Int       : int       typ
  | Int32     : int32     typ
  | Int64     : int64     typ
  | Nativeint : nativeint typ
  | Float     : float     typ

type 'a proto =
  | Ret : 'a typ -> 'a proto
  | Abs : 'a typ * 'b proto -> ('a -> 'b) proto

let ( ** ) x y = Abs (x, y)

(* This form is easier to process programmatically. We don't expose it as
   ocamlopt takes a really really long time to compile a constant list
   of these. *)
type simplified_test = Test : string * 'a * 'a proto -> simplified_test

type test =
  | T1 : string * ('a -> 'b) * 'a typ * 'b typ -> test
  | T2 : string * ('a -> 'b -> 'c) * 'a typ * 'b typ * 'c typ -> test
  | T3 : string * ('a -> 'b -> 'c -> 'd) *
         'a typ * 'b typ * 'c typ * 'd typ -> test
  | T4 : string * ('a -> 'b -> 'c -> 'd -> 'e) *
         'a typ * 'b typ * 'c typ * 'd typ * 'e typ -> test
  | T5 : string * ('a -> 'b -> 'c -> 'd -> 'e -> 'f) *
         'a typ * 'b typ * 'c typ * 'd typ * 'e typ * 'f typ -> test
  | T6 : string * ('a -> 'b -> 'c -> 'd -> 'e -> 'f -> 'g) *
         'a typ * 'b typ * 'c typ * 'd typ * 'e typ * 'f typ * 'g typ -> test
  | T : string * 'a * 'a proto -> test

let expand_test = function
  | T1 (s, fn, a, b) -> Test (s, fn, a ** Ret b)
  | T2 (s, fn, a, b, c) -> Test (s, fn, a ** b ** Ret c)
  | T3 (s, fn, a, b, c, d) -> Test (s, fn, a ** b ** c ** Ret d)
  | T4 (s, fn, a, b, c, d, e) -> Test (s, fn, a ** b ** c ** d ** Ret e)
  | T5 (s, fn, a, b, c, d, e, f) ->
    Test (s, fn, a ** b ** c ** d ** e ** Ret f)
  | T6 (s, fn, a, b, c, d, e, f, g) ->
    Test (s, fn, a ** b ** c ** d ** e ** f ** Ret g)
  | T (s, fn, p) -> Test (s, fn, p)

let string_of : type a. a typ -> a -> string = function
  | Int       -> Int.to_string
  | Int32     -> Printf.sprintf "%ldl"
  | Int64     -> Printf.sprintf "%LdL"
  | Nativeint -> Printf.sprintf "%ndn"
  | Float     ->
      fun f -> Printf.sprintf "float_of_bits 0x%LxL" (Int64.bits_of_float f)

let rec arity : type a. a proto -> int = function
  | Ret _ -> 0
  | Abs (_, p) -> 1 + arity p

module Buffer = struct
  type t = (char, int8_unsigned_elt, c_layout) Array1.t

  let arg_size = 8

  let create ~arity : t =
    Array1.create char c_layout ((arity + 1) * arg_size)

  let clear (t : t) = Array1.fill t '\000'

  let length : t -> int = Array1.dim

  external init_c_side : ocaml_buffer:t -> c_buffer:t -> unit
    = "test_set_buffers"

  external get_int32 : t -> int -> int32 = "%caml_bigstring_get32"
  external get_int64 : t -> int -> int64 = "%caml_bigstring_get64"
  external set_int32 : t -> int -> int32 -> unit = "%caml_bigstring_set32"
  external set_int64 : t -> int -> int64 -> unit = "%caml_bigstring_set64"

  let get_int32 t ~arg = get_int32 t (arg * arg_size)
  let get_int64 t ~arg = get_int64 t (arg * arg_size)
  let set_int32 t ~arg x = set_int32 t (arg * arg_size) x
  let set_int64 t ~arg x = set_int64 t (arg * arg_size) x

  let get_nativeint, set_nativeint =
    match Sys.word_size with
    | 32 -> ((fun t ~arg -> get_int32 t ~arg |> Nativeint.of_int32),
             (fun t ~arg x -> set_int32 t ~arg (Nativeint.to_int32 x)))
    | 64 -> ((fun t ~arg -> get_int64 t ~arg |> Int64.to_nativeint),
             (fun t ~arg x -> set_int64 t ~arg (Int64.of_nativeint x)))
    | n  -> Printf.ksprintf failwith "unknown word size (%d)" n

  let get_int =
    if Sys.word_size = 32 then
      fun buf ~arg -> get_int32 buf ~arg |> Int32.to_int
    else
      fun buf ~arg -> get_int64 buf ~arg |> Int64.to_int

  let set_int =
    if Sys.word_size = 32 then
      fun buf ~arg x -> set_int32 buf ~arg (Int32.of_int x)
    else
      fun buf ~arg x -> set_int64 buf ~arg (Int64.of_int x)

  let get_float buf ~arg = get_int64 buf ~arg |> Int64.float_of_bits
  let set_float buf ~arg x = set_int64 buf ~arg (Int64.bits_of_float x)

  let get : type a. a typ -> t -> arg:int -> a = function
    | Int       -> get_int
    | Int32     -> get_int32
    | Int64     -> get_int64
    | Nativeint -> get_nativeint
    | Float     -> get_float

  let set : type a. a typ -> t -> arg:int -> a -> unit = function
    | Int       -> set_int
    | Int32     -> set_int32
    | Int64     -> set_int64
    | Nativeint -> set_nativeint
    | Float     -> set_float

  (* This is almost a memcpy except that we use get/set which should
     ensure that the values in [dst] don't overflow. *)
  let copy_args ~src ~dst proto =
    let rec loop : type a. a proto -> int -> unit = fun proto arg ->
      match proto with
      | Ret typ ->
        set typ dst ~arg (get typ src ~arg)
      | Abs (typ, rest) ->
        set typ dst ~arg (get typ src ~arg);
        loop rest (arg + 1)
    in
    loop proto 0
end

let exec proto f ~ocaml_buffer ~c_buffer =
  let rec loop : type a. a proto -> a -> int -> unit = fun proto f arg ->
    match proto with
    | Ret typ ->
      Buffer.set typ c_buffer ~arg f
    | Abs (typ, rest) ->
      let x = Buffer.get typ ocaml_buffer ~arg in
      loop rest (f x) (arg + 1)
  in
  loop proto f 0

let strings_of_test_instance name proto buffer =
  let rec loop : type a. a proto -> int -> string list -> string list * string =
    fun proto arg acc ->
      match proto with
      | Ret typ ->
        (List.rev acc, string_of typ (Buffer.get typ buffer ~arg))
      | Abs (typ, rest) ->
        let s = string_of typ (Buffer.get typ buffer ~arg) in
        loop rest (arg + 1) (s :: acc)
  in
  loop proto 0 []

let typ_size : type a. a typ -> int = function
  | Int       -> Sys.word_size / 8
  | Int32     -> 4
  | Int64     -> 8
  | Nativeint -> Sys.word_size / 8
  | Float     -> 8

let rec sizes : type a. a proto -> int list = function
  | Ret typ         -> [typ_size typ]
  | Abs (typ, rest) -> typ_size typ :: sizes rest

let print_hex ~sizes ~arity buffer =
  let printf = Printf.printf in
  printf "(";
  for i = 0 to arity do
    if i = arity then
      printf ") -> "
    else if i > 0 then
      printf ", ";
    for ofs = i * Buffer.arg_size to i * Buffer.arg_size + sizes.(i) - 1 do
      printf "%02x" (Char.code buffer.{ofs});
    done;
  done

let printed_mismatches = ref 0

let print_mismatch name proto ~ocaml_buffer ~c_buffer =
  let printf = Printf.printf in
  printf "Mismatch for %s\n" name;
  let o_args, o_res = strings_of_test_instance name proto ocaml_buffer in
  let c_args, c_res = strings_of_test_instance name proto     c_buffer in
  let o_args, c_args =
    (* Align arguments *)
    List.map2 o_args c_args ~f:(fun a b ->
      let len_a = String.length a and len_b = String.length b in
      let len = max len_a len_b in
      (Printf.sprintf "%*s" len a,
       Printf.sprintf "%*s" len b))
    |> List.split
  in
  printf "ocaml side : (%s) -> %s\n" (String.concat ~sep:", " o_args) o_res;
  printf "c side     : (%s) -> %s\n" (String.concat ~sep:", " c_args) c_res;
  let sizes = sizes proto |> Array.of_list in
  let arity = arity proto in
  printf "ocaml side : "; print_hex ~sizes ~arity ocaml_buffer; printf "\n";
  printf "c side     : "; print_hex ~sizes ~arity     c_buffer; printf "\n";
  incr printed_mismatches;
  if !printed_mismatches >= 1000 then begin
    printf "Output truncated at 1000 failures.";
    exit 0
  end

external cleanup_normal
  :  int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int -> int -> int -> int -> int -> int -> int -> int
  -> int = "" "test_cleanup_normal" [@@noalloc]

external cleanup_float
  :  float -> float -> float -> float -> float -> float -> float -> float
  -> float -> float -> float -> float -> float -> float -> float -> float
  -> float -> float -> float -> float -> float -> float -> float -> float
  -> float -> float -> float -> float -> float -> float -> float -> float
  -> float = "" "test_cleanup_float" [@@noalloc] [@@unboxed]

let cleanup_args_and_stack () =
  let _ : int =
    cleanup_normal
      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
      0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  in
  let _ : float =
    cleanup_float
       0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
       0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
  in
  ()

let run_test ~random_data ~ocaml_buffer ~c_buffer (Test (name, f, proto)) =
  Buffer.clear ocaml_buffer;
  Buffer.clear c_buffer;
  Buffer.copy_args ~src:random_data ~dst:ocaml_buffer proto;
  cleanup_args_and_stack ();
  exec proto f ~ocaml_buffer ~c_buffer;
  let success = ocaml_buffer = c_buffer in
  if not success then print_mismatch name proto ~ocaml_buffer ~c_buffer;
  success

let run_tests tests =
  let tests = List.map tests ~f:expand_test in
  let max_args =
    List.fold_left tests ~init:0 ~f:(fun acc (Test (_, _, p)) ->
      max acc (arity p))
  in

  let ocaml_buffer = Buffer.create ~arity:max_args
  and     c_buffer = Buffer.create ~arity:max_args in
  Buffer.init_c_side ~ocaml_buffer ~c_buffer;

  let random_data = Buffer.create ~arity:max_args in
  let new_random_data () =
    for i = 0 to Buffer.length random_data - 1 do
      random_data.{i} <- char_of_int (Random.int 256)
    done
  in

  let failure = ref false in
  for i = 1 to 1000 do
    new_random_data ();
    List.iter tests ~f:(fun test ->
      if not (run_test ~random_data ~ocaml_buffer ~c_buffer test) then
        failure := true)
  done;
  exit (if !failure then 1 else 0)
