%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2005-2011. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%
%% ------------------------------------------------------------------------------
%% File    : inviso_rt_lib.erl
%% Author  : Lennart Öhman <lennart@st.se>
%% Description : 
%%
%% Created : 27 Sep 2005 by Lennart Öhman <lennart@st.se>
%% ------------------------------------------------------------------------------
-module(inviso_rt_lib).

-export([expand_regexp/2,expand_regexp/3,expand_regexp/4]).
-export([is_tracerdata/1]).
-export([transform/2]).

-export([rpc/4,rpc/5,match_modules/2,match_modules/3]).
-export([debug/3]).

%% ------------------------------------------------------------------------------

%% ==============================================================================
%% Exported API functions.
%% ==============================================================================

%% ------------------------------------------------------------------------------
%% expand_regexp(Nodes,RegExpDir,RegExpMod,Opts) = [{Node,Answer},...] | {error,Reason}
%% expand_regexp(Nodes,RegExpMod,Opts) = [{Node,Answer},...] | {error,Reason}
%% expand_regexp(RegExpDir,RegExpMod,Opts) = ListOfModules | {error,Reason}
%% expand_regexp(RegExpMod,Opts) = ListOfModules | {error,Reason}
%%   Nodes=List of all nodes (atoms) where to expand.
%%   RegExpDir=Reg.exp (string) specifying directories.
%%   RegExpMod=Reg.exp (string) specifying module names.
%%   Node=node name (atom).
%%   Opts=[Opt,...]
%%   Opt=only_loaded
%%   Answer=List of modules (atoms) | 'badrpc'
%%
%% Expands, concurrently, the regular expression on Nodes and returns a list
%% of what modules it expanded to on the different nodes. Note that it may
%% differ between Erlang nodes depending on whether the modules are the same
%% or not. Also note that all modules becomes loaded as a result.
%% RegExpDir can further limit the modules. It introduces the requirement that
%% a module must be loaded from a directory with a path satisfying the RegExpDir.
%% All regular expression are according to the standard lib regexp module.
expand_regexp(RegExpMod,Opts) when is_list(RegExpMod),is_list(Opts) ->
    match_modules(RegExpMod,Opts);
expand_regexp(RegExpMod,Opts) ->
    {error,{badarg,[RegExpMod,Opts]}}.
expand_regexp(NodesOrRegExpDir,RegExpMod,Opts)
  when is_list(NodesOrRegExpDir),is_list(RegExpMod),is_list(Opts) ->
    case is_list_of_atoms(NodesOrRegExpDir) of
	true ->                              % Interpret as list of nodes.
	    lists:foreach(fun(N)->spawn(?MODULE,rpc,[self(),N,RegExpMod,Opts]) end,
			  NodesOrRegExpDir),
	    expand_regexp_answers(NodesOrRegExpDir,[]);
	false ->                             % Interpret as a string.
	    match_modules(NodesOrRegExpDir,RegExpMod,Opts)
    end;
expand_regexp(NodesOrRegExpDir,RegExpMod,Opts) ->
    {error,{badarg,[NodesOrRegExpDir,RegExpMod,Opts]}}.
expand_regexp(Nodes,RegExpDir,RegExpMod,Opts)
  when is_list(Nodes),is_list(RegExpDir),is_list(RegExpMod),is_list(Opts) ->
    lists:foreach(fun(N)->
			  spawn(?MODULE,rpc,[self(),N,RegExpDir,RegExpMod,Opts])
		  end,
		  Nodes),
    expand_regexp_answers(Nodes,[]);
expand_regexp(Nodes,RegExpDir,RegExpMod,Opts) ->
    {error,{badarg,[Nodes,RegExpDir,RegExpMod,Opts]}}.

expand_regexp_answers([],Answers) -> Answers;  % List of [{Node,Answer},...].
expand_regexp_answers(Nodes,Answers) ->
    receive
	{?MODULE,Node,Answer} ->
	    expand_regexp_answers(lists:delete(Node,Nodes),[{Node,Answer}|Answers])
    end.
%% ------------------------------------------------------------------------------

