%%%-------------------------------------------------------------------
%%% File:      erlydtl_compiler_utils.erl
%%% @author    Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
%%% @author    Evan Miller <emmiller@gmail.com>
%%% @author    Andreas Stenius <kaos@astekk.se>
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% ErlyDTL template compiler utils.
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a copy
%%% of this software and associated documentation files (the "Software"), to deal
%%% in the Software without restriction, including without limitation the rights
%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
%%% copies of the Software, and to permit persons to whom the Software is
%%% furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
%%% THE SOFTWARE.
%%%
%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl_compiler_utils).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('Andreas Stenius <kaos@astekk.se>').

%% --------------------------------------------------------------------
%% Definitions
%% --------------------------------------------------------------------

-define(LIB_VERSION, 1).

-export([
         add_error/3, add_errors/2,
         add_filters/2,
         add_tags/2,
         add_warning/3, add_warnings/2,
         begin_scope/1, begin_scope/2,
         call_extension/3,
         empty_scope/0,
         end_scope/4,
         format_error/1,
         full_path/2,
         get_current_file/1,
         init_treewalker/1,
         is_stripped_token_empty/1,
         load_library/2, load_library/3, load_library/4,
         merge_info/2,
         print/3, print/4,
         push_scope/2,
         reset_block_dict/2,
         reset_parse_trail/2,
         resolve_variable/2, resolve_variable/3,
         restore_scope/2,
         shorten_filename/1, shorten_filename/2,
         to_string/2,
         unescape_string_literal/1,
         push_auto_escape/2,
         pop_auto_escape/1,
         token_pos/1
        ]).

-include("erlydtl_ext.hrl").


%% --------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------

init_treewalker(Context) ->
    TreeWalker = #treewalker{ context=Context },
    case call_extension(Context, init_treewalker, [TreeWalker]) of
        {ok, TW} when is_record(TW, treewalker) -> TW;
        undefined -> TreeWalker
    end.

