%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2017. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%
-module(error_logger).

-export([start/0,start_link/0,format/2,error_msg/1,error_msg/2,error_report/1,
	 error_report/2,info_report/1,info_report/2,warning_report/1,
	 warning_report/2,error_info/1,
	 info_msg/1,info_msg/2,warning_msg/1,warning_msg/2, 
	 logfile/1,tty/1,swap_handler/1,
	 add_report_handler/1,add_report_handler/2,
	 delete_report_handler/1]).

-export([init/1,
	 handle_event/2, handle_call/2, handle_info/2,
	 terminate/2]).

-export([get_format_depth/0, limit_term/1]).

-define(buffer_size, 10).

%%-----------------------------------------------------------------
%% Types used in this file
%%-----------------------------------------------------------------

-type msg_tag() :: 'error' | 'error_report'
		 | 'info' | 'info_msg' | 'info_report'
		 | 'warning_msg' | 'warning_report'.

-type state()   :: {non_neg_integer(), non_neg_integer(), [term()]}.

%%% BIF

-export([warning_map/0]).

-spec warning_map() -> Tag when
      Tag :: error | warning | info.

warning_map() ->
    erlang:nif_error(undef).

%%% End of BIF

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

-spec start() -> {'ok', pid()} | {'error', any()}.

start() ->
    case gen_event:start({local, error_logger}) of
	{ok, Pid} ->
	    simple_logger(?buffer_size),
	    {ok, Pid};
	Error -> Error
    end.

-spec start_link() -> {'ok', pid()} | {'error', any()}.

start_link() ->
    case gen_event:start_link({local, error_logger}) of
	{ok, Pid} ->
	    simple_logger(?buffer_size),
	    {ok, Pid};
	Error -> Error
    end.

%%-----------------------------------------------------------------
%% These two simple old functions generate events tagged 'error'
%% Used for simple messages; error or information.
%%-----------------------------------------------------------------

-spec error_msg(Format) -> 'ok' when
      Format :: string().

error_msg(Format) ->
    error_msg(Format,[]).

-spec error_msg(Format, Data) -> 'ok' when
      Format :: string(),
      Data :: list().

error_msg(Format, Args) ->
    notify({error, group_leader(), {self(), Format, Args}}).

-spec format(Format, Data) -> 'ok' when
      Format :: string(),
      Data :: list().

format(Format, Args) ->
    notify({error, group_leader(), {self(), Format, Args}}).

%%-----------------------------------------------------------------
%% This functions should be used for error reports.  Events
%% are tagged 'error_report'.
%% The 'std_error' error_report type can always be used.
%%-----------------------------------------------------------------

-type report() ::
        [{Tag :: term(), Data :: term()} | term()] | string() | term().

-spec error_report(Report) -> 'ok' when
      Report :: report().

error_report(Report) -> 
    error_report(std_error, Report).

-spec error_report(Type, Report) -> 'ok' when
      Type :: term(),
      Report :: report().

error_report(Type, Report) ->
    notify({error_report, group_leader(), {self(), Type, Report}}).

%%-----------------------------------------------------------------
%% This function should be used for warning reports.  
%% These might be mapped to error reports or info reports, 
%% depending on emulator flags. Events that ore not mapped
%% are tagged 'info_report'.
%% The 'std_warning' info_report type can always be used and is 
%% mapped to std_info or std_error accordingly.
%%-----------------------------------------------------------------

-spec warning_report(Report) -> 'ok' when
      Report :: report().

warning_report(Report) -> 
    warning_report(std_warning, Report).

-spec warning_report(Type, Report) -> 'ok' when
      Type :: any(),
      Report :: report().

warning_report(Type, Report) ->
    {Tag, NType} = case error_logger:warning_map() of
		       info ->
			   if 
			       Type =:= std_warning ->
				   {info_report, std_info};
			       true ->
				   {info_report, Type}
			   end;
		       warning ->
			   {warning_report, Type};
		       error ->
			   if
			       Type =:= std_warning ->
				   {error_report, std_error};
			       true ->
				   {error_report, Type}
			   end
		   end,
    notify({Tag, group_leader(), {self(), NType, Report}}).

%%-----------------------------------------------------------------
%% This function provides similar functions as error_msg for
%% warning messages, like warning report it might get mapped to
%% other types of reports.
%%-----------------------------------------------------------------