%% is_tracerdata(TracerData)=true|false
%% Answers the question if TracerData is proper tracerdata. Note that true can be
%% returned if it resembles tracerdata very closely.
is_tracerdata({Fun,_Data}) when is_function(Fun) -> true;
is_tracerdata({relayer,To}) when is_pid(To);is_atom(To) -> true;
is_tracerdata(collector) -> true;
is_tracerdata({file,Param}) when is_tuple(Param);is_list(Param) -> true;
is_tracerdata({ip,_Param}) -> true;
is_tracerdata([{trace,LogTD}|Rest]) ->
    case is_tracerdata(LogTD) of
	true ->
	    is_tracerdata(Rest);
	false ->
	    false
    end;
is_tracerdata([{ti,TiData}|Rest]) ->
    case is_tidata(TiData) of
	true ->
	    is_tracerdata(Rest);
	false ->
	    false
    end;
is_tracerdata([]) ->
    true;
is_tracerdata(_) ->
    false.

is_tidata({file,FileName}) when is_list(FileName) -> true; 
is_tidata({file,FileName,{M,F,Args}}) when is_list(FileName),is_atom(M),is_atom(F),is_list(Args) ->
    true;
is_tidata(_) -> false.
%% ------------------------------------------------------------------------------		


%% ==============================================================================
%% Help functions.
%% ==============================================================================

%% Help function intended to be run in its own process. Will report with
%% a message when done.
%% This function will be spawned on.
rpc(Parent,Node,RegExpMod,Opts) ->			   
    case rpc:call(Node,?MODULE,match_modules,[RegExpMod,Opts]) of
	{badrpc,_Reason} ->                  % The node is probably not healthy.
	    Parent ! {?MODULE,Node,badrpc};
	Modules ->
	    Parent ! {?MODULE,Node,Modules}
    end.

rpc(Parent,Node,RegExpDir,RegExpMod,Opts) ->			   
    case rpc:call(Node,?MODULE,match_modules,[RegExpDir,RegExpMod,Opts]) of
	{badrpc,_Reason} ->                  % The node is probably not healthy.
	    Parent ! {?MODULE,Node,badrpc};
	Modules ->
	    Parent ! {?MODULE,Node,Modules}
    end.
%% ------------------------------------------------------------------------------


%% ==============================================================================
%% Exported function which actually shall be in code.erl.
%% ==============================================================================

%% match_modules(RegExpMod,Actions) = [Module,...] | {error,Reason}
%% match_modules(RegExpDir,RegExpMod,Actions)=[Module,...] | {error,Reason}
%%   RegExpMod=Erlang regular expression describing module names (string).
%%   RegExpDir=Erlang regular expression describing directory paths(string) |
%%             void
%%   Actions=List of;'only_loaded'.
%%
%% Function which matches a regular expresion against module names. The function
%% can also match the directory from where the module is loaded or will be loaded
%% against a regular expresion for directory paths.
%% The function uses the same strategy as code-loading if the same module is
%% discovered in several places.
%% (1) An already loaded module shadows all other occurancies.
%% (2) .beams found in by a path shadows .beams found by paths later in the
%%   code paths.
%%
%% Description of actions:
%% only_loaded: Only consider modules which are loaded.
match_modules(RegExpMod,Actions) ->
    match_modules(void,RegExpMod,Actions).
match_modules(RegExpDir,RegExpMod,Actions) ->
    AllLoaded=code:all_loaded(),
    Mods1=handle_expand_regexp_2(AllLoaded,RegExpDir,RegExpMod,[]),
    case lists:member(only_loaded,Actions) of % Shall we do not loaded too?
	false ->                             % Ok, search all paths too then.
	    Paths=code:get_path(),
	    handle_expand_regexp_3(Paths,RegExpDir,RegExpMod,AllLoaded,Mods1);
	true ->                              % Only loaded modules then.
	    Mods1
    end.


