%%% 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

%%% @doc This module  compares  the expressions of an Erlang  syntax tree to
%%% a RefactorErl database.

%%% @author Ely Deckers <e.deckers@student.ru.nl>

-module(refqc_tc_exprs).

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

-vsn("$Rev: 17551 $").

-export([referl_eq/2,referl_sub/1]).

-author("Ely Deckers <e.deckers@student.ru.nl>").

%% @spec referl_eq(Scope::list(),Node::syntaxTree()) -> bool()
%%
%% @doc Verify that the Expression parsed by Erlang is the same as the one in
%% the database.
referl_eq(Scope,Node) ->
    Type = erl_syntax:type(Node),

    ?QCTCLOG:writepath("?QCTCEXPR:referl_eq",Scope,Node),
    case Type of
        infix_expr ->
            referl_infix_eq(Scope,Node);
        case_expr ->
            referl_case_eq(Scope,Node);
        application ->
            referl_application_eq(Scope,Node);
        _Type ->
            log_referl_eq_primitive(Type,Node),
            referl_eq_primitive(Scope,Node)
    end.

%% @spec referl_eq_primitive(Scope::list(),Node::syntaxTree()) -> bool()
%%
%% @doc Verify that the primitive Expression parsed by Erlang is the same as
%% the one in the database.
referl_eq_primitive(Scope,Node) ->
    SubScope = ?QCTCEXPR:referl_sub(Scope),
    ?QCTCCOMPARE:traverse(SubScope,Node).

%% @private
%%
%% @spec log_referl_eq_primitive(Type::atom(),Node::syntaxTree()) -> bool()
%%
%% @doc Write a referl_eq_primitive action to a log file.
log_referl_eq_primitive(Type,Node) ->
    case Type of
        variable ->
            Representation = atom_to_list(erl_syntax:variable_name(Node));
        atom ->
            Representation = erl_syntax:atom_name(Node);
        integer ->
            Representation = erl_syntax:integer_literal(Node)
    end,

    ?QCTCLOG:append(["?QCTCEXPR:referl_eq_primitive (",
                        Representation,")"]).

%% @spec referl_sub({X::atom(),expr,NodeId::integer()}) -> list()
%%
%% @doc Get the <em>true</em> value of a referl expression node, and return it
%% as a new RefErl-node.
referl_sub({X,expr,NodeId}) ->
    Node = {X,expr,NodeId},

    Kind = ?Expr:type(Node),
    Value = ?Expr:value(Node),

    log_referl_sub(Kind,Value),

    {Kind,NodeId,Value}.

%% @private
%%
%% @spec referl_infix_eq(Scope::list(),Node::syntaxTree()) -> bool()
%%
%% @doc Verify that two infix expressions are the same (one from RefactorErl db
%% and the other from Erlang parse tree).
referl_infix_eq(Scope,Node) ->
    ?QCTCLOG:writepath("?QCTCEXPR:referl_infix_eq",Scope,Node),

    %Retrieve the operator and l+r nodes of the infix expression.
    ErlLeft = erl_syntax:infix_expr_left(Node),
    ErlRight = erl_syntax:infix_expr_right(Node),
    ErlOperator = erl_syntax:infix_expr_operator(Node),

    IsOperatorEq = compare_referl_infix_op(Scope,ErlOperator),

    if IsOperatorEq ->
        compare_referl_infix_lr(Scope,ErlLeft,ErlRight);
    true ->
        false
    end.

%% @private
%%
%% @spec compare_referl_infix_op(Scope::list(),
%% ErlOperator::syntaxTree()) -> bool()
%%
%% @doc Verify that the Erlang operator of an infix expression is equal to the
%% representation in the RefErl db.
compare_referl_infix_op(Scope,ErlOperator) ->
    %RefErl atom representation of operator
    RefOperatorAtom = ?Expr:value(Scope),

    %Erlang atom representation of operator
    ErlOperatorAtom = erl_syntax:atom_value(ErlOperator),

    (RefOperatorAtom == ErlOperatorAtom).

%% @private
%%
%% @spec compare_referl_infix_lr(Scope::list(), ErlLeft::syntaxTree(),
%%                                    ErlRight::syntaxTree()) -> bool()
%%
%% @doc Verify that Erlang lefthand and righthand sides of an infix expressions
%% are equal to those in the RefactorErl db.
compare_referl_infix_lr(Scope,ErlLeft,ErlRight) ->
    [RefLeft,RefRight] = ?Query:exec(Scope,?Expr:children()),

    LeftResult = referl_eq(RefLeft,ErlLeft),
    RightResult = referl_eq(RefRight,ErlRight),

    (LeftResult and RightResult).

%% @private
%%
%% @spec referl_case_eq(Scope::list(),Node::syntaxTree()) -> bool()
%%
%% @doc Verify that a Case statement parsed by Erlang is the same as the one
%% in the RefErl db.
referl_case_eq(Scope,Node) ->
    ?QCTCLOG:writepath("?QCTCEXPR:referl_case_eq",Scope,Node),

    Argument = erl_syntax:case_expr_argument(Node),