-spec warning_msg(Format) -> 'ok' when
      Format :: string().

warning_msg(Format) ->
    warning_msg(Format,[]).

-spec warning_msg(Format, Data) -> 'ok' when
      Format :: string(),
      Data :: list().

warning_msg(Format, Args) ->
    Tag = case error_logger:warning_map() of
	      warning ->
		  warning_msg;
	      info ->
		  info_msg;
	      error ->
		  error
	  end,
    notify({Tag, group_leader(), {self(), Format, Args}}).

%%-----------------------------------------------------------------
%% This function should be used for information reports.  Events
%% are tagged 'info_report'.
%% The 'std_info' info_report type can always be used.
%%-----------------------------------------------------------------

-spec info_report(Report) -> 'ok' when
      Report :: report().

info_report(Report) -> 
    info_report(std_info, Report).

-spec info_report(Type, Report) -> 'ok' when
      Type :: any(),
      Report :: report().

info_report(Type, Report) ->
    notify({info_report, group_leader(), {self(), Type, Report}}).

%%-----------------------------------------------------------------
%% This function provides similar functions as error_msg for
%% information messages.
%%-----------------------------------------------------------------

-spec info_msg(Format) -> 'ok' when
      Format :: string().

info_msg(Format) ->
    info_msg(Format,[]).

-spec info_msg(Format, Data) -> 'ok' when
      Format :: string(),
      Data :: list().

info_msg(Format, Args) ->
    notify({info_msg, group_leader(), {self(), Format, Args}}).

%%-----------------------------------------------------------------
%% Used by the init process.  Events are tagged 'info'.
%%-----------------------------------------------------------------

-spec error_info(Error :: any()) -> 'ok'.

error_info(Error) ->
    notify({info, group_leader(), {self(), Error, []}}).

-spec notify({msg_tag(), pid(), {pid(), any(), any()}}) -> 'ok'.

notify(Msg) ->
    gen_event:notify(error_logger, Msg).

-type swap_handler_type() :: 'false' | 'silent' | 'tty' | {'logfile', string()}.
-spec swap_handler(Type :: swap_handler_type()) -> any().

swap_handler(tty) ->
    R = gen_event:swap_handler(error_logger, {error_logger, swap},
                               {error_logger_tty_h, []}),
    ok = simple_logger(),
    R;
swap_handler({logfile, File}) ->
    R = gen_event:swap_handler(error_logger, {error_logger, swap},
                               {error_logger_file_h, File}),
    ok = simple_logger(),
    R;
swap_handler(silent) ->
    _ = gen_event:delete_handler(error_logger, error_logger, delete),
    ok = simple_logger();
swap_handler(false) ->
    ok. % keep primitive event handler as-is

-spec add_report_handler(Handler) -> any() when
      Handler :: module().

add_report_handler(Module) when is_atom(Module) ->
    gen_event:add_handler(error_logger, Module, []).

-spec add_report_handler(Handler, Args) -> Result when
      Handler :: module(),
      Args :: gen_event:handler_args(),
      Result :: gen_event:add_handler_ret().

add_report_handler(Module, Args) when is_atom(Module) ->
    gen_event:add_handler(error_logger, Module, Args).

-spec delete_report_handler(Handler) -> Result when
      Handler :: module(),
      Result :: gen_event:del_handler_ret().

delete_report_handler(Module) when is_atom(Module) ->
    gen_event:delete_handler(error_logger, Module, []).

%% Start the lowest level error_logger handler with Buffer.

simple_logger(Buffer_size) when is_integer(Buffer_size) ->
    gen_event:add_handler(error_logger, error_logger, Buffer_size).

%% Start the lowest level error_logger handler without Buffer.

simple_logger() -> 
    gen_event:add_handler(error_logger, error_logger, []).

%% Log all errors to File for all eternity

-type open_error() :: file:posix() | badarg | system_limit.

-spec logfile(Request :: {open, Filename}) -> ok | {error, OpenReason} when
                  Filename ::file:name(),
                  OpenReason :: allready_have_logfile | open_error()
           ; (Request :: close) -> ok | {error, CloseReason} when
                  CloseReason :: module_not_found
	   ; (Request :: filename) -> Filename | {error, FilenameReason} when
                  Filename :: file:name(),
                  FilenameReason :: no_log_file.

