let () = Printexc.record_backtrace true

module type GRAPH = sig
  type t
  type id
  type value
  type error = private [> `Not_found ]

  val pp_error : error Fmt.t
  val preds : value -> id list
  val get : t -> id -> (value, error) result Lwt.t
  val compare : value -> value -> int
end

module Make (Q : Ke.Sigs.R) (G : GRAPH with type id = nativeint) = struct
  open Lwt

  exception Graph of G.error

  module Node = struct
    type t = { value : G.value; mutable color : [ `White | `Black ] }

    let compare a b = G.compare a.value b.value
  end

  module Pq = Psq.Make (Nativeint) (Node)
  module Map = Map.Make (Nativeint)

  let pack graph exclude roots =
    let store = Hashtbl.create 128 in
    let memoize get id =
      try
        let ret = Hashtbl.find store id in
        Lwt.return (Some ret)
      with Not_found -> (
        get id >>= function
        | Ok value ->
            let node = { Node.value; color = `White } in
            Hashtbl.add store id node;
            Lwt.return (Some node)
        | Error `Not_found -> Lwt.return None
        | Error err -> Lwt.fail (Graph err))
    in
    let preds v = G.preds v in
    let get = memoize (G.get graph) in
    let all_blacks pq =
      Pq.fold
        (fun _ -> function
          | { Node.color = `Black; _ } -> ( && ) true
          | _ -> ( && ) false)
        true pq
    in
    let propagate { Node.value; color } =
      let p = preds value in
      let q = Q.create ~capacity:(List.length p) Bigarray.Nativeint in
      let rec go () =
        match Q.pop q with
        | Some id -> (
            try
              let node = Hashtbl.find store id in
              node.Node.color <- color;
              go (List.iter (Q.push q) (preds node.Node.value))
            with Not_found -> go ())
        | None -> ()
      in
      List.iter (Q.push q) p;
      go ()
    in
    let rec garbage pq =
      if all_blacks pq then Lwt.return ()
      else
        match Pq.pop pq with
        | Some ((_, { Node.value; color = `Black }), pq) ->
            Lwt_list.fold_left_s
              (fun pq id ->
                get id >>= function
                | Some ({ Node.color = `White; _ } as node) ->
                    node.Node.color <- `Black;
                    propagate node;
                    Lwt.return (Pq.add id node pq)
                | Some node -> Lwt.return (Pq.add id node pq)
                | None -> Lwt.return pq)
              pq (preds value)
            >>= garbage
        | Some ((_, { Node.value; _ }), pq) ->
            Lwt_list.fold_left_s
              (fun pq id ->
                get id >>= function
                | None -> Lwt.return pq
                | Some node -> Lwt.return (Pq.add id node pq))
              pq (preds value)
            >>= garbage
        | None -> Lwt.return ()
    in
    let collect () =
      Hashtbl.fold
        (fun id -> function
          | { Node.color = `White; value } -> Map.add id value
          | _ -> fun acc -> acc)
        store Map.empty
    in
    Lwt_list.map_s
      (fun id ->
        get id >>= function
        | Some node -> Lwt.return (Some (id, node))
        | None -> Lwt.return None)
      roots
    >>= fun roots ->
    Lwt_list.map_s
      (fun id ->
        get id >>= function
        | Some node ->
            node.Node.color <- `Black;
            Lwt.return (Some (id, node))
        | None -> Lwt.return None)
      exclude
    >|= List.append roots
    >|= List.fold_left
          (fun acc -> function None -> acc | Some x -> x :: acc)
          []
    >|= Pq.of_list
    >>= fun pq -> garbage pq >|= collect
end

module Git = struct
  type t = unit
  type id = nativeint
  type value = { name : string; time : int64; ancestors : nativeint list }
  type error = [ `Not_found ]

  let pp_error ppf = function `Not_found -> Fmt.string ppf "`Not_found"
  let store : (id, value) Hashtbl.t = Hashtbl.create 16
  let preds { ancestors; _ } = ancestors

  let get () id =
    try Lwt.return_ok (Hashtbl.find store id)
    with Not_found -> Lwt.return_error `Not_found

  let compare { time = a; _ } { time = b; _ } = Int64.compare a b
end

module Packer = Make (Ke.Rke) (Git)

let json =
  let open Json_encoding in
  let name = req "name" string in
  let time = req "time" (conv Int64.to_string Int64.of_string string) in
  let ancestors =
    req "ancestors" (list (conv Nativeint.to_int32 Nativeint.of_int32 int32))
  in
  conv
    (fun { Git.name; time; ancestors } -> (name, time, ancestors))
    (fun (name, time, ancestors) -> { Git.name; time; ancestors })
    (obj3 name time ancestors)

type await = [ `Await ]
type error = [ `Error of Jsonm.error ]
type eoi = [ `End ]
type value = [ `Null | `Bool of bool | `String of string | `Float of float ]

let pp_json ppf v =
  let rec pp_value ppf = function
    | `Bool v -> Fmt.bool ppf v
    | `String v -> Fmt.quote Fmt.text ppf v
    | `Float v -> Fmt.float ppf v
    | `Null -> Fmt.string ppf "<null>"
    | `A l -> pp_arr ppf l
    | `O l -> pp_obj ppf l
  and pp_arr ppf arr = Fmt.(using Array.of_list (Dump.array pp_value)) ppf arr
  and pp_obj ppf obj =
    Fmt.Dump.iter_bindings
      (fun f -> List.iter (fun (k, v) -> f k v))
      Fmt.(any "object")
      Fmt.string pp_value ppf obj
  in
  pp_value ppf v