%% Help function which traverses all loaded modules and determines
%% which shall be returned. First we check that the module satisfies the
%% module-regexp. Then we, if a dir reg-exp is given, checks that the
%% module is loaded from an approved path. Note that if it can not be
%% determined from where it was loaded (like preloaded or cover-compiled
%% etc), but dir reg-exps are used. That module will be excluded.
%% Returns a list of modules.
handle_expand_regexp_2([{Mod,Path}|Rest],RegExpDir,RegExpMod,Result) ->
    ModStr=atom_to_list(Mod),
    ModLen=length(ModStr),
    case re:run(ModStr,RegExpMod) of
	{match,[{0,ModLen}]} ->                  % Ok, The regexp matches the module.
	    if
		is_list(RegExpDir),is_atom(Path) -> % Preloaded or covercompiled...
		    handle_expand_regexp_2(Rest,RegExpDir,RegExpMod,Result);
		is_list(RegExpDir),is_list(Path) -> % Dir reg-exp is used!
		    PathOnly=filename:dirname(Path), % Must remove beam-file name.
		    case re:run(PathOnly,RegExpDir,[{capture,none}]) of
			match ->             % Did find a match, that is enough!
			    handle_expand_regexp_2(Rest,RegExpDir,RegExpMod,[Mod|Result]);
			_ ->                 % Either error or nomatch.
			    handle_expand_regexp_2(Rest,RegExpDir,RegExpMod,Result)
		    end;
		true ->                      % Otherwise already done!
		    handle_expand_regexp_2(Rest,RegExpDir,RegExpMod,[Mod|Result])
	    end;
	_ ->                                 % Then Mod is not part of the set.
	    handle_expand_regexp_2(Rest,RegExpDir,RegExpMod,Result)
    end;
handle_expand_regexp_2([],_,_,Result) -> Result.

%% Help function which traverses all paths and looks for modules satisfying
%% the module reg.exp.
%% Returns a list of unique module names.
handle_expand_regexp_3([Path|Rest],RegExpDir,RegExpMod,AllLoaded,Result) ->
    if
	is_list(RegExpDir) ->                   % We must consider the directory name.
	    AbsPath=
		case filename:pathtype(Path) of
		    absolute ->              % Is already abs.
			Path;
		    relative ->              % Then it must be made absolute.
			filename:absname(Path);
		    volumerelative ->        % Only on Windows!?
			filename:absname(Path)
		end,
	    case re:run(AbsPath,RegExpDir,[{capture,none}]) of
		match ->                     % Ok, the directory is allowed.
		    NewResult=handle_expand_regexp_3_1(Path,RegExpMod,AllLoaded,Result),
		    handle_expand_regexp_3(Rest,RegExpDir,RegExpMod,AllLoaded,NewResult);
		_ ->                         % This directory does not qualify.
		    handle_expand_regexp_3(Rest,RegExpDir,RegExpMod,AllLoaded,Result)
	    end;
	true ->                              % RegExpDir is not used!
	    NewResult=handle_expand_regexp_3_1(Path,RegExpMod,AllLoaded,Result),
	    handle_expand_regexp_3(Rest,RegExpDir,RegExpMod,AllLoaded,NewResult)
    end;
handle_expand_regexp_3([],_,_,_,Result) -> Result.

handle_expand_regexp_3_1(Path,RegExpMod,AllLoaded,Result) ->
    case file:list_dir(Path) of
	{ok,FileNames} ->
	    handle_expand_regexp_3_2(FileNames,RegExpMod,AllLoaded,Result);
	{error,_Reason} ->                   % Bad path!? Skip it.
	    Result
    end.

handle_expand_regexp_3_2([File|Rest],RegExpMod,AllLoaded,Result) ->
    case filename:extension(File) of
	".beam" ->                           % It is a beam-file. Consider it!
	    ModStr=filename:basename(File,".beam"),
	    Mod=list_to_atom(ModStr),
	    case {lists:keysearch(Mod,1,AllLoaded),lists:member(Mod,Result)} of
		{false,false} ->             % This module is not tried before.
		    ModLen=length(ModStr),
		    case re:run(ModStr,RegExpMod) of
			{match,[{0,ModLen}]} ->  % This module satisfies the regexp.
			    handle_expand_regexp_3_2(Rest,RegExpMod,AllLoaded,[Mod|Result]);
			_ ->                 % Error or not perfect match.
			    handle_expand_regexp_3_2(Rest,RegExpMod,AllLoaded,Result)
		    end;
		{_,_} ->                     % This module is already tested.
		    handle_expand_regexp_3_2(Rest,RegExpMod,AllLoaded,Result)
	    end;
	_ ->                                 % Not a beam-file, skip it.
	    handle_expand_regexp_3_2(Rest,RegExpMod,AllLoaded,Result)
    end;
handle_expand_regexp_3_2([],_,_,Result) -> Result.
%% ------------------------------------------------------------------------------

%% Help function which finds out if its argument is a list of zero or more
%% atoms.
%% Returns 'true' or 'false'.
is_list_of_atoms([A|Rest]) when is_atom(A) ->
    is_list_of_atoms(Rest);
is_list_of_atoms([_|_]) ->
    false;