logfile({open, File}) ->
    case lists:member(error_logger_file_h,
		      gen_event:which_handlers(error_logger)) of
	true ->
	    {error, allready_have_logfile};
	_ ->
	    gen_event:add_handler(error_logger, error_logger_file_h, File)
    end;
logfile(close) ->
    case gen_event:delete_handler(error_logger, error_logger_file_h, normal) of
	{error,Reason} ->
	    {error,Reason};
	_ ->
	    ok
    end;
logfile(filename) ->
    case gen_event:call(error_logger, error_logger_file_h, filename) of
	{error,_} ->
	    {error, no_log_file};
	Val ->
	    Val
    end.

%% Possibly turn off all tty printouts, maybe we only want the errors
%% to go to a file

-spec tty(Flag) -> 'ok' when
      Flag :: boolean().

tty(true) ->
    Hs = gen_event:which_handlers(error_logger),
    case lists:member(error_logger_tty_h, Hs) of
	false ->
	    gen_event:add_handler(error_logger, error_logger_tty_h, []);
	true -> 
	    ignore
    end,
    ok;
tty(false) ->
    gen_event:delete_handler(error_logger, error_logger_tty_h, []),
    ok.


%%% ---------------------------------------------------
%%% This is the default error_logger handler.
%%% ---------------------------------------------------

-spec init(term()) -> {'ok', state() | []}.

init(Max) when is_integer(Max) ->
    {ok, {Max, 0, []}};
%% This one is called if someone took over from us, and now wants to
%% go back.
init({go_back, _PostState}) ->  
    {ok, {?buffer_size, 0, []}};
init(_) ->
    %% The error logger process may receive a huge amount of
    %% messages. Make sure that they are stored off heap to
    %% avoid exessive GCs.
    process_flag(message_queue_data, off_heap),
    {ok, []}.

-spec handle_event(term(), state()) -> {'ok', state()}.

handle_event({Type, GL, Msg}, State) when node(GL) =/= node() ->
    gen_event:notify({error_logger, node(GL)},{Type, GL, Msg}),
    %% handle_event2({Type, GL, Msg}, State);  %% Shall we do something
    {ok, State};                               %% at this node too ???
handle_event({info_report, _, {_, Type, _}}, State) when Type =/= std_info ->
    {ok, State};   %% Ignore other info reports here
handle_event(Event, State) ->
    handle_event2(Event, State).

-spec handle_info(term(), state()) -> {'ok', state()}.

handle_info({emulator, GL, Chars}, State) when node(GL) =/= node() ->
    {error_logger, node(GL)} ! {emulator, GL, add_node(Chars,self())},
    {ok, State};
handle_info({emulator, GL, Chars}, State) ->
    handle_event2({emulator, GL, Chars}, State);
handle_info(_, State) ->
    {ok, State}.

-spec handle_call(term(), state()) -> {'ok', {'error', 'bad_query'}, state()}.

handle_call(_Query, State) -> {ok, {error, bad_query}, State}.

-spec terminate(term(), state()) -> {'error_logger', [term()]}.

terminate(swap, {_, 0, Buff}) ->
    {error_logger, Buff};
terminate(swap, {_, Lost, Buff}) ->
    Myevent = {info, group_leader(), {self(), {lost_messages, Lost}, []}},
    {error_logger, [tag_event(Myevent)|Buff]};
terminate(_, _) ->
    {error_logger, []}.

handle_event2(Event, {1, Lost, Buff}) ->
    display(tag_event(Event)),
    {ok, {1, Lost+1, Buff}};
handle_event2(Event, {N, Lost, Buff}) ->
    Tagged = tag_event(Event),
    display(Tagged),
    {ok, {N-1, Lost, [Tagged|Buff]}};
handle_event2(_, State) ->
    {ok, State}.

tag_event(Event) ->    
    {erlang:localtime(), Event}.

display({Tag,{error,_,{_,Format,Args}}}) ->
    display2(Tag,Format,Args);
display({Tag,{error_report,_,{_,Type,Report}}}) ->
    display2(Tag,Type,Report);
display({Tag,{info_report,_,{_,Type,Report}}}) ->
    display2(Tag,Type,Report);
