%%% This file is part of RefactorErl.
%%%
%%% RefactorErl is free software: you can redistribute it and/or modify
%%% it under the terms of the GNU Lesser General Public License as published
%%% by the Free Software Foundation, either version 3 of the License, or
%%% (at your option) any later version.
%%%
%%% RefactorErl is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%%% GNU Lesser General Public License for more details.
%%%
%%% You should have received a copy of the GNU Lesser General Public License
%%% along with RefactorErl.  If not, see <http://plc.inf.elte.hu/erlang/>.
%%%
%%% The Original Code is RefactorErl.
%%%
%%% The Initial Developer of the Original Code is Eötvös Loránd University.
%%% Portions created  by Eötvös Loránd University and ELTE-Soft Ltd.
%%% are Copyright 2007-2025 Eötvös Loránd University, ELTE-Soft Ltd.
%%% and Ericsson Hungary. All Rights Reserved.


%%% ============================================================================
%%% Module information

%%% @author Istvan Bozo <bozo_i@inf.elte.hu>
%%% @author Ely Deckers <e.deckers@student.ru.nl>

-module(refqc_common).

-vsn("$Rev:$ ").

-include_lib("referl_qc/include/qc.hrl").

-export([get_erl_files/1,
         files_with_path/2,
         initialize/1, initialize/2, initialize/3,
         get_file/1,
         clear_database/0,
         load_database/1,
         get_n/2,
         get_token_pos/2,
         get_funrefs/1,
         get_binding_struct/1,
         prop_compile/2,
         prop_compile/3,
         prop_semtree/3,
         get_files_in_database/0,
         get_funs/1,
         get_fun_args/1,
         get_arg_binding_struct/1,
         args_eq/2
        ]).


-export([generate_semtrees/2]).
generate_semtrees(Files, OptionList) ->
    %% Retrieve all function nodes in the loaded files
    FunNodes = get_funs(Files),
    %% For all function nodes, retrieve the name and arity
    ScArgs = lists:usort([{?Fun:name(FunNode), ?Fun:arity(FunNode)} || FunNode <- FunNodes]),
    % always take the first clause of a function; maybe change this in the future
    ScResults = [?QCSCSYMCOMP:sc(FunName, Arity, 1, OptionList) || {FunName, Arity} <- ScArgs],
    [SemTree || {SemTree, _} <- ScResults].


%% @doc Returns the erlang files from the given directory and the modules.
%% @spec get_erl_files( Dir :: string() ) -> [string()]
get_erl_files(Dir) ->
    {ok, F} = file:list_dir(Dir),
    Files = files_with_path(Dir, F),
    [File || File <- Files,
             filelib:is_regular(File), filename:extension(File) =:= ".erl"].

%% @doc Gets a path and a list of file names and returns full paths
%% @spec files_with_path( Dir :: string(), Files :: [string()] ) -> [string()]
files_with_path(Dir, Files) ->
    [filename:join(Dir, File) || File <- Files].

%% @doc Resets the database and adds new files from the given directory.
%% @todo replace body with the body of initialize(files,_) function
%%
%% @spec initialize(Dir :: string()) -> [atom()]
initialize(Dir)->
    ErlFiles = get_erl_files(Dir),
    initialize(files,ErlFiles).

%% @doc Alias for initialize(files, ErlFiles :: [string()],
%% [{block,false}]) -> [atom()]
%%
%% @spec initialize(files,ErlFiles :: [string()]) -> [atom()]
initialize(files,ErlFiles) ->
    initialize(files,ErlFiles,[{block,false}]).

%% @doc Resets the database and adds new files from the given file list
%% @todo determine which options should be passed to which functions, rather
%% than passing all of them.
%%
%% @spec initialize(files,ErlFiles :: [string()],[{atom(),atom()}]) -> [atom()]
initialize(files,ErlFiles,Options) ->
    io:format("Dropping modules from the database~n"),
    clear_database(),
    io:format("Adding erlang files to the database~n"),
    load_database(ErlFiles,Options),
    io:format("Loading the database finished~n").

%% @doc Drops all files from the database.
%%
%% @spec clear_database() -> [atom()]
clear_database()->
    Files = ?Query:exec([file]),
    [ begin ?FileMan:drop_file(File) end || File <- Files].


