-module(rlx_overlay).

-export([render/2,
         format_error/1]).

-export([generate_overlay_vars/2,
         render_string/2]).

-define(DIRECTORY_RE, ".*(\/|\\\\)$").

-include("relx.hrl").
-include("rlx_log.hrl").

render(Release, State) ->
    case generate_overlay_vars(State, Release) of
        {error, Reason} ->
            erlang:error(?RLX_ERROR(Reason));
        OverlayVars ->
            Files = rlx_util:template_files(),
            case do_overlay(State, Release, Files, OverlayVars) of
                {error, _} = E -> erlang:error(E);
                {ok, State1} -> {ok, State1}
            end
    end.

-spec format_error(ErrorDetail::term()) -> iolist().
format_error({unresolved_release, RelName, RelVsn}) ->
    io_lib:format("The release has not been resolved ~p-~s", [RelName, RelVsn]);
format_error({unable_to_read_varsfile, FileName, Reason}) ->
    io_lib:format("Unable to read vars file (~s) for overlay due to: ~p",
                  [FileName, Reason]);
format_error({read_template, FileName, Reason}) ->
    io_lib:format("Unable to read template file (~s) for overlay due to: ~s",
                  [FileName, file:format_error(Reason)]);
format_error({overlay_failed, Errors}) ->
    [[format_error(rlx_util:error_reason(Error)), "\n"] || Error <- Errors];
format_error({unable_to_make_symlink, AppDir, TargetDir, Reason}) ->
    io_lib:format("Unable to symlink directory ~s to ~s because \n~s~s",
                  [AppDir, TargetDir, rlx_util:indent(2),
                   file:format_error(Reason)]);
format_error({copy_failed, FromFile, ToFile, Err}) ->
    io_lib:format("Unable to copy from ~s to ~s because of ~p",
                  [FromFile, ToFile, Err]);
format_error({unable_to_write, ToFile, Reason}) ->
    io_lib:format("Unable to write to ~s because ~p",
                  [ToFile, Reason]);
format_error({unable_to_enclosing_dir, ToFile, Reason}) ->
    io_lib:format("Unable to create enclosing directory for ~s because ~p",
                  [ToFile, Reason]);
format_error({unable_to_render_template, FromFile, Reason}) ->
    io_lib:format("Unable to render template ~s because ~p",
                  [FromFile, Reason]);
format_error({unable_to_compile_template, FromFile, Reason}) ->
    io_lib:format("Unable to compile template ~s because \n~s",
                  [FromFile, [format_errors(F, Es) || {F, Es} <- Reason]]);
format_error({unable_to_make_dir, Absolute, Error}) ->
    io_lib:format("Unable to make directory ~s because ~p",
                  [Absolute, Error]);
format_error({unable_to_chmod, Mode, File, Err}) ->
    io_lib:format("Unable to chmod ~p ~s because of ~p",
                  [Mode, File, Err]);
format_error({link_failed, FromFile, ToFile, Err}) ->
    io_lib:format("Unable to symlink ~s to ~s because of ~p",
                  [FromFile, ToFile, Err]);
format_error({malformed_overlay, Invalid}) ->
    io_lib:format("Overlay section malformed: ~p", [Invalid]).

%%%===================================================================
%%% Internal Functions
%%%===================================================================

format_errors(File, [{none, Mod, E}|Es]) ->
    [io_lib:format("~s~s: ~ts~n",
                   [rlx_util:indent(2), File,
                    Mod:format_error(E)])
     |format_errors(File, Es)];
format_errors(File, [{{Line, Col}, Mod, E}|Es]) ->
    [io_lib:format("~s~s:~w:~w: ~ts~n",
                   [rlx_util:indent(2), File, Line, Col,
                    Mod:format_error(E)])
     |format_errors(File, Es)];
format_errors(File, [{Line, Mod, E}|Es]) ->
    [io_lib:format("~s~s:~w: ~ts~n",
                   [rlx_util:indent(2), File, Line,
                    Mod:format_error(E)])
     |format_errors(File, Es)];
format_errors(_, []) -> [].


-spec generate_overlay_vars(rlx_state:t(), rlx_release:t()) ->
                                   proplists:proplist() | relx:error().
