%%%----------------------------------------------------------------------------
%%% File    : seqvar.erl
%%% Author  : Anders Dahlin
%%% Purpose : Check to see if variables is handled in sequence
%%% Created : 28 Mar 2008 by Anders Dahlin
%%%----------------------------------------------------------------------------

-module(seqvar).
-author('Anders Dahlin').
-vsn("1.1").

-export([check/0, check/1, check/2]).

-include_lib("kernel/include/file.hrl").

-record(opts, {incl     = nil, 
	       excl     = nil, 
	       incldir  = nil,
	       excldir  = nil,
	       inclvar  = nil,
	       exclvar  = nil,
	       recurse  = false,
	       verbose  = false
	      }).

-define(default_dirs, []).


%%------------------------------------------------------------------------------
%% Func    : check/0, 1, 2
%%
%% Purpose : Main function. Initiates what should be checked
%%
%% Args    : Arg         : DoFs or Options, see below
%%           DoFs        : Path()
%%                         Path to current file (might be header file)
%%           Options     : List()
%%                         Option list, will be handled by fix_opts
%%
%% Returns : void()
%%------------------------------------------------------------------------------

check() ->
    check([]).

check(Arg) ->
    case Arg of
	[X| _Rest] when atom(X); tuple(X) ->	% Arg = options
	    {ok, Cwd} = file:get_cwd(),		
	    check(Cwd, Arg);		
	_Other ->
	    check(Arg, [])			% Arg = dir|file
    end.

check(DoFs, Options)->
    Opts       = fix_opts(Options),
    Files      = get_files(DoFs, Opts),
    OrgPath    = code:get_path(),
    OtpSrcPath = ebin_to_src(OrgPath),
    Includes   = ?default_dirs ++ OtpSrcPath,
    check_files(Files, Opts, Includes).


%%------------------------------------------------------------------------------
%% Func    : check_files/3
%%
%% Purpose : Main function. Initiates what should be checked
%%
%% Args    : Files       : [Path()]
%%                         List of files to be checked
%%           Opts        : #opts{}
%%           Includes    : Files to include in the check
%%
%% Returns : void()
%%------------------------------------------------------------------------------

check_files([File| Rest], Opts, Includes) ->
    io:format("Checking file ~p\n", [File]),
    case epp:open(File, [filename:dirname(File)| Includes], []) of
	{ok, Epp} ->
	    parse_file(Epp, File, Opts),
	    epp:close(Epp);
	Error ->
	    io:format("Unable to open ~s due to ~p", [File, Error])
    end,
    io:nl(),
    check_files(Rest, Opts, Includes);
check_files([], _Opts, _Includes) ->
    done.
    

%%------------------------------------------------------------------------------
%% Func    : parse_file/2
%%
%% Purpose : Parse the module abstract form by abstract form
%%
%% Args    : Epp         : File descriptor of a file opened for parsing
%%           CF          : Path()
%%                         Path to current file (might be header file)
%%           Opts        : #opts{}
%%
%% Returns : void()
%%------------------------------------------------------------------------------

parse_file(Epp, CF, Opts) ->
    case epp:parse_erl_form(Epp) of
	{ok, AbsForm} ->
	    NewCF = parse_abs_form(AbsForm, CF, Opts),
	    parse_file(Epp, NewCF, Opts);
	{eof, _} ->
	    ok;
	{error, Error} ->
	    io:format("Unable to parse ~s due to ~p", [CF, Error]),
	    parse_file(Epp, CF, Opts)	% correct?
    end.


%%------------------------------------------------------------------------------
%% Func    : parse_abs_form/2
%%
%% Purpose : Parse an absolute erlang form
%%
%% Args    : AbsForm     : Absolute form returned by epp:parse_erl_form
%%           CF          : see parse_file/2
%%           Opts        : see parse_file/2
%%
%% Returns : Path()
%%------------------------------------------------------------------------------

