%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2011-2012. 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%
%%
%%
%%----------------------------------------------------------------------
%% Purpose: Module for debug trace functions of the inets application
%%----------------------------------------------------------------------

-module(inets_trace).

%% API
-export([enable/2, enable/3, 
	 disable/0, 
	 set_level/1, set_level/2, 
	 report_event/4]).


%%====================================================================
%% API
%%====================================================================

%%-----------------------------------------------------------------
%% enable(Level, Destination) -> void()
%% enable(Level, Destination, Service) -> void()
%%
%% Parameters:
%% Level -> max | min | integer()
%% Destination -> File | Port | io | HandlerSpec
%% Service -> httpc | httpd | ftpc | tftp | all
%% File -> string()
%% Port -> integer()
%% Verbosity -> true | false
%% HandlerSpec = {function(), Data}
%% Data = term()
%%
%% Description:
%% This function is used to start tracing at level Level and send
%% the result either to the file File, the port Port or to a 
%% trace handler. 
%% Note that it starts a tracer server.
%% When Destination is the atom io (or the tuple {io, Verbosity}),
%% all (printable) inets trace events (trace_ts events which has
%% Severity withing Limit) will be written to stdout using io:format.
%%
%%-----------------------------------------------------------------
enable(Level, Dest) ->
    enable(Level, Dest, all).

enable(Level, Dest, Service) ->
    case valid_trace_service(Service) of
	true ->
	    enable2(Level, Dest, Service);
	false ->
	    {error, {invalid_service, Service}}
    end.

enable2(Level, File, Service) 
  when is_list(File) ->
    case file:open(File, [write]) of
	{ok, Fd} ->
	    HandleSpec = {fun handle_trace/2, {Service, Fd}},
	    do_enable(Level, process, HandleSpec);
	Err ->
	    Err
    end;
enable2(Level, Port, _) when is_integer(Port) ->
    do_enable(Level, port, dbg:trace_port(ip, Port));
enable2(Level, io, Service) ->
    HandleSpec = {fun handle_trace/2, {Service, standard_io}},
    do_enable(Level, process, HandleSpec);
enable2(Level, {Fun, _Data} = HandleSpec, _) when is_function(Fun) ->
    do_enable(Level, process, HandleSpec).

do_enable(Level, Type, HandleSpec) ->
    case dbg:tracer(Type, HandleSpec) of
	{ok, _} ->
	    set_level(Level),
	    ok;
	Error ->
	    Error
    end.    

valid_trace_service(all) ->
    true;
valid_trace_service(Service) ->
    lists:member(Service, [httpc, httpd, ftpc, tftp]).


%%-----------------------------------------------------------------
%% disable() -> void()
%%
%% Description:
%% This function is used to stop tracing.
%%-----------------------------------------------------------------

disable() ->
    %% This is to make handle_trace/2 close the output file (if the
    %% event gets there before dbg closes)
    inets_trace:report_event(100, "stop trace", stop_trace, [stop_trace]),  
    dbg:stop().



%%-----------------------------------------------------------------
%% set_level(Level) -> void()
%%
%% Parameters:
%% Level -> max | min | integer()
%%
%% Description:
%% This function is used to change the trace level when tracing has
%% already been started.
%%-----------------------------------------------------------------
set_level(Level) ->
    set_level(Level, all).

set_level(Level, Service) ->
    Pat = make_pattern(?MODULE, Service, Level),
    change_pattern(Pat).

make_pattern(Mod, Service, Level) 
  when is_atom(Mod) andalso is_atom(Service) ->
    case Level of
        min ->
            {Mod, Service, []};
        max ->
            Head = ['$1', '_', '_', '_'],
            Body = [],
            Cond = [],
            {Mod, Service, [{Head, Cond, Body}]};
        DetailLevel when is_integer(DetailLevel) ->
            Head = ['$1', '_', '_', '_'],
            Body = [],
            Cond = [{ '=<', '$1', DetailLevel}],
            {Mod, Service, [{Head, Cond, Body}]};
        _ ->
            exit({bad_level, Level})
    end.