generate_overlay_vars(State, Release) ->
    StateVars = generate_state_vars(Release, State),
    ReleaseVars = generate_release_vars(Release),
    get_overlay_vars_from_file(State, StateVars ++ ReleaseVars).

-spec get_overlay_vars_from_file(rlx_state:t(), proplists:proplist()) ->
                                        proplists:proplist() | relx:error().
get_overlay_vars_from_file(State, OverlayVars) ->
    case rlx_state:overlay_vars(State) of
        [] ->
            OverlayVars;
        [H | _]=FileNames when is_list(H) ;
                               is_tuple(H) ->
            read_overlay_vars(State, OverlayVars, FileNames);
        FileName when is_list(FileName) ->
            read_overlay_vars(State, OverlayVars, [FileName])
    end.

-spec read_overlay_vars(rlx_state:t(), proplists:proplist(), [file:name()]) ->
                               proplists:proplist() | relx:error().
read_overlay_vars(State, OverlayVars, FileNames) ->
    OverlayVarsValues = rlx_state:overlay_vars_values(State),
    Terms = merge_overlay_vars(State, FileNames),
    case render_overlay_vars(OverlayVars ++ OverlayVarsValues, Terms, []) of
        {ok, NewTerms} ->
            % We place `OverlayVarsValues' at the end on purpose; their
            % definitions should be able to be overwritten by both internal
            % and rendered vars, as not to change behaviour in
            % setups preceding the support for overlays from the caller.
            % Place NewTerms at the start - the last overlays added should be able
            % to override those that came before
            NewTerms ++ OverlayVars ++ OverlayVarsValues;
        Error ->
            Error
    end.

-spec check_overlay_inclusion(rlx_state:t(), string(), proplists:proplist()) ->
                              proplists:proplist().
check_overlay_inclusion(State, RelativeRoot, Terms) ->
    check_overlay_inclusion(State, RelativeRoot, Terms, []).

-spec check_overlay_inclusion(rlx_state:t(), string(), proplists:proplist(), proplists:proplist()) ->
                              proplists:proplist().
check_overlay_inclusion(State, RelativeRoot, [File|T], Terms) when is_list(File) ->
    %% merge terms from the file into the current terms of the system
    %% passing the Terms ensures duplicates delete the existing pair
    %% while keeping the order of terms the same, as is done in `merge_overlay_vars'
    IncludedTerms = merge_overlay_vars(State, Terms, [filename:join(RelativeRoot, File)]),
    check_overlay_inclusion(State, RelativeRoot, T, IncludedTerms);
check_overlay_inclusion(State, RelativeRoot, [Tuple|T], Terms) ->
    check_overlay_inclusion(State, RelativeRoot, T, Terms ++ [Tuple]);
check_overlay_inclusion(_State, _RelativeRoot, [], Terms) ->
    Terms.

-spec merge_overlay_vars(rlx_state:t(), [file:name()]) ->
                                proplists:proplist().
merge_overlay_vars(State, FileNames) ->
    merge_overlay_vars(State, [], FileNames).

merge_overlay_vars(State, Terms0, FileNames) ->
    RelativeRoot = get_relative_root(State),
    lists:foldl(fun(FileName, Acc) when is_list(FileName) ->
                        RelativePath = filename:join(RelativeRoot, iolist_to_binary(FileName)),
                        case file:consult(RelativePath) of
                            {ok, Terms} ->
                                %% the location of the included overlay files will be relative
                                %% to the current one being read
                                OverlayRelativeRoot = filename:dirname(FileName),
                                NewTerms = check_overlay_inclusion(State, OverlayRelativeRoot, Terms),
                                %% Remove already defined variables from Acc,
                                %% append NewTerms, preserving order
                                lists:foldl(fun(NewTerm, A) ->
                                                    lists:keydelete(element(1, NewTerm), 1, A)
                                            end, Acc, NewTerms) ++ NewTerms;
                            {error, Reason} ->
                                ?log_warn(format_error({unable_to_read_varsfile, FileName, Reason})),
                                Acc
                        end;
                   (Var, Acc) ->
                        lists:keystore(element(1, Var), 1, Acc, Var)
                end, Terms0, FileNames).

-spec render_overlay_vars(proplists:proplist(), proplists:proplist(),
                          proplists:proplist()) ->
                                 {ok, proplists:proplist()} | relx:error().