parse_abs_form({attribute, _L, Attribute, Value}, CF, _Opts) ->
    attribute(Attribute, Value, CF);

parse_abs_form({function, _L, F, A, Clauses}, CF, Opts) ->
    func(F, A, Clauses, Opts),
    CF;

parse_abs_form(_AbsForm, CF, _Opts) ->	% ignoring others
    CF.


%%------------------------------------------------------------------------------
%% Func    : attribute/3
%%
%% Purpose : Parse an attribute. Only file attributes are handled. 
%%           All others are disregarded.
%% Args    : Attribute   : Atom()
%%                         file | export | import | compile | Atom()
%%           Value       : Term()
%%           CF          : see parse_file/2
%%
%% Returns : Path()
%%------------------------------------------------------------------------------

%% Handle the file attribute
attribute(file, {NewCF, _L}, _CF) ->
    filename:absname(NewCF);

%% Disregard all other attributes
attribute(_Attribute, _Value, CF) ->
    CF.


%%------------------------------------------------------------------------------
%% Func    : func/4
%%
%% Purpose : Parse a function
%%
%% Args    : F           : Atom() - function
%%           A           : Int() - arity
%%           Clauses     : List()
%%           Opts        : see parse_file/2
%%
%% Returns : void
%%------------------------------------------------------------------------------

func(_F, _A, Clauses, Opts) ->
    clauses(Clauses, Opts).


%%------------------------------------------------------------------------------
%% Func    : clauses/2
%%
%% Purpose : Parse a list of function clauses.
%%
%% Args    : Clauses     : List()
%%                         [{clause, Line, Parameters, Guards, Body},...]
%%           Opts        : see parse_file/2
%%
%% Returns : void()
%%------------------------------------------------------------------------------

%% function clauses
clauses([{clause, _L, _P, _G, B} | Clauses], Opts) ->
    %% Not needed to check the head or the guards?
    %% Vars = sequence(P, [], Opts),	% check parameters (head)
    sequence(B, [], Opts),		% check body
    clauses(Clauses, Opts);
	
clauses([], _Opts) ->
    void.

%%------------------------------------------------------------------------------
%% Func    : sequence/3
%%
%% Purpose : Parse a sequence of expressions
%%
%% Args    : Sequence    : List()
%%                         [Expression, ...]
%%           Vars        : List() - [{Var1, Seq1}, ..., {VarN, SeqN}] 
%%           Opts        : see parse_file/2
%%
%% Returns : Vars
%%------------------------------------------------------------------------------

sequence([Expressions | Rest], Vars, Opts) when list(Expressions) ->
    NewVars = sequence(Expressions, Vars, Opts),
    sequence(Rest, NewVars, Opts);

sequence([Expression | Rest], Vars, Opts) ->
    NewVars = expression(Expression, Vars, Opts),
    sequence(Rest, NewVars, Opts);

sequence([], Vars, _Opts) ->
    Vars.


%%------------------------------------------------------------------------------
%% Func    : fric_clauses/3
%%
%% Purpose : Parse a list of fun/receive/if/case/try clauses. 
%%
%% Args    : Clauses     : List()
%%                         [{clause, Line, Parameters, Guards, Body},...]
%%           Vars        : see sequence/3
%%           Opts        : see parse_file/2
%%
%% Returns : Vars
%%------------------------------------------------------------------------------

fric_clauses(Clauses, Vars, Opts) ->
    fric_clauses(Clauses, Vars, Opts, []).
					  
fric_clauses([{clause, _L, P, G, B} | Clauses], Vars, Opts, AllVars) ->
    NewVars1 = sequence(P, Vars, Opts),		% check parameters
    guards(G, NewVars1, Opts),			% check guards
    NewVars2 = sequence(B, NewVars1, Opts),	% check body
    fric_clauses(Clauses, Vars, Opts, [NewVars2| AllVars]);
