%% -------------------------------------------------------------------
%%
%% cuttlefish_effective: handles generating the effective configuration
%%
%% Copyright (c) 2014 Basho Technologies, Inc.  All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License.  You may obtain
%% a copy of the License at
%%
%%   http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied.  See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
-module(cuttlefish_effective).

-define(FMT(F,A), lists:flatten(io_lib:format(F,A))).

-export([build/3]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

-spec build(cuttlefish_conf:conf(), cuttlefish_schema:schema(), [proplists:property()]) -> [string()].
build(Conf, {_Translations, Mappings, _Validators} = _Schema, AdvConfig) ->
    AliasResolvedConf = cuttlefish_generator:resolve_aliases(Conf, Mappings),
    EffectiveConfig = lists:reverse(lists:sort(cuttlefish_generator:add_defaults(AliasResolvedConf, Mappings))),
    %% EffectiveConfig is a list of { [string()], term() }

    %% Returns the list of cuttlefish variables that have been overridden in advanced.config
    KeysToHateOn = process_advanced(Mappings, AdvConfig),

    EffectiveOutput = lists:foldl(
        fun({Var, Value}, Acc) ->
            Variable = string:join(Var, "."),

            IsHater =
                lists:any(
                    fun(X) ->
                        cuttlefish_variable:is_fuzzy_match(Var, X)
                    end,
                    KeysToHateOn
                    ),

            Line = try ?FMT("~ts = ~ts", [Variable, Value]) of
                X -> X
            catch
                _:_ ->
                    %% I hate that I had to do this, 'cause you know...
                    %% Erlang and Strings, but actually this is ok because
                    %% sometimes there are going to be weird tuply things
                    %% in here, so always good to fall back on ~tp.
                    %% honestly, I think this try should be built into io:format
                    ?FMT("~ts = ~tp", [Variable, Value])
            end,
            case IsHater of
                true ->
                    [?FMT("## ~ts was overridden in advanced.config", [Variable]),
                     "## " ++ Line] ++ Acc;
                _ -> [Line | Acc]
            end
        end,
        [],
        EffectiveConfig
    ),

    case AdvConfig of
        [] -> EffectiveOutput;
        _ ->
            EffectiveOutput ++
            [
             "## The following advanced.config was used in generating the ",
             "## configuration and may have overridden some options that were ",
             "## commented out above."
            ]
            ++ advanced_as_comment(AdvConfig)
    end.

advanced_as_comment(AdvConfig) ->
    Str = lists:flatten(io_lib:format("~tp", [AdvConfig])),
    [ "## " ++ L || L <- string:tokens(Str, "$\n")].

%% @doc checks a mapping's "mapping" is in the set of kvc paths in
%% the provided advanced.config
-spec process_advanced(
        [cuttlefish_mapping:mapping()],
        [proplists:property()]) -> [cuttlefish_variable:variable()].
process_advanced(Mappings, AdvancedConfig) ->
    AdvKeys = proplist_to_kvcpaths(AdvancedConfig),
    lists:foldl(
        fun(M, Acc) ->
            case lists:member(cuttlefish_mapping:mapping(M), AdvKeys) of
                true ->
                    [cuttlefish_mapping:variable(M)|Acc];
                _ -> Acc
            end
        end,
        [],
        Mappings).

%% @doc returns a list of kvcesque paths that represent the structure
%% of a proplist of proplists of proplists etc...
%% e.g. [{parent, [{child1, [{grandchild1, _}]}, {child2, _}]}] ->
%%      ["parent.child1.grandchild1", "parent.child2"]
-spec proplist_to_kvcpaths([proplists:property()]) -> [string()].
proplist_to_kvcpaths(Proplist) ->
    proplist_to_kvcpaths("", Proplist).
-spec proplist_to_kvcpaths(string(), [proplists:property()]) -> [string()].
proplist_to_kvcpaths(Prefix, Proplist) ->
    %% Handles the base case, without this, all keys would start with "."
    NewPrefix = case Prefix of
        "" -> "";
        _ -> Prefix ++ "."
    end,
    lists:foldl(fun(K, Acc) ->
            KeyedPrefix = NewPrefix ++ canonicalize_key(K),
            R = proplist_to_kvcpaths(
                KeyedPrefix,
                proplists:get_value(K, Proplist)),
            case R of
                [] ->
                    [KeyedPrefix|Acc];
                _ ->
                    Acc ++ R
            end
        end,
        [],
        keys_if_you_got_em(Proplist)
    ).

%% So this is gross, but is the simplest scheme for determining the
%% type of data coming into this function. It doesn't really
%% matter how we handle non-atoms because cuttlefish only creates 
%% proplists with atoms as the keynames.
-spec canonicalize_key(atom() | list() | binary()) -> string().
canonicalize_key(K) when is_atom(K) ->
    atom_to_list(K);
canonicalize_key(K) when is_list(K) ->
    "\"" ++ K ++ "\"";
canonicalize_key(K) when is_binary(K) ->
    "<<\""++binary_to_list(K)++"\">>".


-spec keys_if_you_got_em([proplists:property()]) -> [term()].
keys_if_you_got_em(Proplist) when is_list(Proplist) ->
    proplists:get_keys(Proplist);
keys_if_you_got_em(_) -> [].

-ifdef(TEST).

%% This is the comprehensive test of all functionality of this module
%% working together in perfect harmony
probably_the_most_important_test() ->
    Mappings = [
        cuttlefish_mapping:parse(
            {mapping, "namespace.var1", "app.setting1", []}
        ),
        cuttlefish_mapping:parse(
            {mapping, "namespace.2.$sub", "app.setting2", []}
        ),
        cuttlefish_mapping:parse(
            {mapping, "namespace.var3", "app.setting3", []}
        ),
        cuttlefish_mapping:parse(
            {mapping, "namespace.4.$sub", "app.setting4", []}
        )
    ],
    Conf = [
        {["namespace", "var1"], "x"},
        {["namespace", "2", "1"], "x"},
        {["namespace", "2", "2"], "x"},
        {["namespace", "2", "3"], "x"},
        {["namespace", "var3"], "y"},
        {["namespace", "4", "1"], "y"},
        {["namespace", "4", "2"], "y"},
        {["namespace", "4", "3"], "y"}
    ],
    AdvConfig = [{app, [{setting3, "z"}, {setting4, "zz"}]}],

    Effective = build(Conf, {[], Mappings, []}, AdvConfig),
    ?assertEqual(16, length(Effective)),

    %% Remember, this output is sorted by variable, even if there's a comment

    ?assertEqual("namespace.2.1 = x", lists:nth(1, Effective)),
    ?assertEqual("namespace.2.2 = x", lists:nth(2, Effective)),
    ?assertEqual("namespace.2.3 = x", lists:nth(3, Effective)),
    ?assertEqual("## namespace.4.1 was overridden in advanced.config", lists:nth(4, Effective)),
    ?assertEqual("## namespace.4.1 = y", lists:nth(5, Effective)),
    ?assertEqual("## namespace.4.2 was overridden in advanced.config", lists:nth(6, Effective)),
    ?assertEqual("## namespace.4.2 = y", lists:nth(7, Effective)),
    ?assertEqual("## namespace.4.3 was overridden in advanced.config", lists:nth(8, Effective)),
    ?assertEqual("## namespace.4.3 = y", lists:nth(9, Effective)),
    ?assertEqual("namespace.var1 = x", lists:nth(10, Effective)),
    ?assertEqual("## namespace.var3 was overridden in advanced.config", lists:nth(11, Effective)),
    ?assertEqual("## namespace.var3 = y", lists:nth(12, Effective)),
    ?assertEqual("## The following advanced.config was used in generating the ", lists:nth(13, Effective)),
    ?assertEqual("## configuration and may have overridden some options that were ", lists:nth(14, Effective)),
    ?assertEqual("## commented out above.", lists:nth(15, Effective)),
    ?assertEqual("## [{app,[{setting3,\"z\"},{setting4,\"zz\"}]}]", lists:nth(16, Effective)),
    ok.

alias_resolved_in_effective_test() ->
    Mappings = [
        cuttlefish_mapping:parse(
            {mapping, "new.key", "app.setting", [
                {default, "default_val"},
                {aliases, ["old.key"]}
            ]}
        )
    ],
    %% Only alias set: alias value appears under canonical key
    Conf1 = [{["old", "key"], "alias_val"}],
    Effective1 = build(Conf1, {[], Mappings, []}, []),
    ?assert(lists:member("new.key = alias_val", Effective1)),
    ?assertNot(lists:any(
        fun(Line) -> string:find(Line, "old.key") =/= nomatch end,
        Effective1)),
    %% Both set: the canonical key wins, alias does not appear
    Conf2 = [{["new", "key"], "canonical_val"}, {["old", "key"], "alias_val"}],
    Effective2 = build(Conf2, {[], Mappings, []}, []),
    ?assert(lists:member("new.key = canonical_val", Effective2)),
    ?assertNot(lists:any(
        fun(Line) -> string:find(Line, "old.key") =/= nomatch end,
        Effective2)).

process_advanced_test() ->
    Mappings = [
        cuttlefish_mapping:parse(
            {mapping, "thing.1", "a.b.c", []}
        )
    ],
    AdvConfig = [{a, [{b, [{c, ""}, {d, ""}]}]}],
    KeysToWatch = process_advanced(Mappings, AdvConfig),
    ?assertEqual([["thing", "1"]], KeysToWatch),
    ok.

build_with_sub_test() ->
    Mappings = [
        cuttlefish_mapping:parse(
            {mapping, "a.$whatev.thing", "a.b.c", []}
        )
    ],
    AdvConfig = [{a, [{b, [{c, ""}, {d, ""}]}]}],
    Conf = [
        {["a", "1", "thing"], "x"},
        {["a", "2", "thing"], "x"},
        {["a", "3", "thing"], "x"}
    ],

    Effective = build(Conf, {[], Mappings, []}, AdvConfig),

    ?assertEqual(10, length(Effective)),
    ?assertEqual("## a.1.thing was overridden in advanced.config", lists:nth(1, Effective)),
    ?assertEqual("## a.1.thing = x", lists:nth(2, Effective)),
    ?assertEqual("## a.2.thing was overridden in advanced.config", lists:nth(3, Effective)),
    ?assertEqual("## a.2.thing = x", lists:nth(4, Effective)),
    ?assertEqual("## a.3.thing was overridden in advanced.config", lists:nth(5, Effective)),
    ?assertEqual("## a.3.thing = x", lists:nth(6, Effective)),
    ?assertEqual("## The following advanced.config was used in generating the ", lists:nth(7, Effective)),
    ?assertEqual("## configuration and may have overridden some options that were ", lists:nth(8, Effective)),
    ?assertEqual("## commented out above.", lists:nth(9, Effective)),
    ?assertEqual("## [{a,[{b,[{c,[]},{d,[]}]}]}]", lists:nth(10, Effective)),
    ok.

proplist_to_kvcpath_test() ->
    Proplist = [{a, [
                    {b, [
                        {c, "x"}
                        ]},
                    {d, [
                        {e, "y"}
                        ]},
                    {f , "z"}
                    ]
                },
                {g, "q"}],
    Paths = proplist_to_kvcpaths(Proplist),
    ?assertEqual(sets:from_list([
        "a.b.c",
        "a.d.e",
        "a.f",
        "g"
    ]), sets:from_list(Paths)),
    ok.

proplists_to_kvcpath_riak_core_test() ->
    Proplist = [{riak_core,[
        {ring_creation_size,128},
        {cluster_mgr, {"127.0.0.1", 9080 } }
    ]}],
    Paths = lists:sort(proplist_to_kvcpaths(Proplist)),
    ?assertMatch([
        "riak_core.cluster_mgr",
        "riak_core.ring_creation_size"
    ], Paths),
    ok.

-endif.
