%%% 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(refusr_rule_check_handler).

-include("user.hrl").
-export([handle_rule/4]).

-define(Metrics, refusr_metrics).

-spec handle_rule(Type, string(), any(), list(string())) -> list({atom(), Result}) when
  Type :: sem_query | builtin,
  Result :: ok | {nok, list(string())}.
handle_rule(sem_query, Query, _Args, ModList) ->
  LoadedMods = get_modules(ModList),
  ModNames = [?Mod:name(M) || M <- LoadedMods],
  Nodes = [ proplists:get_value(nodes,
                                refusr_sq:run([{output, nodes}],
                                              [{node_list, [M]}], Query)) || M <- LoadedMods],
  NodeTexts = lists:map(fun(N) -> format_returns({nok, N}) end, Nodes),
  lists:zip(ModNames, NodeTexts);

handle_rule(builtin, Query, Args, ModList) ->
  LoadedMods = get_modules(ModList),
  ModNames = [?Mod:name(M) || M <- LoadedMods],
  PackedNodes = [ handle_builtin_rule(list_to_atom(Query), Args, M) || M <- LoadedMods],
  NodeTexts = lists:map(fun(N) -> format_returns(N) end, PackedNodes),
  lists:zip(ModNames, NodeTexts).


-spec format_returns(ok | {nok, list(any())} | {nok_stat, any()}) -> ok | {nok, list(string())}.
format_returns(ok) -> ok;
format_returns({nok, []}) -> ok;
format_returns({nok, List}) when is_list(List) ->
  {nok, [refusr_sq:format_nodes([N], both) || N <- List]};
format_returns({nok_stat, Stat}) -> {nok, [io_lib:format("~p", [Stat])]}.

get_modules(ModList) ->
  RawMods = ?Query:exec(?Mod:all()),
  InputModNames = sets:from_list([list_to_atom(M) || M <- ModList]), %% existing_atom?
  [RM || RM <- RawMods, sets:is_element(?Mod:name(RM), InputModNames)].

handle_builtin_rule('clauses-limit', [Num], Module) ->
  clauses_limit(Module, Num);
handle_builtin_rule('exported-functions-limit', [Num], Module) ->
  exported_functions_limit(Module, Num);
handle_builtin_rule('exported-without-spec', [], Module) ->
  exported_without_spec(Module);
handle_builtin_rule('used-underlined-var', [], Module) ->
  used_underlined_var(Module);
handle_builtin_rule('find-function-call', [Mod, Name], Module) ->
  find_called_function(Module, {list_to_atom(Mod), list_to_atom(Name)});
handle_builtin_rule('find-function-call', [Mod, Name, Arity], Module) ->
  find_called_function(Module, {list_to_atom(Mod), list_to_atom(Name), Arity});
handle_builtin_rule('find-io-format', [], Module) ->
  io_format_found(Module);
handle_builtin_rule('no-imports', [], Module) ->
  no_imports(Module);
handle_builtin_rule('tag-messages', [], Module) ->
  tag_messages(Module);
handle_builtin_rule('flush-message-box', [], Module) ->
  flush_unknown_messages(Module);
handle_builtin_rule('tail-recursive-servers', [], Module) ->
  tail_recursive_servers(Module);
handle_builtin_rule('macro-naming', [], Module) ->
  macro_names(Module);
handle_builtin_rule('no-nested-try-catch', [], Module) ->
  no_nested_try_catch(Module);
handle_builtin_rule('module-naming', [Regex], Module) ->
  module_naming_convention(Module, Regex);
handle_builtin_rule('function-naming', [Regex], Module) ->
  function_naming_convention(Module, Regex);
handle_builtin_rule('state-for-otp-behaviors', [], Module) ->
  state_record_for_behavior(Module);
handle_builtin_rule(Query, Args, Module) ->
  throw({unhandled_query, Query, Args, Module}).

