%% ``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 via the world wide web 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.
%% 
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%% 
%%     $Id$
%%
-module(snmp_log).


-export([
	 create/4, create/5, 
	 change_size/2, close/1, sync/1, info/1, 
	 log/4, 
	 log_to_txt/5, log_to_txt/6, log_to_txt/7,
	 log_to_io/4,  log_to_io/5,  log_to_io/6
	]).


-define(SNMP_USE_V3, true).
-include("snmp_types.hrl").

-define(VMODULE,"LOG").
-include("snmp_verbosity.hrl").

-define(LOG_FORMAT, internal).
-define(LOG_TYPE,   wrap).


%% --------------------------------------------------------------------
%% Exported functions
%% --------------------------------------------------------------------


%% -- create ---

create(Name, File, Size, Repair) ->
    create(Name, File, Size, Repair, false).

create(Name, File, Size, Repair, Notify) ->
    ?vtrace("create -> entry with"
	    "~n   Name:   ~p"
	    "~n   File:   ~p"
	    "~n   Size:   ~p"
	    "~n   Repair: ~p"
	    "~n   Notify: ~p", [Name, File, Size, Repair, Notify]),
    log_open(Name, File, Size, Repair, Notify).
    

%% -- close ---

close(Log) ->
    ?vtrace("close -> entry with"
	    "~n   Log: ~p", [Log]),
    disk_log:close(Log).


%% -- close ---

sync(Log) ->
    ?vtrace("sync -> entry with"
	    "~n   Log: ~p", [Log]),
    disk_log:sync(Log).

%% -- info ---

info(Log) ->
    case disk_log:info(Log) of
	Info when list(Info) ->
	    Items = [no_current_bytes, no_current_items, 
		     current_file, no_overflows],
	    info_filter(Items, Info, []);
	Else ->
	    Else
    end.
    
info_filter([], _Info, Acc) ->
    {ok, Acc};
info_filter([Item|Items], Info, Acc) ->
    case lists:keysearch(Item, 1, Info) of
	{value, New} ->
	    info_filter(Items, Info, [New|Acc]);
	false ->
	    info_filter(Items, Info, Acc)
    end.


%% -- log ---

%%-----------------------------------------------------------------
%% For efficiency reasons, we want to log the packet as a binary.
%% This is only possible for messages that are not encrypted.
%% Therefore, Packet can be either a binary (encoded message), or
%% a tuple {V3Hdr, ScopedPduBytes}
%% 
%% log(Log, Packet, Addr, Port)
%%-----------------------------------------------------------------


log(Log, Packet, Addr, Port) ->
    ?vtrace("log -> entry with"
	    "~n   Log:  ~p"
	    "~n   Addr: ~p"
	    "~n   Port: ~p", [Log, Addr, Port]),
    Entry = {timestamp(), Packet, Addr, Port},
    disk_log:alog(Log, Entry).



%% -- change_size ---

change_size(Log, NewSize) ->
    ?vtrace("change_size -> entry with"
	    "~n   Log:     ~p"
	    "~n   NewSize: ~p", [Log, NewSize]),
    disk_log:change_size(Log, NewSize).


%% -- log_to_txt ---

log_to_txt(Log, FileName, Dir, Mibs, TextFile) ->
    log_to_txt(Log, FileName, Dir, Mibs, TextFile, null, null).

log_to_txt(Log, FileName, Dir, Mibs, TextFile, Start) ->
    log_to_txt(Log, FileName, Dir, Mibs, TextFile, Start, null).

log_to_txt(Log, FileName, Dir, Mibs, TextFile, Start, Stop) 
  when list(Mibs), list(TextFile) ->
    ?vtrace("log_to_txt -> entry with"
	    "~n   Log:      ~p"
	    "~n   FileName: ~p"
	    "~n   Dir:      ~p"
	    "~n   Mibs:     ~p"
	    "~n   TextFile: ~p"
	    "~n   Start:    ~p"
	    "~n   Stop:     ~p", 
	    [Log, FileName, Dir, Mibs, TextFile, Start, Stop]),
    File = filename:join(Dir, FileName),
    Converter = fun(L) ->
			do_log_to_file(L, TextFile, Mibs, Start, Stop)
		end,
    log_convert(Log, File, Converter).