fric_clauses([], _Vars, _Opts, AllVars) ->
    comm(AllVars).	% get vars common to all clauses


%%------------------------------------------------------------------------------
%% Func    : guards/3
%%
%% Purpose : Parse a list of guard expressions
%%
%% Args    : Guards      : List()
%%                         [Expression, ...]
%%           Vars        : see sequence/3
%%           Opts        : see parse_file/2
%%
%% Returns : Vars
%%------------------------------------------------------------------------------

guards([Expressions | Rest], Vars, Opts) when list(Expressions) ->
    guards(Expressions, Vars, Opts),
    guards(Rest, Vars, Opts);

guards([Expression | Rest], Vars, Opts) ->
    expression(Expression, Vars, Opts),
    guards(Rest, Vars, Opts);

guards([], Vars, _Opts) ->
    Vars.


%%------------------------------------------------------------------------------
%% Func    : expression/3
%%
%% Purpose : Parse an expression
%%
%% Args    : Expression  : Tuple()
%%           Vars        : see sequence/3
%%           Opts        : see parse_file/2
%%
%% Returns : Vars
%%------------------------------------------------------------------------------

expression({call, _Line, Call, Parameters}, Vars, Opts) ->
    NewVars = expression(Call, Vars, Opts),
    sequence(Parameters, NewVars, Opts);
    
expression({cons, _Line, Head, Tail}, Vars, Opts) ->
    NewVars = expression(Head, Vars, Opts),
    expression(Tail, NewVars, Opts);
    
expression({tuple, _Line, Expressions}, Vars, Opts) ->
    sequence(Expressions, Vars, Opts);
    
expression({'if', _Line, Clauses}, Vars, Opts) ->
    fric_clauses(Clauses, Vars, Opts);
    
expression({'case', _Line, Expression, Clauses}, Vars, Opts) ->
    NewVars = expression(Expression, Vars, Opts),
    fric_clauses(Clauses, NewVars, Opts);

expression({'receive', _Line, Clauses}, Vars, Opts) ->
    fric_clauses(Clauses, Vars, Opts);

expression({'receive', _Line, Clauses, T1, T2}, Vars, Opts) ->
    Vars1 = expression(T1, Vars, Opts),
    Vars2 = sequence(T2, Vars1, Opts),
    fric_clauses(Clauses, Vars2, Opts);

expression({arith, _, _Operator, Left, Right}, Vars, Opts) ->
    Vars1 = expression(Left, Vars, Opts),
    expression(Right, Vars1, Opts);
expression({arith, _, _Operator, Expression}, Vars, Opts) ->
    expression(Expression, Vars, Opts);

expression({match, _, Left, Right}, Vars, Opts) ->
    Vars1 = expression(Right, Vars, Opts),
    expression(Left, Vars1, Opts);	% will this work?

expression({block, _, Expression}, Vars, Opts) ->
    expression(Expression, Vars, Opts);

expression({'catch', _, Expression}, Vars, Opts) ->
    expression(Expression, Vars, Opts);

expression({send, _, Receiver, Message}, Vars, Opts) ->
    Vars1 = expression(Receiver, Vars, Opts),
    expression(Message, Vars1, Opts);

%% This is where the actual checking starts!
expression({var, L, Var}, Vars, Opts) ->
    parameter(Var, L, Vars, Opts);

expression({record, _, _, Record}, Vars, Opts) when list(Record) ->
    sequence(Record, Vars, Opts);
expression({record, _, Expression, _, Record}, Vars, Opts) when list(Record) ->
    Vars1 = expression(Expression, Vars, Opts),
    sequence(Record, Vars1, Opts);

expression({record_field, _L, _, Expression}, Vars, Opts) -> 
    expression(Expression, Vars, Opts);

expression({record_field, _L, Var, _F, Expression}, Vars, Opts) ->
    Vars1 = expression(Var, Vars, Opts),
    expression(Expression, Vars1, Opts);