%%% ============================================================================
exported_without_spec(M) ->
  ExpFuns = ?Query:exec(M, ?Mod:exports()),
  NoSpec = [ F || F <- ExpFuns, not ?Fun:has_spec(F), ?Fun:is_exported(F)],
  pack_list_into_return(NoSpec).

%%% ============================================================================
exported_functions_limit(Mod, Limit) ->
  ExpFuns = ?Query:exec(Mod, ?Mod:exports()),
  Count = erlang:length(ExpFuns),
  if
    Count =< Limit -> ok;
    true -> {nok_stat, Count}
  end.

%%% ============================================================================
used_underlined_var(M) ->
  do_for_all_funs_in_module(M, fun used_underlined_var_fun/1).

used_underlined_var_fun(Fun) ->
  Refs = ?Query:exec(Fun, ?Query:seq([?Fun:definition(),
                                      ?Form:deep_exprs(),
                                      ?Expr:varrefs()])),
  VarNames = [ {V , ?Var:name(V)} || V <- Refs],
  WrongNames = [ Var || {Var, Name}  <- VarNames, lists:prefix([$_], Name)],
  UniqueNames = lists:usort(WrongNames),
  Tops = lists:flatten([ ?Query:exec(E, ?Query:seq([?Var:references(), ?Expr:top()])) || E <- UniqueNames]),
  pack_list_into_return(Tops). % return UniqueNames if you only want the names

%%% ============================================================================
clauses_limit(Module, N) ->
  do_for_all_funs_in_module(Module, fun clauses_limit_fun/2, N).

clauses_limit_fun(F, N) ->
  FunClauses = ?Query:exec(F, ?Query:seq(?Fun:definition(), ?Form:clauses())),
  L = erlang:length(FunClauses),
  {_M, MFA} = ?Fun:mod_fun_arity(F),
  if
    L > N -> {nok_stat, {MFA, L}};
    true -> ok
  end.

%%% ============================================================================
io_format_found(M) ->
  find_called_function(M, {'io','format'}).

find_called_function(M, {ModName, FunName, Arity}) ->
  Fs = ?Query:exec(M, ?Mod:locals()),
  FunEnt = ?Query:exec(?Query:seq(reflib_module:find(ModName), reflib_function:find(FunName, Arity))),
  TopExprs = ?Query:exec(Fs, ?Query:seq([?Fun:definition(), ?Form:clauses(), ?Clause:exprs()])),
  Apps = lists:flatten([?Query:exec(FunEnt, ?Fun:applications(TopExpr)) || TopExpr <- TopExprs]),
  pack_list_into_return(lists:usort(Apps));

find_called_function(M, {ModName, FunName}) ->
  Fs = ?Query:exec(M, ?Mod:locals()),
  FunEnt = ?Query:exec(?Query:seq(reflib_module:find(ModName), find_fun(FunName))),
  TopExprs = ?Query:exec(Fs, ?Query:seq([?Fun:definition(), ?Form:clauses(), ?Clause:exprs()])),
  Apps = lists:flatten([?Query:exec(FunEnt, ?Fun:applications(TopExpr)) || TopExpr <- TopExprs]),
  pack_list_into_return(lists:usort(Apps)).


%%% ============================================================================
no_imports(M) ->
  Imports = ?Query:exec(M, ?Mod:imports()),
  pack_list_into_return(Imports).

%%% ============================================================================
%%% http://www.erlang.se/doc/programming_rules.shtml
%%% Variables that are underscored, or named Other, Error, Unexpected are not wrong
tag_messages(M) ->
  do_for_all_funs_in_module(M, fun tag_messages_fun/1).

tag_messages_fun(Fun) ->
  RecExprs = get_receive_exprs(Fun),
  Patterns = reflib_query:exec(RecExprs, reflib_query:seq(reflib_expression:clauses(),
                                                          reflib_clause:patterns())),
  pack_list_into_return(lists:filter(fun is_incorrect_pattern/1, Patterns)).