render_overlay_vars(OverlayVars, [{Key, Value} | Rest], Acc)
  when erlang:is_list(Value) ->
    case io_lib:printable_list(Value) of
        true ->
            case render_template(Acc ++ OverlayVars, erlang:iolist_to_binary(Value)) of
                {ok, Data} ->
                    %% Adding to the end sucks, but ordering needs to be retained
                    render_overlay_vars(OverlayVars, Rest, Acc ++ [{Key, Data}]);
                Error ->
                    Error
            end;
        false ->
            case render_overlay_vars(Acc ++ OverlayVars, Value, []) of
                {ok, NewValue} ->
                    render_overlay_vars(OverlayVars, Rest, Acc ++ [{Key, NewValue}]);
                Error ->
                    Error
            end
    end;
render_overlay_vars(OverlayVars, [{Key, Value} | Rest], Acc)
  when erlang:is_binary(Value) ->
    case render_template(Acc ++ OverlayVars, erlang:iolist_to_binary(Value)) of
        {ok, Data} ->
            render_overlay_vars(OverlayVars, Rest, Acc ++ [{Key, erlang:iolist_to_binary(Data)}]);
        Error ->
            Error
    end;
render_overlay_vars(OverlayVars, [KeyValue | Rest], Acc) ->
    render_overlay_vars(OverlayVars, Rest, Acc ++ [KeyValue]);
render_overlay_vars(_OverlayVars, [], Acc) ->
    {ok, Acc}.

-spec generate_release_vars(rlx_release:t()) -> proplists:proplist().
generate_release_vars(Release) ->
    [{erts_vsn, rlx_release:erts(Release)},
     {erts_dir, code:root_dir()},
     {release_erts_version, rlx_release:erts(Release)},
     {release_name, rlx_release:name(Release)},
     {release_version, rlx_release:vsn(Release)},
     {release, rlx_release:canonical_name(Release)}].

-spec generate_state_vars(rlx_release:t(), rlx_state:t()) -> proplists:proplist().
generate_state_vars(Release, State) ->
    [{output_dir, filename:join(rlx_state:base_output_dir(State),
                                rlx_release:name(Release))},
     {target_dir, filename:join(rlx_state:base_output_dir(State),
                                rlx_release:name(Release))},
     {overridden, [AppName || {AppName, _} <- rlx_state:overrides(State)]},
     {overrides, rlx_state:overrides(State)},
     {lib_dirs, rlx_state:lib_dirs(State)},
     {vm_args, rlx_state:vm_args(State)},
     {sys_config, rlx_state:sys_config(State)},
     {root_dir, rlx_state:root_dir(State)}
     %% include a var for each application. makes it easier to copy a file from an application
     | [{rlx_app_info:name(AppInfo), rlx_app_info:dir(AppInfo)}
        || AppInfo <- rlx_release:applications(Release)]].

-spec do_overlay(rlx_state:t(), rlx_release:t(), list(), proplists:proplist()) ->
                                   {ok, rlx_state:t()} | relx:error().
do_overlay(State, Release, Files, OverlayVars) ->
    case rlx_state:overlay(State) of
        [] ->
            {ok, State};
        Overlays ->
            handle_errors(State,
                          lists:map(fun(Overlay) ->
                                            do_individual_overlay(State,
                                                                  Release,
                                                                  Files,
                                                                  OverlayVars,
                                                                  Overlay)
                                    end, Overlays))
    end.

-spec handle_errors(rlx_state:t(), [ok | relx:error()]) ->
                           {ok, rlx_state:t()} | relx:error().
handle_errors(State, Result) ->
    case [Error || Error <- Result,
                   rlx_util:is_error(Error)] of
        Errors = [_|_] ->
            ?RLX_ERROR({overlay_failed, Errors});
        [] ->
            {ok, State}
    end.

-spec do_individual_overlay(rlx_state:t(), rlx_release:t(), list(), proplists:proplist(),
                            OverlayDirective::term()) ->
                                   {ok, rlx_state:t()} | relx:error().
