type value_behaviour = AddValue | ReplaceValue

exception Duplicate_value
exception Node_has_no_value
exception No_such_value
exception Useless_set

type config_node_data = {
    values: string list;
    comment: string option;
    inactive: bool;
    ephemeral: bool;
} [@@deriving yojson]

type t = config_node_data Vytree.t [@@deriving yojson]

let default_data = {
    values = [];
    comment = None;
    inactive = false;
    ephemeral = false;
}

let make name = Vytree.make default_data name

let replace_value node path value =
    let data = {default_data with values=[value]} in
    Vytree.update node path data

let add_value node path value =
    let node' = Vytree.get node path in
    let data = Vytree.data_of_node node' in
    let values = data.values in
    match (Vylist.find (fun x -> x = value) values) with
    | Some _ -> raise Duplicate_value
    | None ->
        let values = values @ [value] in
        Vytree.update node path ({data with values=values})

let delete_value node path value =
    let data = Vytree.data_of_node @@ Vytree.get node path in
    let values = Vylist.remove (fun x -> x = value) data.values in
    Vytree.update node path {data with values=values}

let set_value node path value behaviour =
    match behaviour with
    | AddValue -> add_value node path value
    | ReplaceValue -> replace_value node path value

let set node path value behaviour =
    if (Vytree.exists node path) then
        (match value with
         | None -> raise Useless_set
         | Some v -> set_value node path v behaviour)
    else
        let path_existing = Vytree.get_existent_path node path in
        let path_remaining = Vylist.complement path path_existing in
        let values = match value with None -> [] | Some v -> [v] in
        Vytree.insert_multi_level default_data node path_existing path_remaining {default_data with values=values}

let get_values node path =
    let node' = Vytree.get node path in
    let data = Vytree.data_of_node node' in
    data.values

let get_value node path =
    let values = get_values node path in
    match values with
    | [] -> raise Node_has_no_value
    | x :: _ -> x

let delete node path value =
    match value with
    | Some v ->
        (let values = get_values node path in
        if Vylist.in_list values v then
        (match values with
        | [_] -> Vytree.delete node path
        | _ -> delete_value node path v)
        else raise No_such_value)
    | None ->
	Vytree.delete node path

let set_comment node path comment =
    let data = Vytree.get_data node path in
    Vytree.update node path {data with comment=comment}

let get_comment node path =
    let data = Vytree.get_data node path in
    data.comment

let set_inactive node path inactive =
    let data = Vytree.get_data node path in
    Vytree.update node path {data with inactive=inactive}

let is_inactive node path =
    let data = Vytree.get_data node path in
    data.inactive

let set_ephemeral node path ephemeral =
    let data = Vytree.get_data node path in
    Vytree.update node path {data with ephemeral=ephemeral}

let is_ephemeral node path =
    let data = Vytree.get_data node path in
    data.ephemeral

module Renderer =
struct
    (* TODO Replace use of Printf with Format *)

    module L  = List
    module S  = String
    module PF = Printf
    module VT = Vytree
    module RT = Reference_tree

    (* Nodes are ordered based on a comarison of their names *)
    let compare cmp node1 node2 =
        let name1 = VT.name_of_node node1 in
        let name2 = VT.name_of_node node2 in
        cmp name1 name2

    let indentation indent level = S.make (level * indent) ' '

    let render_inactive data  = if data.inactive  then "#INACTIVE "  else ""
    let render_ephemeral data = if data.ephemeral then "#EPHEMERAL " else ""

    let render_comment data indents =
        match data.comment with
        | None   -> ""
        (* Trailing indentation for the rest of the material on the next line *)
        | Some c -> PF.sprintf "/*%s*/\n%s" c indents

    let render_tag = function
        | None     -> ""
        | Some tag -> PF.sprintf "%s " tag

    let render_outer indents data name tag =
        [render_comment data indents ;
         render_inactive data;
         render_ephemeral data;
         render_tag tag;
         name]
        |> S.concat ""

    let render_values = function
        | [v] -> PF.sprintf "\"%s\";" v
        | vs  -> S.concat "; " vs |> PF.sprintf "[%s];"

    let render_inner_and_outer indents inner outer =
        if inner = ""
        (* Hidden or empty descendents yield empty nodes *)
        then PF.sprintf "%s%s { }" indents outer
        else PF.sprintf "%s%s %s" indents outer inner

    let render
            ?(indent=4)
            ?(reftree)
            ?(cmp=BatString.numeric_compare)
            ?(showephemeral=false)
            ?(showinactive=false)
            (config_tree:t)
        =
        let is_hidden data =
            (not showephemeral && data.ephemeral) ||
            (not showinactive && data.inactive)
        in
        let rec render_node level ?tag node =
            let data = VT.data_of_node node in
            let name = VT.name_of_node node in
            (* Hide inactive and ephemeral when necessary *)
            if is_hidden data then ""
            else
                let indents = indentation indent level in
                let outer = render_outer indents data name tag in
                let inner = (* Children are ignored if the node has values *)
                    match data.values with
                    | [] -> VT.children_of_node node |> render_children level
                    | values -> render_values values
                in
                PF.sprintf "%s%s %s" indents outer inner
        and render_children level = function
            | [] -> "{ }"
            | children ->
            let indents = indentation indent level in
            let render_child node = render_node (level + 1) node in
            let rendered_children = L.map render_child children |> S.concat "\n"
            in
            if rendered_children = "" then "{ }"
            else PF.sprintf "{\n%s\n%s}" rendered_children indents
        in
        (* Walks the reftree and config_tree side-by-side *)
        let rec render_node_rt level tag rt node =
            let data    = VT.data_of_node node in
            let name    = VT.name_of_node node in
            let rt_data = VT.data_of_node rt in
            let rt_name = VT.name_of_node rt in
            (* Hide inactive and ephemeral when necessary *)
            if is_hidden data  then ""
            else
                (* TODO refactor this ugly approach*)
                let (outer_name, level', inner) =
                    let open RT in
                    let children = VT.children_of_node node in
                    let ordered = rt_data.keep_order in
                    match rt_data.node_type with
                    | Tag   ->
                        ("", 0, render_children_rt level (Some name) ordered rt children)
                    | Other ->
                        (name, level, render_children_rt level None ordered rt children)
                    | Leaf  ->
                        (name, level, render_values data.values)
                in
                let indents = indentation indent level' in
                let outer = render_outer indents data outer_name tag in
                PF.sprintf "%s%s %s" indents outer inner
        and render_children_rt level tag ordered rt = function
            | [] -> "{ }"
            | children ->
                let is_tagged = BatOption.is_some tag in
                let indents = indentation indent level in
                let reorder nodes =
                    if ordered then nodes
                    else L.sort (compare cmp) nodes
                in
                let render_child node =
                    let level' = if is_tagged then level else level + 1 in
                    let node_reftree = VT.find rt (VT.name_of_node node) in
                    (* If there is no reftree for a node, default to stand-alone *)
                    match node_reftree with
                    | Some rt' -> render_node_rt level' tag rt' node
                    | None     -> render_node level' ?tag node
                in
                let rendered_children = children
                                        |> reorder
                                        |> L.map render_child
                                        |> S.concat "\n"
                in
                if rendered_children = "" then "{ }"
                else if is_tagged
                then rendered_children
                else PF.sprintf "{\n%s\n%s}" rendered_children indents
        in
        match reftree with
        | None    -> render_node 0 config_tree
        | Some rt -> render_node_rt 0 None rt config_tree

end (* Renderer *)

let render = Renderer.render