%TODO: This doesn't seem right / finished. Verify if it is ok?
    ArgumentSupScope = ?Query:exec(Scope,?Query:seq(
                                    [?Expr:clause(1),?Clause:body(1)])),

    IsValidArg = compare_referl_case_argument(ArgumentSupScope,Argument),

    %If the clause's argument was valid, then proceed to compare its
	%sub clauses.
    if IsValidArg ->
        compare_referl_case_clauses(Scope,Node);
    true ->
        false
    end.

%% @private
%%
%% @spec referl_application_eq(Scope::list(),Node::syntaxTree()) -> bool()
%%
%% @doc Verify that a Erlang and RefErl application are equivalent.
referl_application_eq(Scope,Node) ->
    ?QCTCLOG:append("?QCTCEXPR:referl_application_eq"),

    %Determine the construction of the Erlang application.
    ErlOperator = erl_syntax:application_operator(Node),
    ErlArgs = erl_syntax:application_arguments(Node),

    %First child of an application is the function name, others are arguments.
    Children = ?Query:exec(Scope,?Expr:children()),
    ReferlOperator = ?QCTCEXPR:referl_sub(hd(Children)),
    ReferlArgExprs = tl(Children),
    ReferlArgs = [ ?QCTCEXPR:referl_sub(ReferlArgExpr) ||
                    ReferlArgExpr <- ReferlArgExprs ],

    IsFuncEq = ?QCTCCOMPARE:traverse(ReferlOperator,ErlOperator),
    if IsFuncEq ->
        compare_application_args(ReferlArgs,ErlArgs);
    true ->
        false
    end.

%% @private
%%
%% @spec compare_application_args(ReferlArgs::list(),ErlArgs::list()) -> bool()
%%
%% @doc Verify that a case argument parsed by Erlang is the same as the one
%% in the RefErl db.
compare_application_args(ReferlArgs,ErlArgs) ->
    ?QCTCLOG:append("?QCTCEXPR:compare_application_args"),
    N = length(ReferlArgs),
    IsLengthEq = (N == length(ErlArgs)),

    if IsLengthEq ->
        ComparedArgs = compare_application_args_sub(ReferlArgs,ErlArgs),
        NOValid = length(lists:filter( true_filter(), ComparedArgs )),

        %All arguments should match.
        (NOValid == N);
    true ->
        false
    end.

%% @private
%%
%% @spec compare_application_args_sub(ReferlArgs::list(),
%% ErlArgs::list()) -> bool()
%%
%% @doc Helper function for @link compare_application_args/2
compare_application_args_sub([ReferlArg],[ErlArg]) ->
    [?QCTCCOMPARE:traverse(ReferlArg,ErlArg)];
compare_application_args_sub(ReferlArgs,ErlArgs) ->
    ReferlArg = hd(ReferlArgs),
    ErlArg = hd(ErlArgs),

    [?QCTCCOMPARE:traverse(ReferlArg,ErlArg)|
        compare_application_args_sub(tl(ReferlArgs),tl(ErlArgs))].

%% @private
%%
%% @spec compare_referl_case_argument(Scope::list(),
%% Argument::syntaxTree()) -> bool()
%%
%% @doc Verify that a case argument parsed by Erlang is the same as the one
%% in the RefErl db.
compare_referl_case_argument([],Argument) ->
    (Argument == []);
compare_referl_case_argument(Scope,Argument) ->
    %Compare arguments
%% TODO: Check if hd(Scope) is correct, and if so, replace function pattern
%% with [Scope],Argument and get rid of the 'hd'.
    ArgumentSubScope = referl_sub(hd(Scope)),
    ?QCTCCOMPARE:traverse(ArgumentSubScope,Argument).

%% TODO: Move to a general-purpose module
%% @private
%%
%% @spec true_filter() -> function()
%%
%% @doc For use as a filter function in lists:filter. It filters out
%% everything but the 'true' atom.
true_filter() ->
    (fun(X) -> X==true end).

%% @private
%%
%% @spec compare_referl_case_clauses(Scope::list(),
%% Node::syntaxTree()) -> bool()
%%
%% @doc Verify that the case clauses parsed by Erlang are the same as those
%% in the RefErl db.
compare_referl_case_clauses(Scope,Node) ->
    ?QCTCLOG:writepath("?QCTCEXPR:compare_referl_case_clauses",Scope,Node),
    Clauses = erl_syntax:case_expr_clauses(Node),

    CaseClauses = ?Query:exec(Scope,?Expr:clauses()),

    % Verify for each erl_syntax clause that it exists in the current scope.
    ComparedClauses = [ ?QCTCCOMPARE:traverse(CaseClauses,[Clause]) ||
                            Clause <- Clauses ],

    NOValid = length(lists:filter( true_filter(), ComparedClauses )),
    NOInput = length(Clauses),

    %All clauses should match.
    (NOValid == NOInput).

%% @private
%%
%% @spec log_referl_sub(Kind::atom(),Value::term()) -> ok | {error,What}
%%
%% @doc Log the referl_sub action.
log_referl_sub(Kind,Value) ->
    if (Kind==atom) ->
        Representation = atom_to_list(Value);
    true ->
        Representation = Value
    end,

    ?QCTCLOG:append(["?QCTCEXPR:referl_sub: (expr=",
                    atom_to_list(Kind),":",Representation,")"]).
