% Licensed 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(couch_epi_functions_gen).

-export([
    generate/2,
    get_current_definitions/1,
    get_handle/1,
    hash/1
]).

-export([
    apply/4,
    apply/5,
    modules/3,
    decide/5
]).

-ifdef(TEST).

-export([foo/2, bar/0]).

-endif.

-record(opts, {
    ignore_errors = false,
    pipe = false,
    concurrent = false,
    interruptible = false
}).

get_handle(ServiceId) ->
    module_name(atom_to_list(ServiceId)).

apply(ServiceId, Function, Args, Opts) when is_atom(ServiceId) ->
    apply(get_handle(ServiceId), ServiceId, Function, Args, Opts).

-spec apply(
    Handle :: atom(),
    ServiceId :: atom(),
    Function :: atom(),
    Args :: [term()],
    Opts :: couch_epi:apply_opts()
) -> [any()].

apply(Handle, _ServiceId, Function, Args, Opts) ->
    DispatchOpts = parse_opts(Opts),
    Modules = providers(Handle, Function, length(Args), DispatchOpts),
    dispatch(Handle, Modules, Function, Args, DispatchOpts).

-spec decide(
    Handle :: atom(),
    ServiceId :: atom(),
    Function :: atom(),
    Args :: [term()],
    Opts :: couch_epi:apply_opts()
) ->
    no_decision | {decided, term()}.

decide(Handle, _ServiceId, Function, Args, Opts) ->
    DispatchOpts = parse_opts([interruptible | Opts]),
    Modules = providers(Handle, Function, length(Args), DispatchOpts),
    dispatch(Handle, Modules, Function, Args, DispatchOpts).

%% ------------------------------------------------------------------
%% Codegeneration routines
%% ------------------------------------------------------------------

preamble() ->
    "\n"
    "    -export([version/0, version/1]).\n"
    "    -export([providers/0, providers/2]).\n"
    "    -export([definitions/0, definitions/1]).\n"
    "    -export([dispatch/3]).\n"
    "    -export([callbacks/2]).\n"
    "\n"
    "    version() ->\n"
    "        [{Provider, version(Provider)} || Provider <- providers()].\n"
    "\n"
    "    definitions() ->\n"
    "        [{Provider, definitions(Provider)} || Provider <- providers()].\n"
    "\n"
    "    callbacks(Provider, Function) ->\n"
    "        [].\n"
    "\n"
    "    "
%% In addition to preamble we also generate following methods
%% dispatch(Module, Function, [A1, A2]) -> Module:Function(A1, A2);

%% version(Source1) -> "HASH";
%% version(Source) -> {error, {unknown, Source}}.

%% providers() -> [].
%% providers(Function, Arity) -> [].
%% definitions(Provider) -> [{Module, [{Fun, Arity}]}].
.

generate(Handle, Defs) ->
    DispatchFunForms = couch_epi_codegen:function(dispatchers(Defs)),
    VersionFunForms = couch_epi_codegen:function(version_method(Defs)),

    AllProvidersForms = all_providers_method(Defs),
    ProvidersForms = couch_epi_codegen:function(providers_method(Defs)),
    DefinitionsForms = couch_epi_codegen:function(definitions_method(Defs)),

    Forms =
        couch_epi_codegen:scan(preamble()) ++
            DispatchFunForms ++ VersionFunForms ++
            ProvidersForms ++ AllProvidersForms ++
            DefinitionsForms,

    couch_epi_codegen:generate(Handle, Forms).

all_providers_method(Defs) ->
    Providers = couch_epi_codegen:format_term(defined_providers(Defs)),
    couch_epi_codegen:scan("providers() -> " ++ Providers ++ ".").

providers_method(Defs) ->
    Providers = providers_by_function(Defs),
    DefaultClause = "providers(_, _) -> [].",
    lists:foldl(
        fun({{Fun, Arity}, Modules}, Clauses) ->
            providers(Fun, Arity, Modules) ++ Clauses
        end,
        [couch_epi_codegen:scan(DefaultClause)],
        Providers
    ).