let of_database ic : Git.value list =
  let decoder = Jsonm.decoder (`Channel ic) in

  let error (`Error err) = Fmt.invalid_arg "%a" Jsonm.pp_error err in
  let end_of_input `End = Fmt.invalid_arg "Unexpected end of input" in

  let rec arr acc k =
    match Jsonm.decode decoder with
    | #await -> assert false
    | #error as v -> error v
    | #eoi as v -> end_of_input v
    | `Lexeme `Ae -> k (`A (List.rev acc))
    | `Lexeme v -> base (fun v -> arr (v :: acc) k) v
  and name n k =
    match Jsonm.decode decoder with
    | #await -> assert false
    | #error as v -> error v
    | #eoi as v -> end_of_input v
    | `Lexeme v -> base (fun v -> k (n, v)) v
  and obj acc k =
    match Jsonm.decode decoder with
    | #await -> assert false
    | #error as v -> error v
    | #eoi as v -> end_of_input v
    | `Lexeme `Oe -> k (`O (List.rev acc))
    | `Lexeme (`Name n) -> name n (fun v -> obj (v :: acc) k)
    | `Lexeme v -> Fmt.invalid_arg "Unexpected lexeme: %a" Jsonm.pp_lexeme v
  and base k = function
    | #value as v -> k v
    | `Os -> obj [] k
    | `As -> arr [] k
    | `Ae | `Oe -> Fmt.invalid_arg "Unexpected end of array/object"
    | `Name n -> Fmt.invalid_arg "Unexpected key: %s" n
  in

  let go k =
    match Jsonm.decode decoder with
    | #await -> assert false
    | #error as v -> error v
    | #eoi as v -> end_of_input v
    | `Lexeme (#Jsonm.lexeme as lexeme) -> base k lexeme
  in

  go Json_encoding.(destruct (list json))

let flat_json json : Jsonm.lexeme list =
  let rec arr acc k = function
    | [] -> k (List.rev (`Ae :: acc))
    | (#value as x) :: r -> arr (x :: acc) k r
    | `A l :: r -> arr [ `As ] (fun l -> arr (List.rev_append l acc) k r) l
    | `O l :: r -> obj [ `Os ] (fun l -> arr (List.rev_append l acc) k r) l
  and obj acc k = function
    | [] -> k (List.rev (`Oe :: acc))
    | (n, x) :: r ->
        base (fun v -> obj (List.rev_append v (`Name n :: acc)) k r) x
  and base k = function
    | `A l -> arr [ `As ] k l
    | `O l -> obj [ `Os ] k l
    | #value as x -> k [ x ]
  in

  base (fun l -> l) json

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

let show ppf map =
  let json =
    Json_encoding.(
      construct (list json) (List.map snd (Packer.Map.bindings map)))
  in
  let raw = Bytes.create 0x800 in
  let encoder = Jsonm.encoder `Manual in
  let rec write k = function
    | `Ok -> k ()
    | `Partial ->
        Fmt.string ppf (Bytes.sub_string raw 0 (Jsonm.Manual.dst_rem encoder));
        Jsonm.Manual.dst encoder raw 0 (Bytes.length raw);
        write k (Jsonm.encode encoder `Await)
  in
  let rec go k = function
    | [] -> write k (Jsonm.encode encoder `End)
    | lexeme :: r ->
        write (fun () -> go k r) (Jsonm.encode encoder (`Lexeme lexeme))
  in
  let lexemes = flat_json json in
  go identity lexemes

let run database exclude roots =
  let graph = of_database (open_in database) in
  let () =
    List.iteri
      (fun id value -> Hashtbl.add Git.store (Nativeint.of_int id) value)
      graph
  in
  match
    Lwt_main.run
      Lwt.Infix.(
        Packer.pack () exclude roots >|= Fmt.fmt "%a\n%!" Fmt.stdout show)
  with
  | () -> `Ok ()
  | exception Packer.Graph err ->
      `Error (false, Fmt.str "Retrieve an error: %a." Git.pp_error err)

open Cmdliner

let database =
  let parser s =
    if Sys.file_exists s then Ok s
    else Rresult.R.error_msgf "File %s does not exists" s
  in
  let pp = Fmt.string in
  Arg.conv (parser, pp)

let id =
  let parser s =
    match Nativeint.of_string_opt s with
    | Some n -> Ok n
    | None -> Rresult.R.error_msgf "Invalid id: %s" s
  in
  let pp = Fmt.nativeint in
  Arg.conv (parser, pp)

let database =
  let doc = "Database of graph." in
  Arg.(
    required
    & opt (some database) None
    & info [ "d"; "database" ] ~doc ~docv:"<FILE>")

let roots =
  let doc = "Roots of graph." in
  Arg.(non_empty & opt (list id) [] & info [ "r"; "roots" ] ~doc ~docv:"<LST>")

let exclude =
  let doc = "Excluded nodes of graph." in
  Arg.(
    non_empty & opt (list id) [] & info [ "e"; "exclude" ] ~doc ~docv:"<LST>")

let command =
  let doc = "Example of ke." in
  let exits = Cmd.Exit.defaults in
  Cmd.v
    (Cmd.info "packer" ~version:"dev" ~doc ~exits)
    Term.(ret (const run $ database $ exclude $ roots))

let () = Cmd.(exit @@ eval command)
