%%% 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 Daniel Horpacsi <daniel_h@inf.elte.hu>

-module(refqc_rename_mod).

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

%% Random module based testing callbacks
-export([prepare/1, perform_and_check/2]).
%% Interface
-export([prop_rename_mod/0]).

-record(data, {result, path, newpath, compresult1, oldname, newname,
               existingmodnames, filenamesindir, refstructbefore}).

prop_rename_mod() ->
    ?FORALL(Args, ?LAZY(?QCGEN:gen_args(renmod)), %% TODO: likely crashes
            perform_and_check(none, Args)).

prop_compile(Data) ->
    File = Data#data.newpath,
    CompResult = Data#data.compresult1,
    ?QCCOMMON:prop_compile(File,CompResult).

%% The transformation must not to be executed if there is any name
%% clash with the new name.
prop_nameclash(Data) ->
    not
      (lists:member(Data#data.newname, Data#data.existingmodnames) andalso
       lists:member(Data#data.newname, Data#data.filenamesindir)).

%% The reference structure of the module must be the same before and
%% after the transformation.
prop_references(Data) ->
    RefStructAfter = get_mod_ref_struct(),
    compare_ref_struct(Data#data.refstructbefore, RefStructAfter,
                       Data#data.oldname, Data#data.newname).

%% If the transformation succeeds, a file with the old path must not
%% exist in the database, and the file with the new path should occur
%% in the database.
prop_namechange(Data) ->
    (?Query:exec(?File:find(Data#data.path)) == [])
        andalso
          (length(?Query:exec(?File:find(Data#data.newpath))) == 1).

%% The transformation must be invertible, renaming the module back to
%% the original name must result the same module reference structure
%% as at the starting state.
prop_nameback(Data) ->
    Result2 =
        reftest_utils:exec_transform(
          rename_mod,
          inverted_args(Data#data.path, Data#data.oldname, Data#data.newname)),
    case Result2 of
        {result, _, _} ->
            RefStructInverse = get_mod_ref_struct(),
            compare_ref_struct(Data#data.refstructbefore, RefStructInverse,
                               Data#data.oldname, Data#data.oldname);
        _ -> true
    end.

collect_data_for_properties(Dir, Args) ->
    NewName = ?Args:name(Args),
    OldName = get_old_mod_name(Args),

    ExistingModNames = [?Mod:name(Mod) || Mod <- ?Query:exec([module])],
    FileNamesInDir = get_file_names_from_dir(Dir),
    Path = refqc_common:get_file(Args),

    RefStructBefore = get_mod_ref_struct(),
    CompResult1 = compile:file(Path, [strong_validation, return_errors]),
    NewPath = new_file_path(Args),

    #data{path = Path,
          newpath = NewPath,
          compresult1 = CompResult1,
          oldname = OldName,
          newname = NewName,
          existingmodnames = ExistingModNames,
          filenamesindir = FileNamesInDir,
          refstructbefore = RefStructBefore}.


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

get_old_mod_name(Args) ->
    Path = refqc_common:get_file(Args),
    list_to_atom(filename:basename(Path, ".erl")).

%% the function returns only the base names (without extension and path) of the
%% files
get_file_names_from_dir(Dir) ->
    ErlFiles = refqc_common:get_erl_files(Dir),
    [list_to_atom(filename:basename(File, ".erl")) || File <- ErlFiles].

compare_ref_struct(OldRefStruct, NewRefStruct, OldName, NewName) ->
    Before = lists:keyfind(OldName, 1, OldRefStruct),
    After  = lists:keyfind(NewName, 1, NewRefStruct),
    Rest   = (OldRefStruct -- [Before]) =:= (NewRefStruct -- [After]),
    lists:all(fun({A,B}) -> A==B end,
              lists:zip(element(2, Before) , element(2, After)))
        andalso
        Rest.

new_file_path(Args) ->
    Path = refqc_common:get_file(Args),
    Dir = filename:dirname(Path),
    NewName = ?Args:name(Args),
    filename:join(Dir, atom_to_list(NewName) ++ ".erl").

inverted_args(Path, OldName, NewName) ->
    Dir = filename:dirname(Path),
    NewPath = filename:join(Dir, atom_to_list(NewName) ++ ".erl"),
    [{file, NewPath}, {name, OldName}].

%% get the references to the module, also in other files it returns a
%% list of tuples, the first element of the tuple is the name of a
%% module the second is a list of tuples. This list contains all the
%% references to the actual module.  These tuples describe the
%% location of the references: the first element is the file node, the
%% second is an integer which indicates the position of the token in
%% the token list of the corresponding module.
get_mod_ref_struct() ->
    Modules = ?Query:exec([module]),
    FunLoc =
        fun(Ref)->
                [Token] = ?Query:exec(Ref, [elex]),
                [File]  = ?Syn:get_file(Ref),
                F =
                    fun(T, _Data, _Pos, _End, Acc) ->
                            case T of
                                Token -> {stop, Acc};
                                _ -> {next, Acc + 1}
                            end
                    end,
                ?Token:foldpos(F, 1, File)
        end,
    lists:usort([{?Mod:name(Mod),
                  lists:usort([{hd(?Syn:get_file(Ref)), FunLoc(Ref)}
                               || Ref <- ?Query:exec(Mod, ?Mod:references())])}
                 || Mod <- Modules]).


%% =============================================================================
%% Random module based testing callbacks

prepare(_Mods) -> ?LAZY(?QCGEN:gen_args(renmod)).

perform_and_check(_ModsOrFiles, Args) ->
    Dir = ".", %% TODO!
    Data = collect_data_for_properties(Dir, Args),
    Result = reftest_utils:exec_transform(rename_mod, Args),
    case Result of
        {result, _, _} ->
            prop_compile(Data) andalso prop_nameclash(Data)
                andalso prop_references(Data) andalso prop_namechange(Data)
                andalso prop_nameback(Data);
        _ ->
            io:format("\tTransformation failed."),
            true
    end.