is_list_of_atoms([]) ->
    true.
%% ------------------------------------------------------------------------------


%% =============================================================================
%% Functions transforming function calls in trace-case file.
%% =============================================================================

%% transform(Exprs,Translations)=NewExprs
%%   Exprs=list(); List of abstract format erlang terms, as returned by
%%     io:parse_erl_exprs/2.
%%   Translations=list(); List of translations from function calls to other
%%     function calls. [{Mod,Func,Arity,{NewMod,NewFunc,ParamTransformMF}},...]
%%     Mod can actually be omitted, ParamTransformMF shall be {M,F} where F is
%%     a function taking one argument (the parameter list), and returning the
%%     new parameter list. It can also be anything else should no transformation
%%     of the parameters be the case.
%%
%% Function that transforms function calls in a trace-case file. The transform/2
%% can only transform shallow function calls. I.e where both module and function
%% name are specified as atoms. Any binding-environment is not examined.
transform([Expr|Rest],Translations) ->
    [transform_2(Expr,Translations)|transform(Rest,Translations)];
transform([],_) ->
    [].

%% Help function handling a single expr.
transform_2({call,L1,{remote,L2,ModExpr,FuncExpr},Params},Translations) ->
    case transform_2(ModExpr,Translations) of
	{atom,L3,M} ->
	    case transform_2(FuncExpr,Translations) of
		{atom,L4,F} ->               % Now we have a M:F/Arity!
		    case do_call_translation(M,F,Params,Translations) of
			{ok,NewM,NewF,NewP} ->
			    NewParams=transform(NewP,Translations),
			    {call,L1,{remote,L2,{atom,L3,NewM},{atom,L4,NewF}},NewParams};
			false ->             % No translation or faulty.
			    NewParams=transform(Params,Translations),
			    {call,L1,{remote,L2,ModExpr,FuncExpr},NewParams}
		    end;
		NewFuncExpr ->               % Not translated to a shallow term.
		    NewParams=transform(Params,Translations),
		    {call,L1,{remote,L2,ModExpr,NewFuncExpr},NewParams}
	    end;
	NewModExpr ->                        % Not translated to a shallow term.
	    NewFuncExpr=transform_2(FuncExpr,Translations),
	    NewParams=transform(Params,Translations),
	    {call,L1,{remote,L2,NewModExpr,NewFuncExpr},NewParams}
    end;
transform_2({call,L1,FuncExpr,Params},Translations) ->
    case transform_2(FuncExpr,Translations) of
	{atom,L3,F} ->                       % Now we have a M:F/Arity!
	    case do_call_translation(F,Params,Translations) of
		{ok,NewM,NewF,NewP} ->       % It is turned into a global call.
		    NewParams=transform(NewP,Translations),
		    {call,L1,{remote,L1,{atom,L3,NewM},{atom,L3,NewF}},NewParams};
		false ->                     % No translation or faulty.
		    NewParams=transform(Params,Translations),
		    {call,L1,FuncExpr,NewParams}
	    end;
	NewFuncExpr ->                       % Not translated to a shallow term.
	    NewParams=transform(Params,Translations),
	    {call,L1,NewFuncExpr,NewParams}
    end;
transform_2({match,L,P,E},Translations) ->
    NewPattern=transform_2(P,Translations),
    NewExpr=transform_2(E,Translations),
    {match,L,NewPattern,NewExpr};
transform_2({op,L,Op,Arg1,Arg2},Translations) ->
    NewArg1=transform_2(Arg1,Translations),
    NewArg2=transform_2(Arg2,Translations),
    {op,L,Op,NewArg1,NewArg2};
transform_2({op,L,Op,Arg},Translations) ->
    NewArg=transform_2(Arg,Translations),
    {op,L,Op,NewArg};
transform_2({block,L,Body},Translations) ->
    NewBody=transform(Body,Translations),
    {block,L,NewBody};
transform_2({'if',L,Clauses},Translations) ->
    NewClauses=transform_clauses(Clauses,Translations),
    {'if',L,NewClauses};
transform_2({'case',L,Func,Clauses},Translations) ->
    NewFunc=transform_2(Func,Translations),
    NewClauses=transform_clauses(Clauses,Translations),
    {'case',L,NewFunc,NewClauses};
transform_2({'fun',L,{clauses,Clauses}},Translations) ->
    NewClauses=transform_clauses(Clauses,Translations),
    {'fun',L,NewClauses};