%% @doc Same as `load_database(ErlFiles,[{block,false}])'.
%%
%% @spec load_database(Files :: [string()]) -> [atom()]
load_database(ErlFiles)->
    load_database(ErlFiles,[{block,false}]).

%% @doc Loads list of Erlang files into the database.
%%
%% @spec load_database(Files :: [string()],
%%   Options :: [{atom(),atom()}]) -> [atom()]
load_database(ErlFiles,Options)->
    % functions for adding files either in a synchronous or asynchronous fashion
    NoBlock = fun(X) -> ?UI:add(X) end,
    Block = fun(X) ->
                ?UI:add(X),
                referl_event_helper:wait({uifinished, add})
            end,

    % list filter, which returns only tuples that start with `block'.
    BlockFilter = fun({block,_})-> true; (_) -> false end,

    BlockOption = lists:filter(BlockFilter,Options),

    % apply the desired function.
    case BlockOption of
        [{block,true}] -> lists:map(Block,ErlFiles);
        _ -> lists:map(NoBlock,ErlFiles)
    end.

%% @doc Returns a list of file nodes that are currently loaded into
%% the database
%%
%% @spec get_files_in_database() -> [node()]

get_files_in_database() -> ?Query:exec([file]).

%% @doc Takes `N' element from the given list `List'
%%
%% @spec get_n(N :: integer(), List :: [any()]) -> [any()]

get_n(N, List) ->
    [ begin
          M = rand:uniform(length(List)),
          lists:nth(M, List)
      end
      || _ <- lists:seq(1, N)].

%% @doc Returns the path to the current file from the argument list.
%%
%% @spec get_file(Args :: reflib_args:arglist()) -> string()

get_file(Args) ->
    {file, File} = lists:keyfind(file,1, Args),
    File.

%% @doc Returns the position of the given token in the given file.
%% @spec get_token_pos(File :: node(), TokenNode :: node()) -> integer()

get_token_pos(File, TokenNode) ->
    AllLeaves   = ?Syn:leaves(File),
    {Pos, _, _} = reftest_stress_test:pos_before(AllLeaves, TokenNode),
    Pos + 1.

%% @doc Same as prop_compile(File,CompResult,none)
%%
%% @see prop_compile/3
%%
%% @spec prop_compile(File :: string(), CompResult :: tuple() ) -> boolean()
prop_compile(File,CompResult) ->
    prop_compile(File,CompResult,none).

%% @doc This is the one property that holds for _every_ transformation: iff a
%% module compiles before transformation, it _must_ do so after.
%%
%% @spec prop_compile(File :: string(), CompResult :: tuple(),
%% AfterFun :: fun(tuple()) ) -> boolean()
prop_compile(File,CompResult,AfterFun) ->
    CompResult2 = compile:file(File, [strong_validation, return_errors]),

    Result = case {CompResult, CompResult2} of
        {{ok, _}, {ok,_}} ->
            true;
        {{error, _}, {error, _}} ->
            true;
        _ ->
            false
    end,

    if ((AfterFun /= none) andalso  (CompResult =/= CompResult2)) ->
        AfterFun(CompResult2);    %useful for debugging information for example.
    true ->
        ok
    end,

    Result.

prop_semtree(SemTreesBefore, Files, OptionList) ->
    SemTreesAfter = generate_semtrees(Files, OptionList),
    lists:usort(SemTreesBefore) == lists:usort(SemTreesAfter).

%% @doc Get all funrefs to a certain function (i.e.: applications, exports,
%% implicits, imports).
%%
%% @spec get_funrefs(Fun :: funnode()) -> [node()]
get_funrefs(Fun) ->
    Apps    = ?Query:exec(Fun, ?Query:seq(?Fun:applications(), ?Expr:child(1))),
    Exports = ?Query:exec(Fun, ?Query:seq(?Fun:exports(), ?Expr:child(1))),
    Impls   = ?Query:exec(Fun, ?Query:seq(?Fun:implicits(), ?Expr:child(1))),
    Imports = ?Query:exec(Fun, ?Query:seq(?Fun:imports(), ?Expr:child(1))),

    Apps ++ Exports ++ Impls ++ Imports.

%% @doc Returns the binding structure of a varnode() or a funnode()
%%
%% @spec get_binding_struct(Node :: node()) -> [{BLoc :: int(),RLoc :: int()}]
get_binding_struct(Node) ->
    case Node of
        [] ->
            {[],[]};
        {_,func,_} ->
            get_binding_struct_fun(Node);
        {_,variable,_} ->
            get_binding_struct_var(Node)
    end.