providers(Function, Arity, Modules) ->
    ArityStr = integer_to_list(Arity),
    Mods = couch_epi_codegen:format_term(Modules),
    Fun = atom_to_list(Function),
    %% providers(Function, Arity) -> [Module];
    couch_epi_codegen:scan(
        "providers(" ++ Fun ++ "," ++ ArityStr ++ ") ->" ++ Mods ++ ";"
    ).

dispatchers(Defs) ->
    DefaultClause = "dispatch(_Module, _Fun, _Args) -> ok.",
    fold_defs(
        Defs,
        [couch_epi_codegen:scan(DefaultClause)],
        fun({_Source, Module, Function, Arity}, Acc) ->
            dispatcher(Module, Function, Arity) ++ Acc
        end
    ).

version_method(Defs) ->
    DefaultClause = "version(S) -> {error, {unknown, S}}.",
    lists:foldl(
        fun({Source, SrcDefs}, Clauses) ->
            version(Source, SrcDefs) ++ Clauses
        end,
        [couch_epi_codegen:scan(DefaultClause)],
        Defs
    ).

definitions_method(Defs) ->
    DefaultClause = "definitions(S) -> {error, {unknown, S}}.",
    lists:foldl(
        fun({Source, SrcDefs}, Clauses) ->
            definition(Source, SrcDefs) ++ Clauses
        end,
        [couch_epi_codegen:scan(DefaultClause)],
        Defs
    ).

definition(Source, Defs) ->
    Src = atom_to_list(Source),
    DefsStr = couch_epi_codegen:format_term(Defs),
    couch_epi_codegen:scan("definitions(" ++ Src ++ ") -> " ++ DefsStr ++ ";").

dispatcher(Module, Function, 0) ->
    M = atom_to_list(Module),
    Fun = atom_to_list(Function),

    %% dispatch(Module, Function, []) -> Module:Function();
    couch_epi_codegen:scan(
        "dispatch(" ++ M ++ "," ++ Fun ++ ", []) ->" ++
            M ++ ":" ++ Fun ++ "();"
    );
dispatcher(Module, Function, Arity) ->
    Args = args_string(Arity),
    M = atom_to_list(Module),
    Fun = atom_to_list(Function),
    %% dispatch(Module, Function, [A1, A2]) -> Module:Function(A1, A2);
    couch_epi_codegen:scan(
        "dispatch(" ++ M ++ "," ++ Fun ++ ", [" ++ Args ++ "]) ->" ++
            M ++ ":" ++ Fun ++ "(" ++ Args ++ ");"
    ).

args_string(Arity) ->
    Vars = ["A" ++ integer_to_list(Seq) || Seq <- lists:seq(1, Arity)],
    string:join(Vars, ", ").

version(Source, SrcDefs) ->
    Modules = [Module || {Module, _Exports} <- SrcDefs],
    couch_epi_codegen:scan(
        "version(" ++ atom_to_list(Source) ++ ") ->" ++ hash(Modules) ++ ";"
    ).

%% ------------------------------------------------------------------
%% Helper functions
%% ------------------------------------------------------------------

module_name(ServiceId) when is_list(ServiceId) ->
    list_to_atom(string:join([atom_to_list(?MODULE), ServiceId], "_")).

get_current_definitions(Handle) ->
    if_exists(Handle, definitions, 0, [], fun() ->
        Handle:definitions()
    end).

if_exists(Handle, Func, Arity, Default, Fun) ->
    case erlang:function_exported(Handle, Func, Arity) of
        true -> Fun();
        false -> Default
    end.

defined_providers(Defs) ->
    [Source || {Source, _} <- Defs].