transform_2({lc,L,Items,GeneratorsFilters},Translations) ->
    NewItem=transform_2(Items,Translations),
    NewGensAndFilters=transform_gensandfilters(GeneratorsFilters,Translations),
    {lc,L,NewItem,NewGensAndFilters};
transform_2({'catch',L,Expr},Translations) ->
    NewExpr=transform_2(Expr,Translations),
    {'catch',L,NewExpr};
transform_2({tuple,L,Elements},Translations) ->
    NewElements=transform(Elements,Translations),
    {tuple,L,NewElements};
transform_2({cons,L,Element,Tail},Translations) ->
    NewElement=transform_2(Element,Translations),
    NewTail=transform_2(Tail,Translations),
    {cons,L,NewElement,NewTail};
transform_2({nil,L},_) ->
    {nil,L};
transform_2({bin,L,Elements},Translations) ->
    NewElements=transform_binary(Elements,Translations),
    {bin,L,NewElements};
transform_2(Expr,_) ->                       % Can be a var for instance.
    Expr.

transform_binary([{bin_element,L,Val,Size,TSL}|Rest],Translations) ->
    NewVal=transform_2(Val,Translations),
    NewSize=transform_2(Size,Translations),
    [{bin_element,L,NewVal,NewSize,TSL}|transform_binary(Rest,Translations)];
transform_binary([],_) ->
    [].

transform_clauses([{clause,L,Pattern,Guards,Body}|Rest],Translations) ->
    NewPattern=transform(Pattern,Translations),
    NewBody=transform(Body,Translations),
    [{clause,L,NewPattern,Guards,NewBody}|transform_clauses(Rest,Translations)];
transform_clauses([],_Translations) ->
    [].

transform_gensandfilters([{generator,L,Pattern,Exprs}|Rest],Translations) ->
    NewExprs=transform(Exprs,Translations),
    [{generator,L,Pattern,NewExprs}|transform_gensandfilters(Rest,Translations)];
transform_gensandfilters([Expr|Rest],Translations) ->
    [transform_2(Expr,Translations)|transform_gensandfilters(Rest,Translations)];
transform_gensandfilters([],_) ->
    [].
%% ------------------------------------------------------------------------------

%% This is the heart of the translation functionality. Here we actually try to
%% replace calls to certain functions with other calls. This can include removing
%% arguments.
do_call_translation(M,F,Params,Translations) ->
    case lists:keysearch({M,F,length(Params)},1,Translations) of
	{value,{_,{NewM,NewF,ArgFun}}} ->    % Lets transform the function.
	    do_call_translation_2(Params,NewM,NewF,ArgFun);
	_ ->
	    false                            % No translations at all.
    end.
do_call_translation(F,Params,Translations) ->
    case lists:keysearch({F,length(Params)},1,Translations) of
	{value,{_,{NewM,NewF,ArgFun}}} ->    % Lets transform the function.
	    do_call_translation_2(Params,NewM,NewF,ArgFun);
	_ ->
	    false                            % No translations at all.
    end.

do_call_translation_2(Params,NewM,NewF,ArgFun) ->
    case ArgFun of
	{M,F} when is_atom(M),is_atom(F) ->
	    case catch M:F(Params) of
		{'EXIT',_Reason} ->
		    false;                   % If it does not work, skipp it.
		MungedParams when is_list(MungedParams) ->
		    {ok,NewM,NewF,MungedParams};
		_ ->
		    false
	    end;
	_ ->                                 % No munging of parameters.
	    {ok,NewM,NewF,Params}
    end.
%% ------------------------------------------------------------------------------


%% =============================================================================
%% Functions for the runtime component internal debugging system.
%% =============================================================================

%% The debug system is meant to provide tracing of ttb at different levels.
%%
%% debug(What,Level,Description) -> nothing significant.
%%   What : controls what kind of event. This can both be certain parts of ttb
%%          as well as certain levels (info to catastrophy).
%%   Level: Determines if What shall be printed or not.
%%   Description: this is what happend.
debug(off,_What,_Description) ->
    true;                                    % Debug is off, no action.
debug(On,What,Description) ->
    debug_2(On,What,Description).

debug_2(_,What,Description) ->
    io:format("INVISO DEBUG:~w, ~p~n",[What,Description]).
%% -----------------------------------------------------------------------------