to_string(Arg, #dtl_context{ binary_strings = true }) ->
    to_binary_string(Arg);
to_string(Arg, #dtl_context{ binary_strings = false }) ->
    to_list_string(Arg).

unescape_string_literal(String) ->
    unescape_string_literal(remove_quotes(String), [], noslash).

full_path(File, DocRoot) ->
    case filename:absname(File) of
        File -> File;
        _ -> filename:join([DocRoot, File])
    end.

print(Fmt, Args, Context) ->
    print(?V_INFO, Fmt, Args, Context).

print(Verbosity, Fmt, Args, #treewalker{ context=Context }) ->
    print(Verbosity, Fmt, Args, Context);
print(Verbosity, Fmt, Args, #dtl_context{ verbose = Verbose })
  when Verbosity =< Verbose ->
    io:format(Fmt, Args);
print(_Verbosity, _Fmt, _Args, _Context) ->
    ok.

get_current_file(#treewalker{ context=Context }) ->
    get_current_file(Context);
get_current_file(#dtl_context{ parse_trail=[File|_] }) -> File;
get_current_file(#dtl_context{ is_compiling_dir=Dir }) when Dir =/= false -> Dir;
get_current_file(#dtl_context{ doc_root=Root }) -> Root.

add_error(Module, Error, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=add_error(Module, Error, Context) };
add_error(Module, Error, #dtl_context{
                    errors=#error_info{ report=Report, list=Es }=Ei
                   }=Context) ->
    Item = get_error_item(
             Report, "",
             get_current_file(Context),
             Error, Module),
    Context#dtl_context{
      errors=Ei#error_info{ list=[Item|Es] }
     }.

add_errors(Errors, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=add_errors(Errors, Context) };
add_errors(Errors, Context) ->
    lists:foldl(
      fun (E, C) -> add_error(?MODULE, E, C) end,
      Context, Errors).

add_warning(Module, Warning, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=add_warning(Module, Warning, Context) };
add_warning(Module, Warning, #dtl_context{ warnings=warnings_as_errors }=Context) ->
    add_error(Module, Warning, Context);
add_warning(Module, Warning, #dtl_context{
                        warnings=#error_info{ report=Report, list=Ws }=Wi
                       }=Context) ->
    Item = get_error_item(
             Report, "Warning: ",
             get_current_file(Context),
             Warning, Module),
    Context#dtl_context{
      warnings=Wi#error_info{ list=[Item|Ws] }
     }.

add_warnings(Warnings, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=add_warnings(Warnings, Context) };
add_warnings(Warnings, Context) ->
    lists:foldl(
      fun (W, C) -> add_warning(?MODULE, W, C) end,
      Context, Warnings).

call_extension(#treewalker{ context=Context }, Fun, Args) ->
    call_extension(Context, Fun, Args);
call_extension(#dtl_context{ extension_module=undefined }, _Fun, _Args) ->
    undefined;
call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
  when is_atom(Mod), is_atom(Fun), is_list(Args) ->
    M = case code:is_loaded(Mod) of
            false ->
                case code:load_file(Mod) of
                    {module, Mod} ->
                        Mod;
                    _ ->
                        undefined
                end;
            _ -> Mod
        end,
    if M /= undefined ->
            case erlang:function_exported(M, Fun, length(Args)) of
                true ->
                    apply(M, Fun, Args);
                false ->
                    undefined
            end;
       true ->
            undefined
    end.

merge_info(Info1, Info2) when is_record(Info1, ast_info), is_record(Info2, ast_info) ->
    merge_info1(record_info(size, ast_info), Info1, Info2, #ast_info{}).

resolve_variable(VarName, TreeWalker) ->
    resolve_variable(VarName, undefined, TreeWalker).

resolve_variable(VarName, Default, #treewalker{ context=Context }) ->
    case resolve_variable1(Context#dtl_context.local_scopes, VarName) of
        undefined ->
            case proplists:get_value(VarName, Context#dtl_context.const) of
                undefined ->
                    case proplists:get_value(VarName, Context#dtl_context.vars) of
                        undefined -> {default, Default, []};
                        Value -> {default_vars, Value, []}
                    end;
                Value -> {constant, Value, []}
            end;
        {Value, Filters} -> {scope, Value, Filters}
    end.

push_scope(Scope, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=push_scope(Scope, Context) };
push_scope(Scope, #dtl_context{ local_scopes=Scopes }=Context) ->
    Context#dtl_context{ local_scopes=[Scope|Scopes] }.

restore_scope(Target, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=restore_scope(Target, Context) };
restore_scope(#treewalker{ context=Target }, Context) ->
    restore_scope(Target, Context);
restore_scope(#dtl_context{ local_scopes=Scopes }, Context) ->
    Context#dtl_context{ local_scopes=Scopes }.

begin_scope(TreeWalker) -> begin_scope(empty_scope(), TreeWalker).

begin_scope({Scope, Values}, TreeWalker) ->
    Id = make_ref(),
    {Id, push_scope({Id, Scope, Values}, TreeWalker)}.

end_scope(Fun, Id, AstList, TreeWalker) ->
    close_scope(Fun, Id, AstList, TreeWalker).

empty_scope() -> {[], []}.

reset_block_dict(BlockDict, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=reset_block_dict(BlockDict, Context) };
reset_block_dict(BlockDict, Context) ->
    Context#dtl_context{ block_dict=BlockDict }.

reset_parse_trail(ParseTrail, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=reset_parse_trail(ParseTrail, Context) };
reset_parse_trail(ParseTrail, Context) ->
    Context#dtl_context{ parse_trail=ParseTrail }.

load_library(Lib, Context) -> load_library(none, Lib, [], Context).
load_library(Pos, Lib, Context) -> load_library(Pos, Lib, [], Context).

load_library(Pos, Lib, Which, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=load_library(Pos, Lib, Which, Context) };
load_library(Pos, Lib, Which, Context) ->
    case lib_module(Lib, Context) of
        {ok, Mod} ->
            ?LOG_DEBUG(
               "~s: Load library '~s' from ~s ~p~n",
               [get_current_file(Context), Lib, Mod, Which], Context),
            Filters = read_library(Mod, filters, Which),
            Tags = read_library(Mod, tags, Which),
            Found = Filters ++ Tags,
            Missing = lists:filter(
                        fun (W) -> lists:keyfind(W, 1, Found) == false end,
                        Which),
            add_tags(
              Tags,
              add_filters(
                Filters,
                warn_missing(
                  Missing, {Pos, Lib, Mod}, Context)));
        Error ->
            ?WARN({Pos, Error}, Context)
    end.

add_filters(Load, #dtl_context{ filters=Filters }=Context) ->
    ?LOG_TRACE("Load filters: ~p~n", [Load], Context),
    Context#dtl_context{ filters=Load ++ Filters }.

add_tags(Load, #dtl_context{ tags=Tags }=Context) ->
    ?LOG_TRACE("Load tags: ~p~n", [Load], Context),
    Context#dtl_context{ tags=Load ++ Tags }.

%% shorten_filename/1 copied from Erlang/OTP lib/compiler/src/compile.erl
shorten_filename(Name) ->
    {ok, Cwd} = file:get_cwd(),
    shorten_filename(Name, Cwd).

shorten_filename(Name, Cwd) ->
    case lists:prefix(Cwd, Name) of
        false -> Name;
        true ->
            case lists:nthtail(length(Cwd), Name) of
                "/"++N -> N;
                N -> N
            end
    end.

push_auto_escape(State, #treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=push_auto_escape(State, Context) };
push_auto_escape(State, #dtl_context{ auto_escape=AutoEscape }=Context) ->
    Context#dtl_context{ auto_escape=[State|AutoEscape] }.

pop_auto_escape(#treewalker{ context=Context }=TreeWalker) ->
    TreeWalker#treewalker{ context=pop_auto_escape(Context) };
pop_auto_escape(#dtl_context{ auto_escape=[_|AutoEscape] }=Context)
  when length(AutoEscape) > 0 ->
    Context#dtl_context{ auto_escape=AutoEscape };
pop_auto_escape(Context) -> Context.


token_pos(Token) when is_tuple(Token) ->
    token_pos(tuple_to_list(Token));
token_pos([T|Ts]) when is_tuple(T) ->
    case T of
        {R, C}=P when is_integer(R), is_integer(C) -> P;
        _ -> token_pos(tuple_to_list(T) ++ Ts)
    end;
token_pos([T|Ts]) when is_list(T) -> token_pos(T ++ Ts);
token_pos([_|Ts]) -> token_pos(Ts);
token_pos([]) -> none.

is_stripped_token_empty({string, _, S}) ->
    [] == [C || C <- S, C /= 32, C /= $\r, C /= $\n, C /= $\t];
is_stripped_token_empty({comment, _}) -> true;
is_stripped_token_empty({comment_tag, _, _}) -> true;
is_stripped_token_empty(_) -> false.


format_error({load_library, Name, Mod, Reason}) ->
    io_lib:format("Failed to load library '~p' (~p): ~p", [Name, Mod, Reason]);
format_error({load_from, Name, Mod, Tag}) ->
    io_lib:format("'~p' not in library '~p' (~p)", [Tag, Name, Mod]);
format_error({unknown_extension, Tag}) ->
    io_lib:format("Unhandled extension: ~p", [Tag]);
format_error(Other) ->
    io_lib:format("## Sorry, error description for ~p not yet implemented.~n"
                  "## Please report this so we can fix it.", [Other]).


%%====================================================================
%% Internal functions
%%====================================================================

to_binary_string(Arg) when is_binary(Arg) -> Arg;
to_binary_string(Arg) when is_list(Arg) -> list_to_binary(Arg);
to_binary_string(Arg) when is_integer(Arg) ->
    case erlang:function_exported(erlang, integer_to_binary, 1) of
        true -> erlang:integer_to_binary(Arg);
        false -> list_to_binary(integer_to_list(Arg))
    end;
to_binary_string(Arg) when is_atom(Arg) -> atom_to_binary(Arg, latin1).

to_list_string(Arg) when is_list(Arg) -> Arg;
to_list_string(Arg) when is_binary(Arg) -> binary_to_list(Arg);
to_list_string(Arg) when is_integer(Arg) -> integer_to_list(Arg);
to_list_string(Arg) when is_atom(Arg) -> atom_to_list(Arg).

unescape_string_literal([], Acc, noslash) ->
    lists:reverse(Acc);
unescape_string_literal([$\\ | Rest], Acc, noslash) ->
    unescape_string_literal(Rest, Acc, slash);
unescape_string_literal([C | Rest], Acc, noslash) ->
    unescape_string_literal(Rest, [C | Acc], noslash);
unescape_string_literal("n" ++ Rest, Acc, slash) ->
    unescape_string_literal(Rest, [$\n | Acc], noslash);
unescape_string_literal("r" ++ Rest, Acc, slash) ->
    unescape_string_literal(Rest, [$\r | Acc], noslash);
unescape_string_literal("t" ++ Rest, Acc, slash) ->
    unescape_string_literal(Rest, [$\t | Acc], noslash);
unescape_string_literal([C | Rest], Acc, slash) ->
    unescape_string_literal(Rest, [C | Acc], noslash).

get_error_item(Report, Prefix, File, Error, DefaultModule) ->
    case compose_error_desc(Error, DefaultModule) of
        {Pos, Module, ErrorDesc} ->
            new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc);
        ErrorItem ->
            ErrorItem
    end.

compose_error_desc({{Line, Col}=Pos, ErrorDesc}, Module)
  when is_integer(Line), is_integer(Col), is_atom(Module) ->
    {Pos, Module, ErrorDesc};
compose_error_desc({Line, ErrorDesc}, Module)
  when is_integer(Line); Line =:= none ->
    {Line, Module, ErrorDesc};
compose_error_desc({{Line, Col}, Module, _}=ErrorDesc, _)
  when is_integer(Line), is_integer(Col), is_atom(Module) ->
    ErrorDesc;
compose_error_desc({Line, Module, _}=ErrorDesc, _)
  when is_integer(Line), is_atom(Module) ->
    ErrorDesc;
compose_error_desc({none, Module, _}=ErrorDesc, _)
  when is_atom(Module) ->
    ErrorDesc;
compose_error_desc({_, InfoList}=ErrorDesc, _)
  when is_list(InfoList) -> ErrorDesc;
compose_error_desc(ErrorDesc, Module) ->
    {none, Module, ErrorDesc}.

new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc) ->
    if Report  ->
            io:format("~s:~s~s~s~n",
                      [File, pos_info(Pos), Prefix,
                       Module:format_error(ErrorDesc)]);
       true -> nop
    end,
    {File, [{Pos, Module, ErrorDesc}]}.

pos_info(none) -> " ";
pos_info(Line) when is_integer(Line) ->
    io_lib:format("~b: ", [Line]);
pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
    io_lib:format("~b:~b ", [Line, Col]).

resolve_variable1([], _VarName) -> undefined;
resolve_variable1([Scope|Scopes], VarName) ->
    case lists:keyfind(VarName, 1, get_scope(Scope)) of
        false ->
            resolve_variable1(Scopes, VarName);
        {_, Value} -> {Value, []};
        {_, Value, Filters} when is_list(Filters) -> {Value, Filters};
        {_, Value, Filter} when is_atom(Filter) -> {Value, [{Filter, []}]};
        {_, Value, Filter} -> {Value, [Filter]}
    end.

get_scope({_Id, Scope, _Values}) -> Scope;
get_scope(Scope) -> Scope.

merge_info1(1, _, _, Info) -> Info;
merge_info1(FieldIdx, Info1, Info2, Info) ->
    Value = lists:umerge(
              lists:usort(element(FieldIdx, Info1)),
              lists:usort(element(FieldIdx, Info2))),
    merge_info1(FieldIdx - 1, Info1, Info2, setelement(FieldIdx, Info, Value)).

close_scope(Fun, Id, AstList, TreeWalker) ->
    case merge_scopes(Id, TreeWalker) of
        {[], TreeWalker1} -> {AstList, TreeWalker1};
        {Values, TreeWalker1} ->
            {lists:foldl(
               fun ({ScopeId, ScopeValues}, AstAcc) ->
                       {Pre, Target, Post} = split_ast(ScopeId, AstAcc),
                       Pre ++ Fun(ScopeValues ++ Target) ++ Post
               end,
               AstList, Values),
             TreeWalker1}
    end.

merge_scopes(Id, #treewalker{ context=Context }=TreeWalker) ->
    {Values, Scopes} = merge_scopes(Id, Context#dtl_context.local_scopes, []),
    {lists:reverse(Values),
     TreeWalker#treewalker{
       context=Context#dtl_context{
                 local_scopes = Scopes
                } }}.

merge_scopes(Id, [{Id, _Scope, []}|Scopes], Acc) -> {Acc, Scopes};
merge_scopes(Id, [{Id, _Scope, Values}|Scopes], Acc) -> {[{Id, Values}|Acc], Scopes};
merge_scopes(Id, [{_ScopeId, _Scope, []}|Scopes], Acc) ->
    merge_scopes(Id, Scopes, Acc);
merge_scopes(Id, [{ScopeId, _Scope, Values}|Scopes], Acc) ->
    merge_scopes(Id, Scopes, [{ScopeId, Values}|Acc]);
merge_scopes(Id, [_PlainScope|Scopes], Acc) ->
    merge_scopes(Id, Scopes, Acc).


split_ast(Id, AstList) ->
    split_ast(Id, AstList, []).

split_ast(_Split, [], {Pre, Acc}) ->
    {Pre, lists:reverse(Acc), []};
split_ast(_Split, [], Acc) ->
    {[], lists:reverse(Acc), []};
split_ast(Split, [Split|Rest], {Pre, Acc}) ->
    {Pre, lists:reverse(Acc), Rest};
split_ast(Split, [Split|Rest], Acc) ->
    split_ast(end_scope, Rest, {lists:reverse(Acc), []});
split_ast(Split, [Ast|Rest], {Pre, Acc}) ->
    split_ast(Split, Rest, {Pre, [Ast|Acc]});
split_ast(Split, [Ast|Rest], Acc) ->
    split_ast(Split, Rest, [Ast|Acc]).

lib_module(Name, #dtl_context{ libraries=Libs }) ->
    Mod = proplists:get_value(Name, Libs, Name),
    case code:ensure_loaded(Mod) of
        {module, Mod} ->
            case implements_behaviour(erlydtl_library, Mod) of
                true ->
                    case Mod:version() of
                        ?LIB_VERSION -> {ok, Mod};
                        V -> {load_library, Name, Mod, {version, V}}
                    end;
               false -> {load_library, Name, Mod, behaviour}
            end;
        {error, Reason} ->
            {load_library, Name, Mod, Reason}
    end.

implements_behaviour(Behaviour, Mod) ->
    Attrs = Mod:module_info(attributes),
    Found =
        [B || [B] <- proplists:get_all_values(behaviour, Attrs)] ++
        [B || [B] <- proplists:get_all_values(behavior, Attrs)],
    [] =:= [Behaviour] -- Found.

read_library(Mod, Section, Which) ->
    [{Name, lib_function(Mod, Fun)}
     || {Name, Fun} <- read_inventory(Mod, Section),
        Which =:= [] orelse lists:member(Name, Which)].

warn_missing([], _, Context) -> Context;
warn_missing([X|Xs], {Pos, Lib, Mod}=Info, Context) ->
    warn_missing(Xs, Info, ?WARN({Pos, {load_from, Lib, Mod, X}}, Context)).

lib_function(_, {Mod, Fun}) ->
    lib_function(Mod, Fun);
lib_function(Mod, Fun) ->
    %% we could check for lib function availability here.. (sanity check)
    {Mod, Fun}.

read_inventory(Mod, Section) ->
    [case Item of
         {_Name, _Fun} -> Item;
         Fun -> {Fun, Fun}
     end || Item <- Mod:inventory(Section)].

remove_quotes(String) ->
    remove_last_quote(remove_first_quote(String)).

remove_first_quote([34 | Rest]) ->
    Rest;
remove_first_quote(String) ->
    String.

remove_last_quote(String) ->
    lists:reverse(remove_first_quote(lists:reverse(String))).