%% Defs = [{Source, [{Module, [{Fun, Arity}]}]}]
fold_defs(Defs, Acc, Fun) ->
    lists:foldl(
        fun({Source, SourceData}, Clauses) ->
            lists:foldl(
                fun({Module, Exports}, ExportsAcc) ->
                    lists:foldl(
                        fun({Function, Arity}, InAcc) ->
                            Fun({Source, Module, Function, Arity}, InAcc)
                        end,
                        [],
                        Exports
                    ) ++ ExportsAcc
                end,
                [],
                SourceData
            ) ++ Clauses
        end,
        Acc,
        Defs
    ).

providers_by_function(Defs) ->
    Providers = fold_defs(
        Defs,
        [],
        fun({_Source, Module, Function, Arity}, Acc) ->
            [{{Function, Arity}, Module} | Acc]
        end
    ),
    Dict = lists:foldl(
        fun({K, V}, Acc) ->
            dict:update(
                K,
                fun(Modules) ->
                    append_if_missing(Modules, V)
                end,
                [V],
                Acc
            )
        end,
        dict:new(),
        Providers
    ),
    dict:to_list(Dict).

append_if_missing(List, Value) ->
    case lists:member(Value, List) of
        true -> List;
        false -> [Value | List]
    end.

hash(Modules) ->
    VSNs = [couch_epi_util:module_version(M) || M <- lists:usort(Modules)],
    couch_epi_util:hash(VSNs).