%% -- log_to_io ---

log_to_io(Log, FileName, Dir, Mibs) ->
    log_to_io(Log, FileName, Dir, Mibs, null, null).

log_to_io(Log, FileName, Dir, Mibs, Start) ->
    log_to_io(Log, FileName, Dir, Mibs, Start, null).

log_to_io(Log, FileName, Dir, Mibs, Start, Stop) 
  when list(Mibs) ->
    File = filename:join(Dir, FileName),
    Converter = fun(L) ->
			do_log_to_io(L, Mibs, Start, Stop)
		end,
    log_convert(Log, File, Converter).


%% --------------------------------------------------------------------
%% Internal functions
%% --------------------------------------------------------------------


%% -- log_convert ---

log_convert(Log, File, Converter) ->
    %% First check if the caller process has already opened the
    %% log, because if we close an already open log we will cause
    %% a runtime error.
    case is_owner(Log) of
	true ->
	    Converter(Log);
	false ->
	    %% Not yet member of the ruling party, apply for membership...
	    %% If a log is opened as read_write it is not possible to 
	    %% open it as read_only. So, to get around this we open 
	    %% it under a different name...
	    Log2 = convert_name(Log),
	    case log_open(Log2, File) of
		{ok, _} ->
		    Res = Converter(Log2),
		    disk_log:close(Log2),
		    Res;
		{error, {name_already_open, _}} ->
                    Converter(Log2);
                {error, Reason} ->
                    {error, {Log, Reason}}
	    end
    end.

convert_name(Name) when list(Name) ->
    Name ++ "_tmp";
convert_name(Name) when atom(Name) ->
    list_to_atom(atom_to_list(Name) ++ "_tmp");
convert_name(Name) ->
    lists:flatten(io_lib:format("~w_tmp", [Name])).


%% -- do_log_to_text ---

do_log_to_file(Log, TextFile, Mibs, Start, Stop) ->
    case file:open(TextFile, write) of
        {ok, Fd} ->
            MiniMib = snmp_mini_mib:create(Mibs),
	    Write = fun(X) -> 
			    case format_msg(X, MiniMib, Start, Stop) of
				{ok, S} ->
				    io:format(Fd, "~s", [S]);
				_ ->
				    ok
			    end
		    end,
            Res = (catch loop(disk_log:chunk(Log, start), Log, Write)),
	    snmp_mini_mib:delete(MiniMib), 
            file:close(Fd),
            Res;
        {error, Reason} ->
            {error, {TextFile, Reason}}
    end.


do_log_to_io(Log, Mibs, Start, Stop) ->
    MiniMib = snmp_mini_mib:create(Mibs),
    Write = fun(X) -> 
		    case format_msg(X, MiniMib, Start, Stop) of
			{ok, S} ->
			    io:format("~s", [S]);
			_ ->
			    ok
		    end
	    end,
    (catch loop(disk_log:chunk(Log, start), Log, Write)),
    snmp_mini_mib:delete(MiniMib),
    ok.


loop(eof, _Log, _Write) ->
    ok;
loop({error, _} = Error, _Log, _Write) ->
    Error;
loop({corrupt_log_file, _} = Reason, _Log, _Write) ->
    {error, Reason};
loop({Cont, Terms}, Log, Write) ->
    case (catch lists:foreach(Write, Terms)) of
	{'EXIT', Reason} ->
	    {error, Reason};
	_ ->
	    loop(disk_log:chunk(Log, Cont), Log, Write)
    end;
loop({Cont, Terms, BadBytes}, Log, Write) ->
    error_logger:error_msg("Skipping ~w bytes while converting ~p~n~n", 
			   [BadBytes, Log]),
    case (catch lists:foreach(Write, Terms)) of
	{'EXIT', Reason} ->
	    {error, Reason};
	_ ->
	    loop(disk_log:chunk(Log, Cont), Log, Write)
    end;
loop(Error, _Log, _Write) ->
    Error.