is_incorrect_pattern(Expr) ->
  case ?Expr:type(Expr) of
    atom -> false;
    tuple -> is_incorrect_tuple_pattern(Expr);
    variable ->
      Name = ?Expr:value(Expr),
      Correct = (lists:prefix([$_], Name)) orelse (Name == "Error") orelse
        (Name == "Other") orelse (Name == "Unexpected"),
      not Correct;
    _ -> true
  end.

is_incorrect_tuple_pattern(Expr) ->
  Elements = reflib_query:exec(Expr, reflib_expression:children()),
  case Elements of
    [H | _] ->
      reflib_expression:type(H) /= atom;
    _ -> true
  end.

%%% http://www.erlang.se/doc/programming_rules.shtml
%%% Every server should have an Other alternative in at least one receive statement.
%%% ============================================================================
flush_unknown_messages(M) ->
  do_for_all_funs_in_module(M, fun flush_unknown_messages_fun/1).

flush_unknown_messages_fun(Fun) ->
  case is_rec_function(Fun) of
    false -> ok;
    true ->
      RecExprs = get_receive_exprs(Fun),
      Clauses = ?Query:exec(RecExprs, ?Expr:clauses()),
      ClausesWithoutGuards = [ C ||  C <- Clauses, ?Query:exec(C, ?Clause:guard()) == []],
      Patterns = ?Query:exec(ClausesWithoutGuards, ?Clause:patterns()),
      case {lists:any(fun is_catch_all_pattern/1, Patterns), RecExprs} of
        {true, _} -> ok;
        {_, []} -> ok;
        _ -> {nok, [Fun]}
      end
  end.

is_catch_all_pattern(Expr) ->
  case ?Expr:type(Expr) of
    variable ->
      Bindings = ?Query:exec(Expr, ?Query:seq(?Expr:variables(), ?Var:bindings())),
      case Bindings of
        %% the variable is not bound previously, it is bound in the pattern
        [BindingExpr] -> Expr == BindingExpr;
        _ -> false
      end;
    _ -> false
  end.

is_rec_function(F) ->
  case ?Query:exec(F, ?Fun:definition()) of
    [_FunDef] ->
      0 =< ?Metrics:metric({is_tail_recursive, function, F});
    [] -> false
  end.

%%% ============================================================================
% http://www.erlang.se/doc/programming_rules.shtml
% if a function is recursive, and if there is a receive clause, it should be tail recursive
tail_recursive_servers(M) ->
  Funs = ?Query:exec(M, ?Mod:locals()),
  RecFuns = lists:filter(fun rec_but_not_tailrec_function/1, Funs),
  Servers  = lists:filter(fun has_receive_expression/1, RecFuns),
  pack_list_into_return(Servers).

rec_but_not_tailrec_function(F) ->
  case ?Query:exec(F, ?Fun:definition()) of
    [_FunDef] ->
      0 == ?Metrics:metric({is_tail_recursive, function, F});
    [] -> false
  end.

has_receive_expression(F) ->
  [] /= get_receive_exprs(F).

%%% ============================================================================
% https://github.com/inaka/elvis_core/wiki/Rules#macro-names
% Macro names should only contain upper-case alphanumeric characters.
macro_names(M) ->
  Macros = ?Query:exec(M, ?Query:seq(?Mod:file(), ?File:macros())),
  BadMacros = [Macro || Macro <- Macros, is_invalid_macro_name(?Macro:name(Macro))],
  pack_list_into_return(BadMacros).

is_invalid_macro_name(Name) ->
  case re:run(Name, "^[A-Z0-9_]*$") of
    nomatch -> true;
    _ -> false
  end.

%%% ============================================================================
no_nested_try_catch(M) ->
  do_for_all_funs_in_module(M, fun no_nested_try_catch_fun/1).