display({Tag,{info,_,{_,Error,_}}}) ->
    display2(Tag,Error,[]);
display({Tag,{info_msg,_,{_,Format,Args}}}) ->
    display2(Tag,Format,Args);
display({Tag,{warning_report,_,{_,Type,Report}}}) ->
    display2(Tag,Type,Report);
display({Tag,{warning_msg,_,{_,Format,Args}}}) ->
    display2(Tag,Format,Args);
display({Tag,{emulator,_,Chars}}) ->
    display2(Tag,Chars,[]).

add_node(X, Pid) when is_atom(X) ->
    add_node(atom_to_list(X), Pid);
add_node(X, Pid) ->
    lists:concat([X,"** at node ",node(Pid)," **~n"]).

%% Can't do io_lib:format

display2({{_Y,_Mo,_D},{_H,_Mi,_S}} = Date, F, A) ->
    display_date(Date),
    display3(string_p(F), F, A).

display_date({{Y,Mo,D},{H,Mi,S}}) ->
    erlang:display_string(
      integer_to_list(Y) ++ "-" ++
	  two_digits(Mo) ++ "-" ++
	  two_digits(D)  ++ " " ++
	  two_digits(H)  ++ ":" ++
	  two_digits(Mi) ++ ":" ++
	  two_digits(S)  ++ " ").

two_digits(N) when 0 =< N, N =< 9 ->
    [$0, $0 + N];
two_digits(N) ->
    integer_to_list(N).

display3(true, F, A) ->
    %% Format string with arguments
    erlang:display_string(F ++ "\n"),
    [begin
	 erlang:display_string("\t"),
	 erlang:display(Arg)
     end || Arg <- A],
    ok;
display3(false, Atom, A) when is_atom(Atom) ->
    %% The widest atom seems to be 'supervisor_report' at 17.
    ColumnWidth = 20,
    AtomString = atom_to_list(Atom),
    AtomLength = length(AtomString),
    Padding = lists:duplicate(ColumnWidth - AtomLength, $\s),
    erlang:display_string(AtomString ++ Padding),
    display4(A);
display3(_, F, A) ->
    erlang:display({F, A}).

display4([A, []]) ->
    %% Not sure why crash reports look like this.
    display4(A);
display4(A = [_|_]) ->
    case lists:all(fun({Key,_Value}) -> is_atom(Key); (_) -> false end, A) of
	true ->
	    erlang:display_string("\n"),
	    lists:foreach(
	      fun({Key, Value}) ->
		      erlang:display_string(
			"    " ++
			    atom_to_list(Key) ++
			    ": "),
		      erlang:display(Value)
	      end, A);
	false ->
	    erlang:display(A)
    end;
display4(A) ->
    erlang:display(A).

string_p([]) ->
    false;
string_p(Term) ->
    string_p1(Term).

string_p1([H|T]) when is_integer(H), H >= $\040, H =< $\176 ->
    string_p1(T);
string_p1([H|T]) when is_integer(H), H >= 16#A0, H < 16#D800;
                      is_integer(H), H > 16#DFFF, H < 16#FFFE;
                      is_integer(H), H > 16#FFFF, H =< 16#10FFFF ->
    string_p1(T);
string_p1([$\n|T]) -> string_p1(T);
string_p1([$\r|T]) -> string_p1(T);
string_p1([$\t|T]) -> string_p1(T);
string_p1([$\v|T]) -> string_p1(T);
string_p1([$\b|T]) -> string_p1(T);
string_p1([$\f|T]) -> string_p1(T);
string_p1([$\e|T]) -> string_p1(T);
string_p1([H|T]) when is_list(H) ->
    case string_p1(H) of
	true -> string_p1(T);
	_    -> false
    end;
string_p1([]) -> true;
string_p1(_) ->  false.

-spec limit_term(term()) -> term().

limit_term(Term) ->
    case get_format_depth() of
        unlimited -> Term;
        D -> io_lib:limit_term(Term, D)
    end.

-spec get_format_depth() -> 'unlimited' | pos_integer().

get_format_depth() ->
    case application:get_env(kernel, error_logger_format_depth) of
	{ok, Depth} when is_integer(Depth) ->
	    max(10, Depth);
	undefined ->
	    unlimited
    end.