change_pattern({Mod, Service, Pattern}) 
  when is_atom(Mod) andalso is_atom(Service) ->
    MFA = {Mod, report_event, 4},
    case Pattern of
        [] ->
	    try
		error_to_exit(ctp, dbg:ctp(MFA)),
		error_to_exit(p,   dbg:p(all, clear))
	    catch
		exit:{Where, Reason} ->
		    {error, {Where, Reason}}
	    end;
        List when is_list(List) ->
	    try
		error_to_exit(ctp, dbg:ctp(MFA)),
		error_to_exit(tp,  dbg:tp(MFA, Pattern)),
		error_to_exit(p,   dbg:p(all, [call, timestamp]))
	    catch
		exit:{Where, Reason} ->
		    {error, {Where, Reason}}
	    end
    end.

error_to_exit(_Where, {ok, _} = OK) ->
    OK;
error_to_exit(Where, {error, Reason}) ->
    exit({Where, Reason}).


%%-----------------------------------------------------------------
%% report_event(Severity, Label, Service, Content)
%%
%% Parameters:
%% Severity -> 0 =< integer() =< 100
%% Label -> string()
%% Service -> httpd | httpc | ftp | tftp
%% Content -> [{tag, term()}]
%%
%% Description:
%% This function is used to generate trace events, that is,  
%% put trace on this function.
%%-----------------------------------------------------------------

report_event(Severity, Label, Service, Content) 
  when (is_integer(Severity) andalso 
	(Severity >= 0) andalso (100 >= Severity)) andalso 
       is_list(Label) andalso 
       is_atom(Service) andalso 
       is_list(Content) ->
    hopefully_traced.


%% ----------------------------------------------------------------------
%% handle_trace(Event, Fd) -> Verbosity
%%
%% Parameters:
%% Event -> The trace event
%% Fd -> standard_io | file_descriptor() | trace_port()
%%
%% Description:
%% This function is used to "receive" and pretty print the trace events.
%% Events are printed if:
%%   - Verbosity is max
%%   - Severity is =< Verbosity (e.g. Severity = 30, and Verbosity = 40)
%% Events are not printed if:
%%   - Verbosity is min
%%   - Severity is > Verbosity
%%-----------------------------------------------------------------

handle_trace(_, closed_file = Fd) ->
    Fd;
handle_trace({trace_ts, _Who, call,
              {?MODULE, report_event,
               [_Sev, "stop trace", stop_trace, [stop_trace]]},
              Timestamp},
             {_, standard_io} = Fd) ->
    (catch io:format(standard_io, "stop trace at ~s~n", [format_timestamp(Timestamp)])),
    Fd;
handle_trace({trace_ts, _Who, call,
              {?MODULE, report_event,
               [_Sev, "stop trace", stop_trace, [stop_trace]]},
              Timestamp},
             standard_io = Fd) ->
    (catch io:format(Fd, "stop trace at ~s~n", [format_timestamp(Timestamp)])),
    Fd;
handle_trace({trace_ts, _Who, call,
              {?MODULE, report_event,
               [_Sev, "stop trace", stop_trace, [stop_trace]]},
              Timestamp},
             {_Service, Fd}) ->
    (catch io:format(Fd, "stop trace at ~s~n", [format_timestamp(Timestamp)])),
    (catch file:close(Fd)),
    closed_file;
handle_trace({trace_ts, _Who, call,
              {?MODULE, report_event,
               [_Sev, "stop trace", stop_trace, [stop_trace]]},
              Timestamp},
             Fd) ->
    (catch io:format(Fd, "stop trace at ~s~n", [format_timestamp(Timestamp)])),
    (catch file:close(Fd)),
    closed_file;
handle_trace({trace_ts, Who, call,
              {?MODULE, report_event,
               [Sev, Label, Service, Content]}, Timestamp},
             Fd) ->
    (catch print_inets_trace(Fd, Sev, Timestamp, Who, 
			     Label, Service, Content)),
    Fd;
