type change = Unchanged | Added | Subtracted | Updated of string list

exception Incommensurable
exception Empty_comparison
exception Nonexistent_child

module Diff_tree = struct
    type t = { left: Config_tree.t;
               right: Config_tree.t;
               add: Config_tree.t;
               sub: Config_tree.t;
               del: Config_tree.t;
               inter: Config_tree.t;
             }
end

module Diff_string = struct
    type t = { left: Config_tree.t;
               right: Config_tree.t;
               skel: Config_tree.t;
               ppath: string list;
               udiff: string;
             }
end

module Diff_cstore = struct
    type t = { left: Config_tree.t;
               right: Config_tree.t;
               handle: int;
             }
end

type _ result =
    | Diff_tree : Diff_tree.t -> Diff_tree.t result
    | Diff_string : Diff_string.t -> Diff_string.t result
    | Diff_cstore : Diff_cstore.t -> Diff_cstore.t result

let eval_result : type a. a result -> a = function
    | Diff_tree x -> x
    | Diff_string x -> x
    | Diff_cstore x -> x

type 'a diff_func = ?recurse:bool -> string list -> 'a result -> change -> 'a result

let make_diff_trees l r = Diff_tree { left = l; right = r;
                                  add = (Config_tree.make "");
                                  sub = (Config_tree.make "");
                                  del = (Config_tree.make "");
                                  inter = (Config_tree.make "");
}

let make_diff_string l r = Diff_string {
                               left = l; right = r;
                               skel = (Config_tree.make "");
                               ppath = [];
                               udiff = "";
                           }

let name_of n = Vytree.name_of_node n
let data_of n = Vytree.data_of_node n
let children_of n = Vytree.children_of_node n
let make data name children = Vytree.make_full data name children

module ValueOrd = struct
    type t = string
    let compare a b =
        Util.lexical_numeric_compare a b
end
module ValueS = Set.Make(ValueOrd)

module TreeOrd = struct
    type t = Config_tree.t
    let compare a b =
        Util.lexical_numeric_compare (name_of a) (name_of b)
end
module ChildrenS = Set.Make(TreeOrd)

