%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ts=4 sw=4 et
-module(rebar_resource_v2).

-export([new/3,
         find_resource_state/2,
         format_source/1,
         lock/2,
         download/3,
         needs_update/2,
         make_vsn/3,
         format_error/1]).

-export_type([resource/0,
              source/0,
              type/0,
              location/0,
              ref/0,
              resource_state/0]).

-include("rebar.hrl").
-include_lib("providers/include/providers.hrl").

-type resource() :: #resource{}.
-type source() :: {type(), location(), ref()} | {type(), location(), ref(), binary()}
                | {type(), location(), ref(), binary(), binary()}.
-type type() :: atom().
-type location() :: string().
-type ref() :: any().
-type resource_state() :: term().

-callback init(type(), rebar_state:t()) -> {ok, resource()}.
-callback lock(rebar_app_info:t(), resource_state()) -> source().
-callback download(file:filename_all(), rebar_app_info:t(), rebar_state:t(), resource_state()) ->
    ok | {error, any()}.
-callback needs_update(rebar_app_info:t(), resource_state()) -> boolean().
-callback make_vsn(rebar_app_info:t(), resource_state()) ->
    {plain, string()} | {error, string()}.

-spec new(type(), module(), term()) -> resource().
new(Type, Module, State) ->
    #resource{type=Type,
              module=Module,
              state=State,
              implementation=?MODULE}.

-spec find_resource(type(), [resource()]) -> {ok, resource()} | {error, not_found}.
find_resource(Type, Resources) ->
    case ec_lists:find(fun(#resource{type=T}) -> T =:= Type end, Resources) of
        error when is_atom(Type) ->
            case code:which(Type) of
                non_existing ->
                    {error, not_found};
                _ ->
                    {ok, rebar_resource:new(Type, Type, #{})}
            end;
        error ->
            {error, not_found};
        {ok, Resource} ->
            {ok, Resource}
    end.

find_resource_state(Type, Resources) ->
    case lists:keyfind(Type, #resource.type, Resources) of
        false ->
            {error, not_found};
        #resource{state=State} ->
            State
    end.

format_source(AppInfo) ->
    Name = rebar_app_info:name(AppInfo),
    case rebar_app_info:source(AppInfo) of
        {pkg, _Name, Vsn, _OldHash, _Hash, _} ->
            io_lib:format("~ts v~s", [Name, Vsn]);
        Source ->
            io_lib:format("~ts (from ~p)", [Name, Source])
    end.

lock(AppInfo, State) ->
    resource_run(lock, rebar_app_info:source(AppInfo), [AppInfo], State).

resource_run(Function, Source, Args, State) ->
    Resources = rebar_state:resources(State),
    case get_resource_type(Source, Resources) of
        {ok, #resource{type=_,
                       module=Module,
                       state=ResourceState,
                       implementation=?MODULE}} ->
            erlang:apply(Module, Function, Args++[ResourceState]);
        {ok, #resource{type=_,
                       module=Module,
                       state=_,
                       implementation=rebar_resource}} ->
            erlang:apply(rebar_resource, Function, [Module | Args])
    end.

download(TmpDir, AppInfo, State) ->
    resource_run(download, rebar_app_info:source(AppInfo), [TmpDir, AppInfo, State], State).

needs_update(AppInfo, State) ->
    resource_run(needs_update, rebar_app_info:source(AppInfo), [AppInfo], State).

%% this is a special case since it is used for project apps as well, not just deps
make_vsn(AppInfo, Vsn, State) ->
    VcsType = case Vsn of {T, _} -> T; T -> T end,
    Resources = rebar_state:resources(State),
    case is_resource_type(VcsType, Resources) of
        true ->
            case find_resource(VcsType, Resources) of
                {ok, #resource{type=_,
                               module=Module,
                               state=ResourceState,
                               implementation=?MODULE}} ->
                    Module:make_vsn(AppInfo, ResourceState);
                {ok, #resource{type=_,
                               module=Module,
                               state=_,
                               implementation=rebar_resource}} ->
                    rebar_resource:make_vsn(Module, AppInfo)
            end;
        false ->
            unknown
    end.

format_error({no_resource, Location, Type}) ->
    io_lib:format("Cannot handle dependency ~ts.~n"
                  "     No module found for resource type ~p.", [Location, Type]);
format_error({no_resource, Source}) ->
    io_lib:format("Cannot handle dependency ~ts.~n"
                  "     No module found for unknown resource type.", [Source]).

is_resource_type(Type, Resources) ->
    lists:any(fun(#resource{type=T}) -> T =:= Type end, Resources).

-spec get_resource_type(tuple(), [resource()]) -> {ok, resource()}.
get_resource_type({Type, Location}, Resources) ->
    get_resource(Type, Location, Resources);
get_resource_type({Type, Location, _}, Resources) ->
    get_resource(Type, Location, Resources);
get_resource_type({Type, _, _, Location}, Resources) ->
    get_resource(Type, Location, Resources);
get_resource_type(Source, Resources) when tuple_size(Source) > 3 ->
    Type = element(1, Source),
    get_resource(Type, Source, Resources);
get_resource_type(Source, _) ->
    throw(?PRV_ERROR({no_resource, Source})).

-spec get_resource(type(), term(), [resource()]) -> {ok, resource()}.
get_resource(Type, Location, Resources) ->
    case find_resource(Type, Resources) of
        {error, not_found} ->
            throw(?PRV_ERROR({no_resource, Location, Type}));
        {ok, Resource} ->
            {ok, Resource}
    end.