handle_trace(Event, Fd) ->
    (catch print_trace(Fd, Event)),
    Fd.


print_inets_trace({Service, Fd}, 
		  Sev, Timestamp, Who, Label, Service, Content) ->
    do_print_inets_trace(Fd, Sev, Timestamp, Who, Label, Service, Content);
print_inets_trace({ServiceA, Fd}, 
		  Sev, Timestamp, Who, Label, ServiceB, Content) 
  when (ServiceA =:= all) ->
    do_print_inets_trace(Fd, Sev, Timestamp, Who, Label, ServiceB, Content);
print_inets_trace({ServiceA, _Fd}, 
		  _Sev, _Timestamp, _Who, _Label, ServiceB, _Content) 
  when ServiceA =/= ServiceB ->
    ok;
print_inets_trace(Fd, Sev, Timestamp, Who, Label, Service, Content) ->
    do_print_inets_trace(Fd, Sev, Timestamp, Who, Label, Service, Content).

do_print_inets_trace(Fd, Sev, Timestamp, Who, Label, Service, Content) ->
    Ts = format_timestamp(Timestamp),
    io:format(Fd, "[inets ~w trace ~w ~w ~s] ~s "
              "~n   Content: ~p"
              "~n",
              [Service, Sev, Who, Ts, Label, Content]).

print_trace({_, Fd}, Event) ->
    do_print_trace(Fd, Event);
print_trace(Fd, Event) ->
    do_print_trace(Fd, Event).

do_print_trace(Fd, {trace, Who, What, Where}) ->
    io:format(Fd, "[trace]"
              "~n   Who:   ~p"
              "~n   What:  ~p"
              "~n   Where: ~p"
              "~n", [Who, What, Where]);

do_print_trace(Fd, {trace, Who, What, Where, Extra}) ->
    io:format(Fd, "[trace]"
              "~n   Who:   ~p"
              "~n   What:  ~p"
              "~n   Where: ~p"
              "~n   Extra: ~p"
              "~n", [Who, What, Where, Extra]);

do_print_trace(Fd, {trace_ts, Who, What, Where, When}) ->
    Ts = format_timestamp(When),
    io:format(Fd, "[trace ~s]"
              "~n   Who:   ~p"
              "~n   What:  ~p"
              "~n   Where: ~p"
              "~n", [Ts, Who, What, Where]);

do_print_trace(Fd, {trace_ts, Who, What, Where, Extra, When}) ->
    Ts = format_timestamp(When),
    io:format(Fd, "[trace ~s]"
              "~n   Who:   ~p"
              "~n   What:  ~p"
              "~n   Where: ~p"
              "~n   Extra: ~p"
              "~n", [Ts, Who, What, Where, Extra]);

do_print_trace(Fd, {seq_trace, What, Where}) ->
    io:format(Fd, "[seq trace]"
              "~n   What:       ~p"
              "~n   Where:      ~p"
              "~n", [What, Where]);

do_print_trace(Fd, {seq_trace, What, Where, When}) ->
    Ts = format_timestamp(When),
    io:format(Fd, "[seq trace ~s]"
              "~n   What:       ~p"
              "~n   Where:      ~p"
              "~n", [Ts, What, Where]);

do_print_trace(Fd, {drop, Num}) ->
    io:format(Fd, "[drop trace] ~p~n", [Num]);

do_print_trace(Fd, Trace) ->
    io:format(Fd, "[trace] "
              "~n   ~p"
              "~n", [Trace]).


format_timestamp({_N1, _N2, N3} = Now) ->
    {Date, Time}   = calendar:now_to_datetime(Now),
    {YYYY,MM,DD}   = Date,
    {Hour,Min,Sec} = Time,
    FormatDate =
        io_lib:format("~.4w:~.2.0w:~.2.0w ~.2.0w:~.2.0w:~.2.0w 4~w",
                      [YYYY,MM,DD,Hour,Min,Sec,round(N3/1000)]),
    lists:flatten(FormatDate).