dispatch(_Handle, _Modules, _Func, _Args, #opts{concurrent = true, pipe = true}) ->
    throw({error, {incompatible_options, [concurrent, pipe]}});
dispatch(
    Handle,
    Modules,
    Function,
    Args,
    #opts{pipe = true, ignore_errors = true}
) ->
    lists:foldl(
        fun(Module, Acc) ->
            try
                Handle:dispatch(Module, Function, Acc)
            catch
                _:_ ->
                    Acc
            end
        end,
        Args,
        Modules
    );
dispatch(
    Handle,
    Modules,
    Function,
    Args,
    #opts{pipe = true}
) ->
    lists:foldl(
        fun(Module, Acc) ->
            Handle:dispatch(Module, Function, Acc)
        end,
        Args,
        Modules
    );
dispatch(
    Handle,
    Modules,
    Function,
    Args,
    #opts{interruptible = true}
) ->
    apply_while(Modules, Handle, Function, Args);
dispatch(Handle, Modules, Function, Args, #opts{} = Opts) ->
    [do_dispatch(Handle, Module, Function, Args, Opts) || Module <- Modules].

do_dispatch(
    Handle,
    Module,
    Function,
    Args,
    #opts{concurrent = true, ignore_errors = true}
) ->
    spawn(fun() ->
        (catch Handle:dispatch(Module, Function, Args))
    end);
do_dispatch(
    Handle,
    Module,
    Function,
    Args,
    #opts{ignore_errors = true}
) ->
    (catch Handle:dispatch(Module, Function, Args));
do_dispatch(
    Handle,
    Module,
    Function,
    Args,
    #opts{concurrent = true}
) ->
    spawn(fun() -> Handle:dispatch(Module, Function, Args) end);
do_dispatch(Handle, Module, Function, Args, #opts{}) ->
    Handle:dispatch(Module, Function, Args).

apply_while([], _Handle, _Function, _Args) ->
    no_decision;
apply_while([Module | Modules], Handle, Function, Args) ->
    case Handle:dispatch(Module, Function, Args) of
        no_decision ->
            apply_while(Modules, Handle, Function, Args);
        {decided, _Decission} = Result ->
            Result
    end.

parse_opts(Opts) ->
    parse_opts(Opts, #opts{}).

parse_opts([ignore_errors | Rest], #opts{} = Acc) ->
    parse_opts(Rest, Acc#opts{ignore_errors = true});
parse_opts([pipe | Rest], #opts{} = Acc) ->
    parse_opts(Rest, Acc#opts{pipe = true});
parse_opts([concurrent | Rest], #opts{} = Acc) ->
    parse_opts(Rest, Acc#opts{concurrent = true});
parse_opts([interruptible | Rest], #opts{} = Acc) ->
    parse_opts(Rest, Acc#opts{interruptible = true});
parse_opts([], Acc) ->
    Acc.

providers(Handle, Function, Arity, #opts{}) ->
    Handle:providers(Function, Arity).

-spec modules(Handle :: atom(), Function :: atom(), Arity :: pos_integer()) ->
    list().
modules(Handle, Function, Arity) ->
    providers(Handle, Function, Arity, #opts{}).

%% ------------------------------------------------------------------
%% Tests
%% ------------------------------------------------------------------

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

foo(A1, A2) ->
    {A1, A2}.

bar() ->
    [].

basic_test() ->
    Module = foo_bar_dispatcher,
    Defs = [{?MODULE, [{foo, 2}, {bar, 0}]}],

    generate(Module, [{app1, Defs}, {app2, Defs}]),

    Exports = lists:sort([
        {callbacks, 2},
        {version, 1},
        {providers, 2},
        {definitions, 1},
        {module_info, 0},
        {version, 0},
        {dispatch, 3},
        {providers, 0},
        {module_info, 1},
        {definitions, 0}
    ]),

    ?assertEqual(Exports, lists:sort(Module:module_info(exports))),
    ?assertEqual([app1, app2], lists:sort(Module:providers())),

    ?assertEqual([?MODULE], lists:sort(Module:providers(foo, 2))),
    ?assertEqual([?MODULE], lists:sort(Module:providers(bar, 0))),

    Defs2 = lists:usort(Module:definitions()),
    ?assertMatch([{app1, [{?MODULE, _}]}, {app2, [{?MODULE, _}]}], Defs2),

    ?assertMatch([{app1, Hash}, {app2, Hash}], Module:version()),

    ?assertMatch([], Module:dispatch(?MODULE, bar, [])),
    ?assertMatch({1, 2}, Module:dispatch(?MODULE, foo, [1, 2])),

    ok.

generate_module(Name, Body) ->
    Tokens = couch_epi_codegen:scan(Body),
    couch_epi_codegen:generate(Name, Tokens).

decide_module(decide) ->
    "\n"
    "    -export([inc/1]).\n"
    "\n"
    "    inc(A) ->\n"
    "        {decided, A + 1}.\n"
    "    ";
decide_module(no_decision) ->
    "\n"
    "    -export([inc/1]).\n"
    "\n"
    "    inc(_A) ->\n"
    "        no_decision.\n"
    "    ".

decide_test() ->
    ok = generate_module(decide, decide_module(decide)),
    ok = generate_module(no_decision, decide_module(no_decision)),

    DecideDef = {foo_app, [{decide, [{inc, 1}]}]},
    NoDecissionDef = {bar_app, [{no_decision, [{inc, 1}]}]},

    DecideFirstHandle = decide_first_handle,
    ok = generate(DecideFirstHandle, [DecideDef, NoDecissionDef]),
    ?assertMatch([decide, no_decision], DecideFirstHandle:providers(inc, 1)),
    ?assertMatch({decided, 4}, decide(DecideFirstHandle, anything, inc, [3], [])),

    DecideSecondHandle = decide_second_handle,
    ok = generate(DecideSecondHandle, [NoDecissionDef, DecideDef]),
    ?assertMatch([no_decision, decide], DecideSecondHandle:providers(inc, 1)),
    ?assertMatch({decided, 4}, decide(DecideSecondHandle, anything, inc, [3], [])),

    NoDecissionHandle = no_decision_handle,
    ok = generate(NoDecissionHandle, [NoDecissionDef]),
    ?assertMatch([no_decision], NoDecissionHandle:providers(inc, 1)),
    ?assertMatch(no_decision, decide(NoDecissionHandle, anything, inc, [3], [])),

    NoHandle = no_handle,
    ok = generate(NoHandle, []),
    ?assertMatch([], NoHandle:providers(inc, 1)),
    ?assertMatch(no_decision, decide(NoHandle, anything, inc, [3], [])),

    ok.

-endif.