let (^~) (node : Config_tree.t) (node' : Config_tree.t) =
  name_of node = name_of node' &&
  (data_of node).values <> (data_of node').values

let left_opt_pairs n m =
    (children_of n) |> List.map (fun x ->
        let maybe_node =
            (children_of m) |> List.find_opt (fun y ->
                name_of y = name_of x) in
        (Some x, maybe_node))

let right_opt_pairs n m =
    (children_of m) |> List.map (fun y ->
        let maybe_node =
            (children_of n) |> List.find_opt (fun x ->
                name_of x = name_of y) in
        (maybe_node, Some y))

(* this is module option 'compare', but with Some _ preceding None, which is
   useful for maintaing left-right -> top-down order for show_diff
 *)
let opt_cmp o0 o1 =
    match o0, o1 with
    | Some v0, Some v1 -> compare (name_of v0) (name_of v1)
    | None, None -> 0
    | None, Some _ -> 1
    | Some _, None -> -1

let tuple_cmp t1 t2 =
    match t1, t2 with
    | (x1, y1), (x2, y2) ->
            let first = opt_cmp x1 x2 in
            if first <> 0 then first else opt_cmp y1 y2

let opt_zip n m =
    left_opt_pairs n m @ right_opt_pairs n m |> List.sort_uniq tuple_cmp

let get_opt_name left_opt right_opt =
    match left_opt, right_opt with
    | Some left_node, None -> name_of left_node
    | None, Some right_node -> name_of right_node
    | Some left_node, Some _ -> name_of left_node
    | None, None -> raise Empty_comparison

let update_path path left_opt right_opt =
    let name = get_opt_name left_opt right_opt in
    if name = "" then path
    else path @ [name]

(* tree diff algorithm: walk the tree pair, calling a function of type
   diff_func for each comparison.
   The idea of matching on pairs of (node opt) is from
   https://github.com/LukeBurgessYeo/tree-diff
 *)
let rec diff (path : string list) (f : 'a diff_func) (res: 'a result) ((left_node_opt, right_node_opt) : Config_tree.t option * Config_tree.t option) =
    let path = update_path path left_node_opt right_node_opt in
    match left_node_opt, right_node_opt with
    | None, None -> raise Empty_comparison
    | Some _, None -> f path res Subtracted
    | None, Some _ -> f path res Added
    | Some left_node, Some right_node when left_node = right_node ->
        f ~recurse:true path res Unchanged
    | Some left_node, Some right_node when left_node ^~ right_node ->
        let values = (data_of right_node).values in
        f path res (Updated values)
    | Some left_node, Some right_node ->
        let ret = f ~recurse:false path res Unchanged in
        List.fold_left (diff path f) ret (opt_zip left_node right_node)

(* copy node paths between trees *)
let rec clone_path ?(recurse=true) ?(set_values=None) old_root new_root path_done path_remaining =
    match path_remaining with
    | [] | [_] ->
        let path_total = path_done @ path_remaining in
        let old_node = Vytree.get old_root path_total in
        let data =
            match set_values with
            | Some v -> { (data_of old_node) with Config_tree.values = v }
            | None -> data_of old_node
        in
        if recurse then
            Vytree.insert ~position:Lexical ~children:(children_of old_node) new_root path_total data
        else
            Vytree.insert ~position:Lexical new_root path_total data
    | name :: names ->
        let path_done = path_done @ [name] in
        let old_node = Vytree.get old_root path_done in
        let new_root = Vytree.insert ~position:Lexical new_root path_done (data_of old_node) in
        clone_path ~recurse:recurse ~set_values:set_values old_root new_root path_done names

let clone ?(recurse=true) ?(set_values=None) old_root new_root path =
    match path with
    | [] -> if recurse then old_root else new_root
    | _ ->
            let path_existing = Vytree.get_existent_path new_root path in
            let path_remaining = Vylist.complement path path_existing in
            clone_path ~recurse:recurse ~set_values:set_values old_root new_root path_existing path_remaining

let is_empty l = (l = [])

(* define the diff_func *)
let decorate_trees ?(recurse=true) (path : string list) (Diff_tree res) (m : change) =
    match m with
    | Added -> Diff_tree {res with add = clone res.right res.add path; }
    | Subtracted ->
        Diff_tree {res with sub = clone res.left res.sub path;
         del = clone ~recurse:false ~set_values:(Some []) res.left res.del path; }
    | Unchanged ->
        Diff_tree {res with inter = clone ~recurse:recurse res.left res.inter path; }
    | Updated v ->
            (* if in this case, node at path is guaranteed to exist *)
            let ov = Config_tree.get_values res.left path in
            match ov, v with
            | [_], [_] -> Diff_tree {res with sub = clone res.left res.sub path;
                           del = clone res.left res.del path;
                           add = clone res.right res.add path; }
            | _, _ -> let ov_set = ValueS.of_list ov in
                      let v_set = ValueS.of_list v in
                      let sub_vals = ValueS.elements (ValueS.diff ov_set v_set) in
                      let add_vals = ValueS.elements (ValueS.diff v_set ov_set) in
                      let inter_vals = ValueS.elements (ValueS.inter ov_set v_set) in
                      let sub_tree =
                          if not (is_empty sub_vals) then
                              clone ~set_values:(Some sub_vals) res.left res.sub path
                          else
                              res.sub
                      in
                      let del_tree =
                          if not (is_empty sub_vals) then
                              if (is_empty add_vals) && (is_empty inter_vals) then
                                  (* delete whole node, not just values *)
                                  clone ~set_values:(Some []) res.left res.del path
                              else
                                  clone ~set_values:(Some sub_vals) res.left res.del path
                          else
                              res.del
                      in
                      let add_tree =
                          if not (is_empty add_vals) then
                            clone ~set_values:(Some add_vals) res.right res.add path
                          else
                              res.add
                      in
                      let inter_tree =
                          if not (is_empty inter_vals) then
                              clone ~set_values:(Some inter_vals) res.left res.inter path
                          else
                              res.inter
                      in Diff_tree { res with add = add_tree;
                           sub = sub_tree;
                           del = del_tree;
                           inter = inter_tree; }

(* get sub trees for path-relative comparison *)
let tree_at_path path node =
    try
        let node = Vytree.get node path in
        make Config_tree.default_data "" [node]
    with Vytree.Nonexistent_path -> raise Empty_comparison

(* call recursive diff on config_trees with decorate_trees as the diff_func *)
let compare path left right =
    if (name_of left) <> (name_of right) then
        raise Incommensurable
    else
        let (left, right) = if not (path = []) then
            (tree_at_path path left, tree_at_path path right) else (left, right) in
        let trees = make_diff_trees left right in
        let d = diff [] decorate_trees trees (Option.some left, Option.some right)
        in eval_result d

(* wrapper to return diff trees *)
let diff_tree path left right =
    let trees = compare path left right in
    let add_node = make Config_tree.default_data "add" (children_of (trees.add)) in
    let sub_node = make Config_tree.default_data "sub" (children_of (trees.sub)) in
    let del_node = make Config_tree.default_data "del" (children_of (trees.del)) in
    let int_node = make Config_tree.default_data "inter" (children_of (trees.inter)) in
    let ret = make Config_tree.default_data "" [add_node; sub_node; del_node; int_node] in
    ret

(* the following builds a diff_func to return a unified diff string of
   configs or config commands
 *)
let list_but_last l =
    let len = List.length l in
    List.filteri (fun i _ -> i < len - 1) l

let path_to_string (path: string list) =
    Printf.sprintf "[%s]\n" (String.concat " " path)

let marked_render mark node =
    let lines = Config_tree.render_config node in
    let l = String.split_on_char '\n' lines in
    let m =
        List.map (fun s -> if (String.length s) > 0 then mark ^ s else s) l in
    String.concat "\n" m

let added_lines ?(cmds=false) node path =
    if not cmds then marked_render "+ " (tree_at_path path node)
    else
        (Config_tree.render_commands ~op:Set node []) ^ "\n"

let removed_lines ?(cmds=false) node path =
    if not cmds then marked_render "- " (tree_at_path path node)
    else
        (Config_tree.render_commands ~op:Delete node []) ^ "\n"

let order_commands (strl: string) =
    let l = String.split_on_char '\n' strl in
    let del = List.filter (fun s -> (s <> "") && (s.[0] = 'd')) l in
    let set = List.filter (fun s -> (s <> "") && (s.[0] = 's')) l in
    (String.concat "\n" del) ^ "\n" ^ (String.concat "\n" set) ^ "\n"

let unified_diff ?(cmds=false) ?recurse:_ (path : string list) (Diff_string res) (m : change) =
    let ppath_l = list_but_last path
    in
    let ppath_s =
        if (ppath_l <> res.ppath) then path_to_string ppath_l
        else ""
    in
    let str_diff =
        if not cmds then res.udiff ^ ppath_s
        else res.udiff
    in
    match m with
    | Added ->
            let str_diff =
                let add_tree = clone res.right res.skel path in
                str_diff ^ (added_lines ~cmds:cmds add_tree path)
            in
            Diff_string { res with ppath = ppath_l; udiff = str_diff; }
    | Subtracted ->
            let str_diff =
                let sub_tree = clone res.left res.skel path in
                str_diff ^ (removed_lines ~cmds:cmds sub_tree path)
            in
            Diff_string { res with ppath = ppath_l; udiff = str_diff; }
    | Unchanged -> Diff_string (res)
    | Updated v ->
            let ov = Config_tree.get_values res.left path in
            match ov, v with
            | [_], [_] ->
                    let str_diff =
                        let sub_tree = clone res.left res.skel path in
                        str_diff ^ (removed_lines ~cmds:cmds sub_tree path)
                    in
                    let str_diff =
                        let add_tree = clone res.right res.skel path in
                        str_diff ^ (added_lines ~cmds:cmds add_tree path)
                    in
                    Diff_string { res with ppath = ppath_l; udiff = str_diff; }
            | _, _ -> let ov_set = ValueS.of_list ov in
                      let v_set = ValueS.of_list v in
                      let sub_vals = ValueS.elements (ValueS.diff ov_set v_set) in
                      let add_vals = ValueS.elements (ValueS.diff v_set ov_set) in
                      let str_diff =
                          if not (is_empty sub_vals) then
                              let sub_tree =
                                  clone ~set_values:(Some sub_vals) res.left res.skel path
                              in str_diff ^ (removed_lines ~cmds:cmds sub_tree path)
                          else str_diff
                      in
                      let str_diff =
                          if not (is_empty add_vals) then
                              let add_tree =
                                  clone ~set_values:(Some add_vals) res.right res.skel path
                              in str_diff ^ (added_lines ~cmds:cmds add_tree path)
                          else str_diff
                      in
                      Diff_string { res with ppath = ppath_l; udiff = str_diff; }

let add_empty_path src_node dest_node path =
    clone ~recurse:false ~set_values:(Some []) src_node dest_node path

let compare_at_path_maybe_empty left right path =
    let left =
        try
            tree_at_path path left
        with Empty_comparison ->
            try
                let left = add_empty_path right left path in
                tree_at_path path left
             with Vytree.Nonexistent_path ->
                 raise Empty_comparison
     and right =
        try
            tree_at_path path right
        with Empty_comparison ->
            try
                let right = add_empty_path left right path in
                tree_at_path path right
             with Vytree.Nonexistent_path ->
                 raise Empty_comparison
    in (left, right)

let show_diff ?(cmds=false) path left right =
    if (name_of left) <> (name_of right) then
        raise Incommensurable
    else
        let (left, right) =
            if (path <> []) then
                compare_at_path_maybe_empty left right path
            else (left, right) in
        let dstr = make_diff_string left right in
        let dstr =
            diff [] (unified_diff ~cmds:cmds) dstr (Option.some left, Option.some right)
        in
        let dstr = eval_result dstr in
        let strs =
            if cmds then order_commands dstr.udiff
            else dstr.udiff
        in
        strs

let is_terminal_path node path =
    try
        let n = Vytree.get node path in
        match (Vytree.children_of_node n) with
        | [] -> true
        | _ -> false
    with Vytree.Nonexistent_path -> false

(* mask function; mask applied on right *)
let mask_func ?recurse:_ (path : string list) (Diff_tree res) (m : change) =
    match m with
    | Added -> Diff_tree (res)
    | Subtracted ->
            (match path with
            | [_] -> Diff_tree {res with left = Vytree.delete res.left path}
            |  _  -> if not (is_terminal_path res.right (list_but_last path)) then
                         Diff_tree {res with left = Vytree.delete res.left path}
                     else Diff_tree (res))
    | Unchanged -> Diff_tree (res)
    | Updated _ -> Diff_tree (res)

(* call recursive diff with mask_func; mask applied on right *)
let mask_tree left right =
    let trees = make_diff_trees left right in
    let d = diff [] mask_func trees (Option.some left, Option.some right)
    in
    let res = eval_result d in
    res.left

let union_of_values (n : Config_tree.t) (m : Config_tree.t) =
    let set_n = ValueS.of_list (data_of n).values in
    let set_m = ValueS.of_list (data_of m).values in
    ValueS.elements (ValueS.union set_n set_m)

let union_of_children n m =
    let set_n = ChildrenS.of_list (children_of n) in
    let set_m = ChildrenS.of_list (children_of m) in
    ChildrenS.elements (ChildrenS.union set_n set_m)

(* tree_union is currently used only for unit tests, so only values of data
   are considered. Should there be a reason to expose it in the future,
   consistency check and union of remaining data will need to be added.
 *)
let rec tree_union s t =
    let child_of_union s t c =
        let s_c = Vytree.find s (name_of c) in
        let t_c = Vytree.find t (name_of c) in
        match s_c, t_c with
        | Some child, None -> clone s t [(name_of child)]
        | None, Some _ -> t
        | Some u, Some v ->
                if u ^~ v then
                    let values = union_of_values u v in
                    let data = {(data_of v) with Config_tree.values = values} in
                    Vytree.replace t (Vytree.make data (name_of v))
                else
                    Vytree.replace t (tree_union u v)
        | None, None -> raise Nonexistent_child
    in
    List.fold_left (fun x c -> child_of_union s x c) t (union_of_children s t)