format_msg({TimeStamp, {V3Hdr, ScopedPdu}, {Addr, Port}}, 
	   Mib, Start, Stop) ->
    format_msg({TimeStamp, {V3Hdr, ScopedPdu}, Addr, Port}, 
	      Mib, Start, Stop);
format_msg({TimeStamp, {V3Hdr, ScopedPdu}, Addr, Port}, 
	  Mib, Start, Stop) ->
%     io:format("format_msg -> entry with"
% 	      "~n   TimeStamp: ~p"
% 	      "~n   Start:     ~p"
% 	      "~n   Stop:      ~p", [TimeStamp, Start, Stop]),
    case timestamp_filter(TimeStamp, Start, Stop) of
        true ->
            case (catch snmp_pdus:dec_scoped_pdu(ScopedPdu)) of
                ScopedPDU when record(ScopedPDU, scopedPdu) -> 
                    Msg = #message{version = 'version-3',
                                   vsn_hdr = V3Hdr,
                                   data    = ScopedPDU},
                    f(ts2str(TimeStamp), Msg, Addr, Port, Mib);
                {'EXIT', Reason} ->
                    format_tab("** error in log file at ~s from ~p:~w ~p\n\n", 
			       [ts2str(TimeStamp), ip(Addr), Port, Reason])
            end;
        false ->
            ignore
    end;
format_msg({TimeStamp, Packet, {Addr, Port}}, Mib, Start, Stop) -> 
    format_msg({TimeStamp, Packet, Addr, Port}, Mib, Start, Stop);
format_msg({TimeStamp, Packet, Addr, Port}, Mib, Start, Stop) ->
    case timestamp_filter(TimeStamp, Start, Stop) of
        true ->
            case (catch snmp_pdus:dec_message(binary_to_list(Packet))) of
                Msg when record(Msg, message) ->
                    f(ts2str(TimeStamp), Msg, Addr, Port, Mib);
                {'EXIT', Reason} ->
                    format_tab("** error in log file ~p\n\n", [Reason])
            end;
        false ->
            ignore
    end;
format_msg(_, _Mib, _Start, _Stop) ->
    format_tab("** unknown entry in log file\n\n", []).

f(TimeStamp, #message{version = Vsn, vsn_hdr = VsnHdr, data = Data}, 
  Addr, Port, Mib) ->
    Str    = format_pdu(Data, Mib),
    HdrStr = format_header(Vsn, VsnHdr),
    case get_type(Data) of
        trappdu ->
            f_trap(TimeStamp, Vsn, HdrStr, Str, Addr, Port);
        'snmpv2-trap' ->
            f_trap(TimeStamp, Vsn, HdrStr, Str, Addr, Port);
        'inform-request' ->
            f_inform(TimeStamp, Vsn, HdrStr, Str, Addr, Port);
        'get-response' ->
            f_response(TimeStamp, Vsn, HdrStr, Str, Addr, Port);
        report ->
            f_report(TimeStamp, Vsn, HdrStr, Str, Addr, Port);
        _ ->
            f_request(TimeStamp, Vsn, HdrStr, Str, Addr, Port)
    end.

f_request(TimeStamp, Vsn, HdrStr, Str, Addr, Port) ->
    format_tab("request ~s:~w - ~s [~s] ~w\n~s", 
	       [ip(Addr), Port, HdrStr, TimeStamp, Vsn, Str]).

f_response(TimeStamp, Vsn, HdrStr, Str, Addr, Port) ->
    format_tab("response ~s:~w - ~s [~s] ~w\n~s", 
	       [ip(Addr), Port, HdrStr, TimeStamp, Vsn, Str]).

f_report(TimeStamp, Vsn, HdrStr, Str, Addr, Port) ->
    format_tab("report ~s:~w - ~s [~s] ~w\n~s", 
	       [ip(Addr), Port, HdrStr, TimeStamp, Vsn, Str]).

f_trap(TimeStamp, Vsn, HdrStr, Str, Addr, Port) ->
    format_tab("trap ~s:~w - ~s [~s] ~w\n~s", 
	       [ip(Addr), Port, HdrStr, TimeStamp, Vsn, Str]).