do_individual_overlay(State, Release, _Files, OverlayVars, {chmod, Mode, Path}) ->
    % mode can be specified directly as an integer value, or if it is
    % not an integer we assume it's a template, which we render and convert
    % blindly to an integer.  So this will crash with an exception if for
    % some reason something other than an integer is used
    NewMode =
        case is_integer(Mode) of
            true ->
                Mode;
            false ->
                erlang:binary_to_integer(render_string(OverlayVars, Mode))
        end,

    file_render_do(OverlayVars, Path,
                   fun(NewPath) ->
                            Absolute = absolute_path_to(State, Release, NewPath),
                            case file:change_mode(Absolute, NewMode) of
                                {error, Error} ->
                                    ?RLX_ERROR({unable_to_chmod, NewMode, NewPath, Error});
                                ok -> ok
                            end
                   end);
do_individual_overlay(State, Release, _Files, OverlayVars, {mkdir, Dir}) ->
    file_render_do(OverlayVars, Dir,
                   fun(Dir0) ->
                       Absolute = absolute_path_to(State, Release, Dir0),
                       case rlx_file_utils:mkdir_p(Absolute) of
                           {error, Error} ->
                               ?RLX_ERROR({unable_to_make_dir, Absolute, Error});
                           ok ->
                               ok
                       end
                   end);
do_individual_overlay(State, Release, _Files, OverlayVars, {copy, From, To}) ->
    file_render_do(OverlayVars, From,
                   fun(FromFile) ->
                           file_render_do(OverlayVars, To,
                                          fun(ToFile) ->
                                                  copy_to(State, Release, FromFile, ToFile)
                                          end)
                   end);
do_individual_overlay(State, Release, Files, OverlayVars, {link, From, To}) ->
    case rlx_state:mode(State) of
        dev  ->
            file_render_do(OverlayVars, From,
                           fun(FromFile) ->
                                   file_render_do(OverlayVars, To,
                                                  fun(ToFile) ->
                                                          link_to(State, Release, FromFile, ToFile)
                                                  end)
                           end);
        _ ->
            do_individual_overlay(State, Release, Files, OverlayVars, {copy, From, To})
    end;
do_individual_overlay(State, Release, _Files, OverlayVars, {template, From, To}) ->
    file_render_do(OverlayVars, From,
                   fun(FromFile) ->
                           file_render_do(OverlayVars, To,
                                          fun(ToFile) ->
                                                  write_template(OverlayVars,
                                                                 absolute_path_from(State, FromFile),
                                                                 absolute_path_to(State, Release, ToFile))
                                          end)
                   end);
do_individual_overlay(State, Release, _Files, OverlayVars0, {template, From, To, OverlayFilename}) ->
    OverlayVars = read_overlay_vars(State, OverlayVars0, [OverlayFilename]),
    file_render_do(OverlayVars, From,
                   fun(FromFile) ->
                           file_render_do(OverlayVars, To,
                                          fun(ToFile) ->
                                                  write_template(OverlayVars,
                                                                 absolute_path_from(State, FromFile),
                                                                 absolute_path_to(State, Release, ToFile))
                                          end)
                   end);
do_individual_overlay(_State, _Release, _Files, _OverlayVars, Invalid) ->
    ?RLX_ERROR({malformed_overlay, Invalid}).

-spec wildcard_copy(rlx_state:t(), rlx_release:t(), file:filename_all(), file:filename_all(),
      fun((file:filename_all(), file:filename_all()) -> ok | {error, term()}),
      ErrorTag :: atom()) -> ok | relx:error().
wildcard_copy(State, Release, FromFile0, ToFile0, CopyFun, ErrorTag) ->
    FromFile1 = absolute_path_from(State, FromFile0),
    ToFile1 = absolute_path_to(State, Release, ToFile0),

    Res = case is_directory(ToFile0) of
              false ->
                  filelib:ensure_dir(ToFile1),
                  CopyFun(FromFile1, ToFile1);
              true ->
                  Root = absolute_path_from(State, "."),
                  FromFiles = case is_list(FromFile0) of
                                  true ->
                                      filelib:wildcard(FromFile0, Root);
                                  false ->
                                      [FromFile1]
                              end,
                  rlx_file_utils:mkdir_p(ToFile1),
                  lists:foldl(fun(_, {error, _} = Error) -> Error;
                                 (FromFile, ok) ->
                                      CopyFun(filename:join(Root, FromFile),
                                              filename:join(ToFile1, filename:basename(FromFile)))
                              end, ok, FromFiles)
          end,
                  
    case Res of
        ok ->
            ok;
        {error, Err} ->
            ?RLX_ERROR({ErrorTag,
                        FromFile1,
                        ToFile1, Err})
    end.