expression({record_index, _L, _, _}, Vars, _Opts) ->
    Vars;

expression({op, _L, _Op, Expr1, Expr2}, Vars, Opts) ->
    Vars1 = expression(Expr1, Vars, Opts),
    expression(Expr2, Vars1, Opts);

expression({op, _L, _Op, Expr}, Vars, Opts) ->
    expression(Expr, Vars, Opts);

expression({lc, _, Expr1, Expr2}, Vars, Opts) ->
    Vars1 = expression(Expr1, Vars, Opts),
    sequence(Expr2, Vars1, Opts);
   
expression({generate, _, Expr1, Expr2}, Vars, Opts) ->
    Vars1 = expression(Expr1, Vars, Opts),
    expression(Expr2, Vars1, Opts);
   
expression({remote, _, Expr1, Expr2}, Vars, Opts) ->
    Vars1 = expression(Expr1, Vars, Opts),
    expression(Expr2, Vars1, Opts);

expression({'fun', _, {clauses, Clauses}}, Vars, Opts) ->
    fric_clauses(Clauses, Vars, Opts);

expression({'fun', _L, _}, Vars, _Opts) ->
    Vars;

expression({'query', _, Expr}, Vars, Opts) ->
    expression(Expr, Vars, Opts);

expression({atom, _L, _Atom}, Vars, _Opts) ->
    Vars;

expression({integer, _L, _Int}, Vars, _Opts) ->
    Vars;

expression({float, _L, _Float}, Vars, _Opts) ->
    Vars;

expression({string, _L, _String}, Vars, _Opts) ->
    Vars;

expression({char, _L, _Char}, Vars, _Opts) ->
    Vars;

expression({bin, _L, _Bin}, Vars, _Opts) ->
    Vars;

expression({nil, _L}, Vars, _Opts) ->
    Vars;

expression({'try', _L, Call, Clauses, Exceptions, After}, Vars, Opts) ->
    Vars1 = sequence(Call, Vars, Opts),
    Vars2 = fric_clauses(Clauses, Vars1, Opts),
    Vars3 = fric_clauses(Exceptions, Vars1, Opts),
    Vars4 = comm([Vars2, Vars3]),
    sequence(After, Vars4, Opts),
    Vars4;

expression(BeginEnd, Vars, Opts) when is_list(BeginEnd) ->
    sequence(BeginEnd, Vars, Opts);

expression(Expr, Vars, Opts) ->
    case Opts#opts.verbose of
	true when is_tuple(Expr) -> 
	    io:format("Line ~w: unhandled expression ~p~n", [element(2, Expr), Expr]);
	true -> 
	    io:format("unhandled expression ~p~n", [Expr]);
	_ -> 
	    ok
    end,
    Vars.