f_inform(TimeStamp, Vsn, HdrStr, Str, Addr, Port) ->
    format_tab("inform ~s:~w - ~s [~s] ~w\n~s", 
	       [ip(Addr), Port, HdrStr, TimeStamp, Vsn, Str]).


%% Convert a timestamp 2-tupple to a printable string
%%
ts2str({Local,Universal}) ->
    dat2str(Local) ++ " , " ++ dat2str(Universal);
ts2str(_) ->
    "".

%% Convert a datetime 2-tupple to a printable string
%%
dat2str({{Y,M,D},{H,Min,S}}) -> 
    io_lib:format("~w-~w-~w,~w:~w:~w",[Y,M,D,H,Min,S]).


timestamp_filter({Local,Universal},Start,Stop) ->
    tsf_ge(Local,Universal,Start) and tsf_le(Local,Universal,Stop);
timestamp_filter(_,_Start,_Stop) -> 
    true.

tsf_ge(_Local,_Universal,null) ->
    true;
tsf_ge(Local,_Universal,{local_time,DateTime}) ->
    tsf_ge(Local,DateTime);
tsf_ge(_Local,Universal,{universal_time,DateTime}) ->
    tsf_ge(Universal,DateTime);
tsf_ge(Local,_Universal,DateTime) ->
    tsf_ge(Local,DateTime).

tsf_ge(TimeStamp,DateTime) -> 
    T1 = calendar:datetime_to_gregorian_seconds(TimeStamp),
    T2 = calendar:datetime_to_gregorian_seconds(DateTime),
    T1 >= T2.

tsf_le(_Local,_Universal,null) ->
    true;
tsf_le(Local,_Universal,{local_time,DateTime}) ->
    tsf_le(Local,DateTime);
tsf_le(_Local,Universal,{universal_time,DateTime}) ->
    tsf_le(Universal,DateTime);
tsf_le(Local,_Universal,DateTime) ->
    tsf_le(Local,DateTime).

tsf_le(TimeStamp,DateTime) ->
    T1 = calendar:datetime_to_gregorian_seconds(TimeStamp),
    T2 = calendar:datetime_to_gregorian_seconds(DateTime),
    T1 =< T2.


%% In the output replace TAB by ESC TAB, and add a single trailing TAB.
%%
format_tab(Format, Args) ->
    Str  = lists:flatten(io_lib:format(Format, Args)),
    DStr = lists:map(fun($\t) -> "\e\t"; (C) -> C end, Str),
    {ok, io_lib:format("~s\t", [DStr])}.


format_header('version-1', CommunityStr) ->
    CommunityStr;
format_header('version-2', CommunityStr) ->
    CommunityStr;
format_header('version-3', #v3_hdr{msgFlags              = MsgFlags,
                                   msgSecurityModel      = SecModel,
                                   msgSecurityParameters = SecParams}) ->
    SecLevel = snmp_misc:get_sec_level(MsgFlags),
    case SecModel of
        ?SEC_USM ->
            case catch snmp_pdus:dec_usm_security_parameters(SecParams) of
                #usmSecurityParameters{msgAuthoritativeEngineID = AuthEngineID,
                                       msgUserName              = UserName} ->
                    io_lib:format("~w:\"~s\":\"~s\"",
                                  [SecLevel, AuthEngineID, UserName]);
                _ ->
                    "-"
            end;
        _ ->
            "\"unknown security model\""
    end.


