type 'a action = Push of 'a * 'a action | Pop of 'a action | Empty

exception Invalid

external identity : 'a -> 'a = "%identity"

let valid a =
  let rec go k = function
    | Empty -> k 0
    | Push (_, a) -> go (fun len -> k (succ len)) a
    | Pop a -> go (function 0 -> raise Invalid | n -> k (pred n)) a
  in
  go identity a

let push x a = Push (x, a)
let empty = Empty
let pop a = Pop a
let () = assert (valid empty = 0)
let () = assert (valid (push 0 empty) = 1)
let () = assert (valid (push 1 (push 0 empty)) = 2)
let () = assert (valid (pop (push 0 empty)) = 0)

let () =
  assert (match valid (pop empty) with _ -> false | exception Invalid -> true)

module Peano = struct type zero = Zero type 'a succ = Succ end
module Refl = struct type ('a, 'b) t = Refl : ('a, 'a) t end

module Value = struct
  type 'a value =
    | Zero : Peano.zero value
    | Succ : 'a value -> 'a Peano.succ value

  type t = V : 'a value -> t

  let of_int n =
    let rec go k = function
      | 0 -> k (V Zero)
      | n -> go (fun (V n) -> k (V (Succ n))) (pred n)
    in
    if n < 0 then Fmt.invalid_arg "Value.of_int" else go identity n

  let to_int v =
    let rec go : type a. (int -> 'r) -> a value -> 'r =
     fun k -> function Zero -> k 0 | Succ x -> go (fun v -> k (succ v)) x
    in
    go identity v

  let () = assert (to_int Zero = 0)
  let () = assert (to_int (Succ Zero) = 1)
  let () = assert (to_int (Succ (Succ Zero)) = 2)
  let pp : t Fmt.t = fun ppf (V v) -> Fmt.int ppf (to_int v)

  let is_zero : type a. a value -> (a, Peano.zero) Refl.t option = function
    | Zero -> Some Refl.Refl
    | Succ _ -> None

  type 'a is_not_zero =
    | Is_not_zero : ('a, _ Peano.succ) Refl.t -> 'a is_not_zero

  let is_not_zero : type a. a value -> a is_not_zero option = function
    | Zero -> None
    | Succ _ -> Some (Is_not_zero Refl.Refl)
end

module Stack = struct
  type ('a, 'l) action =
    | Push : 'a * ('a, 'l) action -> ('a, 'l Peano.succ) action
    | Pop : ('a, 'l Peano.succ) action -> ('a, 'l) action
    | Empty : ('a, Peano.zero) action

  type 'a t = V : ('a, 'l) action -> 'a t

  let rec pp : type l. 'a Fmt.t -> ('a, l) action Fmt.t =
   fun pp_elt ppf -> function
    | Push (v, a) ->
        Fmt.pf ppf "@[<1>(Push %a)]" Fmt.(Dump.pair pp_elt (pp pp_elt)) (v, a)
    | Pop a -> Fmt.pf ppf "@[<1>(Pop %a)]" (pp pp_elt) a
    | Empty -> Fmt.pf ppf "Empty"

  let rec length : type a l. (a, l) action -> l Value.value = function
    | Empty -> Value.Zero
    | Pop a -> ( match length a with Value.Succ x -> x)
    | Push (_, a) -> Value.Succ (length a)

  let is_empty : type l. ('a, l) action -> (l, Peano.zero) Refl.t option =
    function
    | Empty -> Some Refl.Refl
    | Push _ -> None
    | Pop a -> (
        match length a with
        | Value.Succ Value.Zero -> Some Refl.Refl
        | Value.Succ _ -> None)

  type 'a is_not_empty =
    | Is_not_empty : ('a, _ Peano.succ) Refl.t -> 'a is_not_empty

  let is_not_empty : type l. ('a, l) action -> l is_not_empty option = function
    | Empty -> None
    | Push _ -> Some (Is_not_empty Refl.Refl)
    | Pop a -> (
        match length a with
        | Value.Succ Value.Zero -> None
        | Value.Succ (Value.Succ _) -> Some (Is_not_empty Refl.Refl))
end

open Crowbar

type tree = Tree of Value.t * tree list * bool

let rec list_of_tree (Tree (v, x, pop)) : [ `Push of Value.t | `Pop ] list =
  if pop then [ `Push v ] @ List.concat (List.map list_of_tree x) @ [ `Pop ]
  else [ `Push v ] @ List.concat (List.map list_of_tree x)

let generate : tree gen =
  let value = map [ range 30 ] Value.of_int in
  fix @@ fun m -> map [ value; list m; bool ] (fun v l pop -> Tree (v, l, pop))

let action_of_tree tree : Value.t Stack.t =
  let lst = list_of_tree tree in
  List.fold_left
    (fun (Stack.V acc) -> function
      | `Push v -> Stack.(V (Push (v, acc)))
      | `Pop -> (
          match Stack.is_not_empty acc with
          | Some (Stack.Is_not_empty Refl.Refl) -> Stack.V (Stack.Pop acc)
          | None -> bad_test ()))
    Stack.(V Empty)
    lst

(* XXX(dinosaure): [Stdlib.Queue] is oracle. *)

module Compare = struct
  exception Not_equal

  let fke q fke =
    let q' = Queue.copy q in
    try
      Ke.Fke.iter
        (fun x ->
          let x' = Queue.pop q' in
          if x <> x' then raise Not_equal)
        fke;
      true
    with Not_equal | Queue.Empty -> false

  let rke q rke =
    let q' = Queue.copy q in
    try
      Ke.Rke.iter
        (fun x ->
          let x' = Queue.pop q' in
          if x <> x' then raise Not_equal)
        rke;
      true
    with Not_equal | Queue.Empty -> false
end

let iter iter pp_name pp_elt ppf v =
  let is_first = ref true in
  let pp_elt v =
    if !is_first then is_first := false else Fmt.pf ppf "@ ";
    Fmt.pf ppf "@[%a@]" pp_elt v
  in
  Fmt.pf ppf "@[<1>(%a@ " pp_name v;
  iter pp_elt v;
  Fmt.pf ppf ")@]"

let pp_fke pp_elt = iter Ke.Fke.iter (Fmt.any "fke") pp_elt
let pp_rke pp_elt = iter Ke.Rke.iter (Fmt.any "rke") pp_elt

let rke_of_action a =
  let q =
    Ke.Rke.create ~capacity:(Value.to_int (Stack.length a)) Bigarray.Int
  in
  let rec go : type l. (Value.t, l) Stack.action -> unit = function
    | Stack.Empty -> ()
    | Stack.Push (Value.V v, a) ->
        go a;
        Ke.Rke.push q (Value.to_int v)
    | Stack.Pop a ->
        go a;
        ignore @@ Ke.Rke.pop_exn q
  in
  go a;
  q

let queue_of_action a =
  let q = Queue.create () in
  let rec go : type l. (Value.t, l) Stack.action -> unit = function
    | Stack.Empty -> ()
    | Stack.Push (Value.V v, a) ->
        go a;
        Queue.push (Value.to_int v) q
    | Stack.Pop a ->
        go a;
        ignore @@ Queue.pop q
  in
  go a;
  q

let fke_of_action a =
  let rec go : type l. (int Ke.Fke.t -> 'r) -> (Value.t, l) Stack.action -> 'r =
   fun k -> function
    | Stack.Empty -> k Ke.Fke.empty
    | Stack.Push (Value.V v, a) ->
        go
          (fun q ->
            let q = Ke.Fke.push q (Value.to_int v) in
            k q)
          a
    | Stack.Pop a ->
        go
          (fun q ->
            let _, q = Ke.Fke.pop_exn q in
            k q)
          a
  in
  go identity a

let () =
  add_test ~name:"queue" [ map [ generate ] action_of_tree ]
  @@ fun (Stack.V a) ->
  let fke = fke_of_action a in
  let rke = rke_of_action a in
  let queue = queue_of_action a in
  if not (Compare.fke queue fke) then
    failf "%a <> %a" Fmt.(Dump.queue int) queue (pp_fke Fmt.int) fke;
  if not (Compare.rke queue rke) then
    failf "%a <> %a" Fmt.(Dump.queue int) queue (pp_rke Fmt.int) rke;
  ()

let ( >>= ) = dynamic_bind
let failf fmt = Fmt.kstr fail fmt

let blit src src_off dst dst_off len =
  let a = Bigarray.Array1.sub src src_off len in
  let b = Bigarray.Array1.sub dst dst_off len in
  Bigarray.Array1.blit a b

let blit_from_string src src_off dst dst_off len =
  Bigstringaf.blit_from_string src ~src_off dst ~dst_off ~len

let () =
  add_test ~name:"compress-and-push"
    [ range 0x100 >>= bytes_fixed; range 0x100 >>= bytes_fixed ]
  @@ fun fill0 fill1 ->
  let capacity = String.length fill0 + String.length fill1 in
  let q, capacity = Ke.Rke.Weighted.create ~capacity Bigarray.Char in
  match
    Ke.Rke.Weighted.N.push q ~blit:blit_from_string ~length:String.length fill0
  with
  | Some [ fill0' ] -> (
      let fill0' = Bigstringaf.create (Bigstringaf.length fill0') in
      Ke.Rke.Weighted.compress q;
      Ke.Rke.Weighted.N.keep_exn q ~blit ~length:Bigstringaf.length fill0';
      match
        Ke.Rke.Weighted.N.push q ~blit:blit_from_string ~length:String.length
          fill1
      with
      | Some [ fill1' ] ->
          let a =
            Bigstringaf.memcmp_string fill0' 0 fill0 0 (String.length fill0)
          in
          let b =
            Bigstringaf.memcmp_string fill1' 0 fill1 0 (String.length fill1)
          in
          if a <> 0 || b <> 0 then failf "Queue differs from inputs"
      | Some _ -> failf "push returns multiple payloads"
      | None ->
          if String.length fill0 + String.length fill1 <= capacity then
            failf "push fails for unknow reason"
          else bad_test ())
  | Some _ -> failf "push returns multiple payloads."
  | None ->
      if String.length fill0 <= capacity then
        failf "push fails for unknow reason"
      else bad_test ()