%% @doc Returns locations (token-locations, not characters) of appearance of a
%% certain node in a module.
%%
%% @spec get_binding_struct_loc(File :: string(), V :: node()) -> [int()]
get_binding_struct_loc(File,V) ->
    Token =
        hd(?Query:exec(V, [elex])),
    F =
        fun(T, _Data, _Pos, _End, Acc) ->
                case T of
                    Token ->
                        {stop, Acc};
                    _ ->
                        {next, Acc + 1}
                end
        end,
    ?Token:foldpos(F, 1, File).

%% @doc Returns binding structure of a function ("'Binding structure' here
%% refers to the association of uses of identifiers with their definitions in a
%% program, and is determined by the scope of the identifiers" @reference
%% Huiqing Li and Simon Thompson, <em>Testing Erlang Refactorings
%% with QuickCheck</em>, Kent University, 2007).
%%
%% @spec get_binding_struct_fun(Fun :: funnode()) -> tuple()
get_binding_struct_fun(Fun) ->
    Loc = fun(X,Y) -> get_binding_struct_loc(X,Y) end,

    Refs = get_funrefs(Fun),

    if (Refs /= []) ->
        [File] = ?Syn:get_file(hd(Refs)),

        %Put all lexical tokens of function references (for Fun) in a list.
        RLoc = lists:usort([Loc(File,R) || R <- Refs]),
        DLoc = [], %Definition of the function TODO: Add this.

        {DLoc,RLoc};
    true ->
        []
    end.

%% @doc Returns the binding structure of a certain variable.
%%
%% @spec get_binding_struct_var( Var :: node() ) -> tuple()
get_binding_struct_var(Var) ->
    Loc = fun(X,Y) -> get_binding_struct_loc(X,Y) end,

    Bindings = ?Query:exec(Var, ?Var:bindings()),
    References = ?Query:exec(Var, ?Var:references()),

    [File] = ?Syn:get_file(hd(Bindings)),

    BLoc = lists:usort([Loc(File,B) || B <- Bindings]),
    RLoc = lists:usort([Loc(File,R) || R <- References]),

    {BLoc, RLoc}.


%%% ============================================================================

%% @doc return all the functions defined in certain files
%% @spec get_funs([filenode()]|{modules, [modulenode()]}) -> [funnode()]
get_funs({modules, Modules}) ->
    lists:flatten(?Query:exec(Modules, ?Mod:locals()));
get_funs(Files) ->
    lists:flatten(?Query:exec(Files, ?Query:seq(?File:module(), ?Mod:locals()))).

%% @doc returns all arguments (clause pattern) of a function.
%% @spec get_fun_args( Fun :: funnode() ) -> [ [ expr() ] ]
get_fun_args(Fun) ->
    Clauses = ?Query:exec(Fun, ?Query:seq(?Fun:definition(), ?Form:clauses())),
    [?Query:exec(Clause, ?Clause:patterns()) || Clause <- Clauses].

%%% ============================================================================

get_arg_binding_struct(ArgList) when is_list(ArgList) ->
    [get_arg_binding_struct(ArgsOrArg) || ArgsOrArg <- ArgList];
get_arg_binding_struct(Arg) ->
    handle_arg_binding(Arg).

handle_arg_binding(Arg) ->
    Kind = ?Expr:type(Arg),
    case Kind of
        variable ->
            [X] = ?Query:exec(Arg,?Expr:variables()),
            ?QCCOMMON:get_binding_struct(X);
        _ -> {undefined, undefined}
    end.

%% @doc Compares two lists of arguments
%% @spec args_eq(ArgsBefore :: list(node()),ArgsAfter :: list(node())) ->
%% boolean()
args_eq([], []) -> true;
args_eq([ArgBefore | XS],[ArgAfter | YS]) ->
    args_eq(ArgBefore, ArgAfter) andalso args_eq(XS, YS);
args_eq(X, Y) -> arg_is_valid(X, Y).

arg_is_valid(ArgBefore, ArgAfter) ->
    KindBefore = ?Expr:type(ArgBefore),
    KindAfter = ?Expr:type(ArgAfter),
    KindBefore =:= KindAfter
        andalso
          ((KindBefore =/= atom) or (?Expr:value(ArgBefore) == ?Expr:value(ArgAfter))).