format_pdu(#scopedPdu{contextName = Context, data = Pdu}, Mib) ->
    io_lib:format("Context: \"~s\"\n~s",
                  [Context, snmp_misc:format_pdu(Pdu, Mib)]);
format_pdu(Pdu, Mib) ->
    snmp_misc:format_pdu(Pdu, Mib).

get_type(#scopedPdu{data = Pdu}) ->
    get_type(Pdu);
get_type(Pdu) when record(Pdu, trappdu) ->
    trappdu;
get_type(#pdu{type = Type}) ->
    Type.


ip({A,B,C,D}) ->
    io_lib:format("~w.~w.~w.~w", [A,B,C,D]).



%% -------------------------------------------------------------------    
%% Various utility functions
%% -------------------------------------------------------------------    

log_open(Name, File, Size, Repair, Notify) ->
    case do_log_open(Name, File, Size, Repair, Notify) of
	{ok, Log} ->
	    {ok, Log};
	{repaired, Log, Rec, Bad} ->
	    ?vlog("log_open -> repaired: "
		  "~n   Rec: ~p"
		  "~n   Bad: ~p", [Rec, Bad]),
	    {ok, Log};
	Error ->
	    Error
    end.

%% We need to make sure we do not end up in an infinit loop
%% Take the number of files of the wrap log and add 2 (for
%% the index and size files).
do_log_open(Name, File, {_, N} = Size, snmp_repair = _Repair, Notify) ->
    do_snmp_log_open(Name, File, Size, N+2, Notify);

do_log_open(Name, File, Size, snmp_repair = _Repair, Notify) ->
    do_snmp_log_open(Name, File, Size, 1, Notify);

do_log_open(Name, File, Size, Repair, Notify) ->
    do_std_log_open(Name, File, Size, Repair, Notify).


do_snmp_log_open(Name, File, Size, N, Notify) when N =< 0 ->
    do_std_log_open(Name, File, Size, true, Notify);
do_snmp_log_open(Name, File, Size, N, Notify) ->
    case do_std_log_open(Name, File, Size, true, Notify) of
	{error, {not_a_log_file, XFile}} ->
	    case file:rename(XFile, lists:append([XFile, ".MOVED"])) of
		ok ->
		    ?vinfo("Failed open log file (even with repair) - "
			   "not a logfile:"
			   "~n   Attempting to move file aside (.MOVED)"
			   "~n   ~s", [XFile]),
		    do_snmp_log_open(Name, File, Size, N-1, Notify);
		Error ->
		    {error, {rename_failed, Error}}
	    end;
	{error, Reason} ->
	    ?vinfo("Failed open log file (even with repair) - "
		   "~n   Attempting to move old log file aside (.MOVED)"
		   "~n~p", [Reason]),
	    move_log(File),
	    do_std_log_open(Name, File, Size, true, Notify);
	Else ->
	    Else
    end.


%% First try to open the log without the size-spec.  This will 
%% succeed if the log has already been created.  In that case, 
%% we'll use whatever size the log had at the time it was closed.
do_std_log_open(Name, File, Size, Repair, Notify) ->
    Opts = [{name,   Name},
	    {file,   File},
	    {type,   ?LOG_TYPE},
	    {format, ?LOG_FORMAT},
	    {mode,   read_write},
	    {notify, Notify}, 
	    {repair, Repair}],
    case disk_log:open(Opts) of
	{error, {badarg, size}} ->
	    %% The log didn't exist, try with the size-spec
            disk_log:open([{size, Size} | Opts]);
	Else ->
	    Else
    end.


log_open(Name, File) ->
    Opts = [{name,   Name}, 
	    {file,   File}, 
	    {type,   ?LOG_TYPE},
	    {format, ?LOG_FORMAT},	    
	    {mode,   read_only}],
    case disk_log:open(Opts) of
	{error, {badarg, size}} ->
	    {error, no_such_log};
	Else ->
	    Else
    end.


move_log(File) ->
    Dir      = filename:dirname(File),
    FileName = filename:basename(File),
    case file:list_dir(Dir) of
        {ok, Files0} ->
	    Files = [F || F <- Files0, lists:prefix(FileName, F)],
	    F = fun(XFile) ->
			file:rename(XFile, lists:append([XFile, ".MOVED"]))
		end,
            lists:foreach(F, Files);
        _ ->
            ok
    end.
    

is_owner(Log) ->    
    lists:member(self(), log_owners(Log)).

log_owners(Log) ->
    Info = log_info(Log),
    case lists:keysearch(owners, 1, Info) of
	{value, {_, Pids}} ->
	    [P || {P, _} <- Pids];
	_ ->
	    []
    end.

log_info(Log) ->
    case disk_log:info(Log) of
	Info when list(Info) ->
	    Info;
	_ ->
	    []
    end.


timestamp() ->     
    {calendar:local_time(), calendar:universal_time()}.