%%------------------------------------------------------------------------------
%% Func    : parameter/4
%%
%% Purpose : Check a variable and report if it's used out of sequence
%%
%% Args    : Var         : Atom()
%%                         The variable
%%           L           : Integer()
%%                         Line number
%%           Vars        : see sequence/3
%%           Opts        : see parse_file/2
%%
%% Returns : Vars
%%------------------------------------------------------------------------------
parameter(Var, L, Vars, Opts) ->
    case include(Var, {Opts#opts.inclvar, Opts#opts.exclvar}) of
	true ->
	    {V, NewS} = parse_var(Var),
	    case lists:keysearch(V, 1, Vars) of
		{value, {_, NewS}} ->
		    Vars;
		{value, {_, -1}} ->
		    lists:keyreplace(V, 1, Vars, {V, NewS});
		{value, {_, OldS}} when NewS > OldS ->
		    lists:keyreplace(V, 1, Vars, {V, NewS});
		{value, {_, OldS}} ->	% using old var
		    io:format("Line ~p: Usage of ~p, expected ~p~n", 
			      [L, frmt_var({V, NewS}), frmt_var({V, OldS})]),
		    Vars;
		false ->
		    [{V, NewS}| Vars]
	    end;
	false ->
	    Vars
    end.

parse_var(VarIn) ->
    parse_var(lists:reverse(atom_to_list(VarIn)), []).

parse_var([H| T], Seq) when H >= 48, H =< 57 ->
    parse_var(T, [H| Seq]);
parse_var(RevVar, []) ->
    {lists:reverse(RevVar), -1};
parse_var([], Seq) ->
    {Seq, -1};
parse_var(RevVar, Seq) ->
    {lists:reverse(RevVar), list_to_integer(Seq)}.
			      

frmt_var({Var, -1}) ->
    list_to_atom(Var);
frmt_var({Var, Seq}) ->
    list_to_atom(lists:concat([Var, Seq])).


%%------------------------------------------------------------------------------
%% Utility functions
%%------------------------------------------------------------------------------

%% Convert paths which ends in ebin to src
ebin_to_src(PathList) ->
     F = fun(P) -> 
		 case filename:basename(P) of
		     "ebin" ->
			 filename:join(filename:dirname(P), "src");
		     _ ->
			 P
		 end
	 end,
    lists:map(F, PathList).


%%% get_files(DoFs, Opts) where DoFs is a list of directories and files 
%%% or a directory or file. get_files will list all files in the specified 
%%% directories and also traverse sub directories if the recurse option is set. 
%%% Include only .erl files for which Incl(File) returns true 
%%% and Excl(File) returns false. If the recurse option is set
%%% get_files will traverse all sub directories.

get_files([DoF| Rest], Opts) when list(DoF); atom(DoF) ->
    get_files(DoF, Opts) ++ get_files(Rest, Opts);
get_files(InDoF, Opts) ->
    DoF = coerce2list(InDoF),
    #opts{incl    = Incl, 
	  excl    = Excl, 
	  incldir = InclDir, 
	  excldir = ExclDir,
	  recurse = Recurse} = Opts,
    FileFuns = {Incl, Excl}, 
    DirFuns  = {InclDir, ExclDir},
    case type(DoF) of
	file ->
	    case include(DoF, FileFuns) of
		true ->
		    [DoF];
		false ->
		    []
	    end;
	dir ->
	    case include(DoF, DirFuns) of
		true ->
		    list_dir(DoF, {FileFuns, DirFuns}, Recurse, []);
		false ->
		    []
	    end;
	_Other ->
	    []
    end.


list_dir(Dir, IEFuns, Recurse, AllFiles) ->
    {Dirs, Files} = list_dir(Dir, IEFuns),
    case Recurse of
	true ->
	    list_dirs(Dirs, IEFuns, Recurse, AllFiles ++ Files);
	false ->
	    AllFiles ++ Files
    end.

list_dirs([Dir | Dirs], IEFuns, Recurse, AllFiles) ->
    NewAllFiles = list_dir(Dir, IEFuns, Recurse, AllFiles),
    list_dirs(Dirs, IEFuns, Recurse, NewAllFiles);
list_dirs([], _IEFuns, _Recurse, AllFiles) ->
    AllFiles.


list_dir(Dir, IEFuns) ->
    {ok, Content} = file:list_dir(Dir),
    extract(Content, Dir, IEFuns, [], []).


extract([DoF | Content], Dir, Funs, Files, Dirs) ->
    case include(DoF, Dir, Funs) of
	{dir, PathDoF} ->
	    extract(Content, Dir, Funs, Files, [PathDoF| Dirs]);
	{file, PathDoF} ->
	    extract(Content, Dir, Funs, [PathDoF| Files], Dirs);
	false ->
	    extract(Content, Dir, Funs, Files, Dirs)
    end;
extract([], _Dir, _Funs, Files, Dirs) ->
    {lists:reverse(Dirs), lists:reverse(Files)}.

include(DoF, Dir, {FileFuns, DirFuns}) ->
    PathDoF = filename:join(Dir, DoF),
    case type(PathDoF) of
	file -> 
	    case filename:extension(DoF) of
		".erl" ->
		    case include(PathDoF, FileFuns) of
			true ->
			    {file, PathDoF};
			false ->
			    false
		    end;
		_SomethingElse ->
		    false
	    end;
	dir ->
	    case include(PathDoF, DirFuns) of
		true ->
		    {dir, PathDoF};
		false ->
		    false
	    end;
	_Other -> 
	    false
    end.


type(#file_info{type=directory}) -> dir;
type(#file_info{type=regular})   -> file;
type(#file_info{type=Type})      -> Type;
type(DoF) when list(DoF); atom(DoF) ->
    case file:read_file_info(DoF) of
	{ok, FileInfo} ->
	    type(FileInfo);
	Error ->
	    Error
    end;
type(Error) -> {error, Error}.

coerce2list(X) when atom(X) ->
    atom_to_list(X);
coerce2list(X) ->
    X.


%%% fix_opts will scan a list of options and set the appropriate
%%% fields in the opts record which will be used later in the
%%% code. Options appearing before others in the list has precedence

fix_opts(Options) when record(Options, opts) ->
    Options;
fix_opts(Options) ->
    fix_opts(lists:reverse(Options), #opts{}).


fix_opts([Option| Options], Opts) ->
    NewOpts = 
	case Option of
	    rec            -> Opts#opts{recurse = true};
	    recurse        -> Opts#opts{recurse = true};
	    recursive      -> Opts#opts{recurse = true};
	    {incl,    Fun} -> Opts#opts{incl    = Fun};
	    {excl,    Fun} -> Opts#opts{excl    = Fun};
	    {incldir, Fun} -> Opts#opts{incldir = Fun};
	    {excldir, Fun} -> Opts#opts{excldir = Fun};
	    {inclvar, Fun} -> Opts#opts{inclvar = Fun};
	    {exclvar, Fun} -> Opts#opts{exclvar = Fun};
	    verbose        -> Opts#opts{verbose = true};
	    Option         -> Opts
	end,
    
    fix_opts(Options, NewOpts);
fix_opts([], Opts) ->
    Opts.


%% default excludes
%% exclude(AbsDir) ->
%%     Dir = filename:basename(AbsDir),
%%     case Dir of
%% 	"lost+found" ->
%% 	    true;
%% 	Dir ->
%% 	    case string:to_upper(lists:reverse(Dir)) of
%% 		"KAB." ++ _ ->	% ends with .BAK
%% 		    true;
%% 		"DLO." ++ _ ->	% ends with .OLD
%% 		    true;
%% 		_Other ->
%% 		    false
%% 	    end
%%     end.

%% include or exclude something
include(_Cand, {nil, nil}) ->
    true;
include(Cand, {Incl, nil}) ->
    Incl(Cand);
include(Cand, {nil, Excl}) ->
    not(Excl(Cand));
include(Cand, {Incl, Excl}) ->
    Incl(Cand) and not(Excl(Cand)).


%% comm returns the intersection of a list of lists
comm([L1, L2| Lists]) ->
    Comm = common(lists:sort(L1), lists:sort(L2)),
    comm([lists:sort(L) || L <- Lists], Comm);
comm(Lists) ->
    Lists.

comm([L1| Lists], Comm) ->
    comm(Lists, common(L1, Comm));
comm([], Comm) ->
    Comm.

%% assumes sorted lists
common(L1, L2) ->
    common(L1, L2, []).
common([E| L1], [E| L2], Res) ->
    common(L1, L2, [E| Res]);
common([E1| L1], [E2| L2], Res) when E1 < E2 ->
    common(L1, [E2| L2], Res);
common([E1| L1], [_E2| L2], Res) ->
    common([E1| L1] , L2, Res);
common(_, _, Res) ->
    lists:reverse(Res).