-spec copy_to(rlx_state:t(), rlx_release:t(), file:name(), file:name()) -> ok | relx:error().
copy_to(State, Release, FromFile0, ToFile0) ->
    wildcard_copy(State, Release, FromFile0, ToFile0,
                  fun(FromPath, ToPath) ->
                          rlx_file_utils:copy(FromPath, ToPath, [recursive, {file_info, [mode, time]}]) end,
                  copy_failed).

-spec link_to(rlx_state:t(), rlx_release:t(), file:name(), file:name()) -> ok | relx:error().
link_to(State, Release, FromFile0, ToFile0) ->
    wildcard_copy(State, Release, FromFile0, ToFile0,
                  fun make_link/2,
                  link_failed).

make_link(FromFile, ToFile) ->
    case rlx_file_utils:is_symlink(ToFile) of
        true -> file:delete(ToFile);
        false -> rlx_file_utils:remove(ToFile, [recursive])
    end,
    file:make_symlink(FromFile, ToFile).

get_relative_root(State) ->
    rlx_state:root_dir(State).

absolute_path_from(State, Path) ->
    absolutize(State, filename:join(get_relative_root(State), Path)).

absolute_path_to(State, Release, Path) ->
    absolutize(State, filename:join([rlx_state:base_output_dir(State),
                                     rlx_release:name(Release),
                                     Path])).

-spec is_directory(file:name()) -> boolean().
is_directory(ToFile0) ->
    case re:run(ToFile0, ?DIRECTORY_RE) of
        nomatch ->
            false;
        _ ->
            true
    end.

-spec render_template(proplists:proplist(), binary()) ->
                             {ok, binary()} | relx:error().
render_template(OverlayVars, Data) ->
    case rlx_util:render(Data, OverlayVars) of
        {ok, IoData} ->
            {ok, IoData};
        {error, Reason} ->
            ?RLX_ERROR({unable_to_render_template, Data, Reason})
    end.

-spec write_template(proplists:proplist(), file:name(), file:name()) ->
                            ok | relx:error().
write_template(OverlayVars, FromFile, ToFile) ->
    case file:read_file(FromFile) of
        {ok, File} ->
            case render_template(OverlayVars, File) of
                {ok, IoData} ->
                    case filelib:ensure_dir(ToFile) of
                        ok ->
                            %% we were asked to render a template
                            %% onto a symlink, this would cause an overwrite
                            %% of the original file, so we delete the symlink
                            %% and go ahead with the template render
                            case rlx_file_utils:is_symlink(ToFile) of
                                true -> rlx_file_utils:remove(ToFile);
                                false -> ok
                            end,
                            case file:write_file(ToFile, IoData) of
                                ok ->
                                    ok = rlx_file_utils:copy_file_info(ToFile, FromFile, [mode, time]),
                                    ok;
                                {error, Reason} ->
                                    ?RLX_ERROR({unable_to_write, ToFile, Reason})
                            end;
                        {error, Reason} ->
                            ?RLX_ERROR({unable_to_enclosing_dir, ToFile, Reason})
                    end;
                Error ->
                    Error
            end;
        {error, Error} ->
            ?RLX_ERROR({read_template, FromFile, Error})
    end.

-spec render_string(proplists:proplist(), iolist()) ->
                      binary() | relx:error().
render_string(OverlayVars, Data) ->
    case rlx_util:render(Data, OverlayVars) of
        {ok, Bin} -> Bin;
        {error, Error} ->
            ?RLX_ERROR({render_failed, Data, Error})
    end.

-spec file_render_do(proplists:proplist(), iolist(),
                     fun((string() | binary()) -> {ok, rlx_state:t()} | relx:error())) ->
                            {ok, rlx_state:t()} | relx:error().
file_render_do(OverlayVars, File, NextAction) ->
    case rlx_util:render(File, OverlayVars) of
        {ok, Binary} when is_binary(File) ->
            NextAction(Binary);
        {ok, Binary} when is_list(File) ->
            NextAction(binary_to_list(Binary));
        {error, Error} ->
            ?RLX_ERROR({render_failed, File, Error})
    end.

absolutize(State, FileName) ->
    filename:absname(filename:join(rlx_state:root_dir(State), FileName)).