no_nested_try_catch_fun(Fun) ->
  Outers = get_exprs(Fun, try_expr),
  pack_list_into_return(lists:filter(fun contains_try_expr/1, Outers)).

contains_try_expr(Expr) ->
  SubExprs = ?Query:exec(Expr, ?Expr:deep_sub()),
  lists:any(fun(Ex) -> (?Expr:type(Ex) == try_expr) and (Expr /= Ex) end, SubExprs).

%%% ============================================================================
module_naming_convention(M, Regex) ->
  Name = atom_to_list(?Mod:name(M)),
  case re:run(Name, Regex) of
    nomatch -> {nok, [M]};
    _ -> ok
  end.

%%% ============================================================================
function_naming_convention(M, Regex) ->
  do_for_all_funs_in_module(M, fun function_naming_convention_fun/2, Regex).

function_naming_convention_fun(Fun, Regex) ->
  Name = atom_to_list(?Fun:name(Fun)),
  case re:run(Name, Regex) of
    nomatch -> {nok, [Fun]};
    _ -> ok
  end.

% https://github.com/inaka/elvis_core/wiki/Rules#state-record-and-type
% Every module that implements an OTP behavior in the following list should have a
% state record (#state{}) and a state type (type is optional)
state_record_for_behavior(M) ->
  File = ?Query:exec(M, ?Mod:file()),
  Forms = ?Query:exec(File, ?File:real_forms()),
  Texts = [ (?Graph:data(F))#form.cache || F <- Forms, ?Form:type(F) == behavior],
  case lists:any(fun is_searched_behavior/1, Texts) of
    true ->
      StateRec = ?Query:exec(File, ?File:record(state)),
      case StateRec of
        [] -> {nok, [M]};
        _ -> ok
      end;
    false -> ok
  end.

is_searched_behavior(Text) ->
  case re:run(Text, "gen_server|gen_event|gen_fsm|supervisor_bridge|gen_statem") of
    nomatch -> false;
    _ -> true
  end.

%% =============================
%% ----- Support functions -----
%% =============================
get_receive_exprs(Fun) ->
  get_exprs(Fun, receive_expr).

get_exprs(Fun, Type) ->
  Exprs = lists:flatten(?Query:exec(Fun, ?Query:seq([?Fun:definition(),
                                                     ?Form:clauses(),
                                                     ?Clause:exprs(),
                                                     ?Expr:deep_sub()]))),
  lists:filter(fun(E) -> ?Expr:type(E) == Type end, Exprs).


%% @spec find_fun(atom()) -> query(#module{}, #func{})
%% @doc The result query returns the function with name `Name'.
find_fun(Name) ->
  [{func, {name, '==', Name}}].


do_for_all_funs_in_module(M, Op, Args) when is_function(Op, 2)->
  Funs = ?Query:exec(M, ?Mod:locals()),
  Rets = [ Op(F, Args) || F <- Funs ],
  filter_nok(Rets).

do_for_all_funs_in_module(M, Op) when is_function(Op, 1) ->
  Funs = ?Query:exec(M, ?Mod:locals()),
  Rets = [ Op(F) || F <- Funs ],
  filter_nok(Rets).


filter_nok([]) -> ok;
filter_nok([{nok, R} | T]) ->
  {nok, lists:flatten([R | do_filter_nok(T)])};
filter_nok([{nok_stat, R} | T]) ->
  {nok_stat, lists:flatten([R | do_filter_nok(T)])};
filter_nok([_H | T]) -> filter_nok(T).

do_filter_nok([]) -> [];
do_filter_nok([{nok, Ret} | T]) -> [Ret | do_filter_nok(T)];
do_filter_nok([{nok_stat, Ret} | T]) -> [Ret | do_filter_nok(T)];
do_filter_nok([_H | T]) -> do_filter_nok(T).


pack_list_into_return([]) -> ok;
pack_list_into_return(L = [_H | _T]) -> {nok, L}.