%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1996-2025. 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(rb).
-moduledoc """
The Report Browser Tool

The Report Browser (RB) tool is used to browse and format error reports written
by the error logger handler `m:log_mf_h` in STDLIB.
""".

-behaviour(gen_server).

%% External exports
-export([start/0, start/1, stop/0, rescan/0, rescan/1]).
-export([list/0, list/1, log_list/0, log_list/1, show/0, show/1, grep/1, filter/1, filter/2, start_log/1, stop_log/0]).
-export([h/0, help/0]).

%% Internal exports
-export([start_link/1]).

%% gen_server callbacks
-export([init/1, terminate/2, handle_call/3,
	 handle_cast/2, handle_info/2, code_change/3]).

-type type() :: error | error_report | info_msg | info_report |
                warning_msg | warning_report | crash_report |
                supervisor_report | progress | all.
-type option() :: {start_log, FileName :: string() | atom() | pid()} |
                  {max, MaxNoOfReports :: integer() | all} |
                  {report_dir, DirString :: string()} |
                  {type, ReportType :: type() | [type()] | all } |
                  {abort_on_error, boolean()}.

%%%-----------------------------------------------------------------
%%% Report Browser Tool.
%%% Formats Error reports written by log_mf_h
%%%-----------------------------------------------------------------

-record(state, {dir, data, device, max, type, abort, log}).

%%-----------------------------------------------------------------
%% Interface functions.
%% For available options; see print_options().
%%-----------------------------------------------------------------
-doc(#{equiv => start([])}).
-spec start() -> term().
start() -> start([]).
-doc """
start(Options)

Function [`start/1`](`start/1`) starts `rb_server` with the specified options,
whereas function `start/0` starts with default options.

`rb_server` must be started before reports can be browsed. When
`rb_server` is started, the files in the specified directory are
scanned. The other functions assume that the server has started.

_Options:_

- **`{start_log, FileName}`** - Starts logging to file, registered name, or
  `io_device`. All reports are printed to the specified destination. Default is
  `standard_io`. Option `{start_log, standard_error}` is not allowed and will be
  replaced by default `standard_io`.

- **`{max, MaxNoOfReports}`** - Controls how many reports `rb_server` is to read
  at startup. This option is useful, as the directory can contain a large amount
  of reports. If this option is specified, the `MaxNoOfReports` latest reports
  are read. Default is `all`.

- **`{report_dir, DirString}`** - Defines the directory where the error log
  files are located. Default is the directory specified by application
  environment variable `error_logger_mf_dir`, see [sasl(6)](sasl_app.md).

- **`{type, ReportType}`** - Controls what kind of reports `rb_server` is to
  read at startup. `ReportType` is a supported type, `all`, or a list of
  supported types. Default is `all`.

- **`{abort_on_error, Bool}`** - Specifies if logging is to be ended if `rb`
  encounters an unprintable report. (You can get a report with an incorrect form
  if function `error_logger`, `error_msg`, or `info_msg` has been called with an
  invalid format string)

  - If `Bool` is `true`, `rb` stops logging (and prints an error message to
    `stdout`) if it encounters a badly formatted report. If logging to file is
    enabled, an error message is appended to the log file as well.
  - If `Bool` is `false` (the default value), `rb` prints an error message to
    `stdout` for every bad report it encounters, but the logging process is
    never ended. All printable reports are written. If logging to file is
    enabled, `rb` prints `* UNPRINTABLE REPORT *` in the log file at the
    location of an unprintable report.
""".
-spec start(Options) -> term() when
      Options :: [option()].
start(Options) ->
    supervisor:start_child(sasl_sup, 
			   {rb_server, {rb, start_link, [Options]},
			    temporary, brutal_kill, worker, [rb]}).

-doc false.
start_link(Options) ->
    gen_server:start_link({local, rb_server}, rb, Options, []).

-doc """
stop()

Stops `rb_server`.
""".
-spec stop() -> term().
stop() -> 
    supervisor:terminate_child(sasl_sup, rb_server).

-doc(#{equiv => rescan([])}).
-spec rescan() -> term().
rescan() -> rescan([]).
-doc """
rescan(Options)

Rescans the report directory. `Options` is the same as for function `start/1`.
""".
-spec rescan(Options) -> term() when Options :: [option()].
rescan(Options) ->
    call({rescan, Options}).

-doc(#{equiv => list(all)}).
-spec list() -> term().
list() -> list(all).
-doc """
list(Type)

Lists all reports loaded in `rb_server`. Each report is given a unique number
that can be used as a reference to the report in function `show/1`.

If no `Type` is specified, all reports are listed.
""".
-spec list(Type :: type()) -> term().
list(Type) -> call({list, Type}).

-doc(#{equiv => log_list(all)}).
-doc(#{since => <<"OTP R16B02">>}).
-spec log_list() -> term().
log_list() -> log_list(all).
-doc """
log_list(Type)

Same as functions `list/0` or `list/1`, but the result is printed to a log file,
if set; otherwise to `standard_io`.

If no `Type` is specified, all reports are listed.
""".
-doc(#{since => <<"OTP R16B02">>}).
-spec log_list(Type :: type()) -> term().
log_list(Type) -> call({log_list, Type}).

-doc """
All reports are displayed.
""".
-spec show() -> term().
show() ->
    call(show).

-doc """
show(Report)

If argument `Report` is specified as one of the values of
[`type()`](`t:type/0`), all loaded reports of that type are
displayed. If `Report` is specified as an integer, the report with
this reference number is displayed.
""".
-spec show(Report) -> term() when Report :: integer() | type().
show(Number) when is_integer(Number) ->
    call({show_number, Number});
show(Type) when is_atom(Type) ->
    call({show_type, Type}).

-type regexp() :: string() | {string(), Options :: [re:options()]} |
                  re:mp() | {re:mp(), Options :: [re:compile_options()]}.

-doc """
grep(RegExp)

All reports matching the regular expression `RegExp` are displayed. `RegExp` can
be any of the following:

- A string containing the regular expression
- A tuple with the string and the options for compilation
- A compiled regular expression
- A compiled regular expression and the options for running it

For a definition of valid regular expressions and options, see the `m:re` module
in STDLIB and in particular function `re:run/3`.

For details about data type `mp()`, see `t:re:mp/0`.
""".
-spec grep(RegExp :: regexp()) -> term().
grep(RegExp) -> call({grep, RegExp}).

-type filter() :: {Key :: term(), Value :: term()} |
                  {Key :: term(), Value :: term(), no} |
                  {Key :: term(), RegExp :: regexp(), re} |
                  {Key :: term(), RegExp :: regexp(), re, no}.

-doc(#{equiv => filter/2}).
-doc(#{since => <<"OTP R13B04">>}).
-spec filter(Filters) -> term() when
      Filters :: [filter()].
filter(Filters) when is_list(Filters) ->
    call({filter, Filters}).

-doc """
filter(Filters, Dates)

Displays the reports that match the provided filters.

When a filter includes the `no` atom, it excludes the reports that match that
filter.

The reports are matched using the `m:proplists` module in STDLIB. The report
must be a proplist to be matched against any of the filters.

If the filter has the form `{Key, RegExp, re}`, the report must contain an
element with key equal to `Key` and the value must match the regular expression
`RegExp`.

If parameter `Dates` is specified, the reports are filtered according to the
date when they occurred. If `Dates` has the form `{DateFrom, from}`, reports
that occurred after `DateFrom` are displayed.

If `Dates` has the form `{DateTo, to}`, reports that occurred before `DateTo`
are displayed.

If two `Dates` are specified, reports that occurred between those dates are
returned.

To filter only by dates, specify the empty list as the `Filters` parameter.

For details about parameter `RegExp`, see `rb:grep/1`.

For details about data type `mp()`, see `t:re:mp/0`.

For details about data type `datetime()`, see `t:calendar:datetime/0`.
""".
-doc(#{since => <<"OTP R13B04">>}).
-spec filter(Filters, Dates) -> term() when
      Filters :: [filter()],
      Dates :: {DateFrom, DateTo} | {DateFrom, from} | {DateTo, to},
      DateFrom :: calendar:datetime(),
      DateTo :: calendar:datetime().
filter(Filters, FDates) when is_list(Filters) andalso is_tuple(FDates) ->
    call({filter, {Filters, FDates}}).

-doc """
start_log(FileName)

Redirects all report output from the RB tool to the specified file, registered
name, or `io_device`.
""".
-spec start_log(FileName) -> term() when FileName :: string() | atom() | pid().
start_log(FileName) -> call({start_log, FileName}).

-doc """
stop_log()

Closes the log file. The output from the RB tool is directed to `standard_io`.
""".
-spec stop_log() -> term().
stop_log() -> call(stop_log).

-doc(#{equiv => help()}).
-spec h() -> term().
h() -> help().
-doc """
help()

Displays online help information.
""".
-spec help() -> term().
help() ->
    io:format("~nReport Browser Tool - usage~n"),
    io:format("===========================~n"),
    io:format("rb:start()         - start the rb_server with default options~n"),
    io:format("rb:start(Options)  - where Options is a list of:~n"),
    print_options(),
    io:format("rb:h()             - print this help~n"),
    io:format("rb:help()          - print this help~n"),
    io:format("rb:list()          - list all reports~n"),
    io:format("rb:list(Type)      - list all reports of type Type~n"),
    io:format("rb:log_list()      - log list of all reports~n"),
    io:format("rb:log_list(Type)  - log list of all reports of type Type~n"),
    io:format("      currently supported types are:~n"),
    print_types(),
    io:format("rb:grep(RegExp)      - print reports containing RegExp.~n"),
    io:format("                     RegExp must be a valid argument for ~n"),
    io:format("                     the function re:run/2 or re:run/3.~n"),
    io:format("rb:filter(Filters) - print reports matching Filters.~n"),
    io:format("                     reports must be proplists.~n"),
    io:format("      Filters is a list of tuples of the following form:~n"),
    print_filters(),
    io:format("rb:filter(Filters, Dates)  -~n"),
    io:format("      same as rb:filter/1 but accepts date ranges to filter reports.~n"),
    io:format("      Dates must be of the following form:~n"),
    print_dates(),
    io:format("rb:rescan()        - rescans the report directory with same~n"),
    io:format("                     options.~n"),
    io:format("rb:rescan(Options) - rescans the report directory with new~n"),
    io:format("                     options. Options is same as in start/1.~n"),
    io:format("rb:show(Number)    - print report no Number~n"),
    io:format("rb:show(Type)      - print all reports of type Type~n"),
    io:format("rb:show()          - print all reports~n"),
    io:format("rb:start_log(File) - redirect all reports to file or io_device~n"),
    io:format("rb:stop_log()      - close the log file and redirect to~n"),
    io:format("                     standard_io~n"),
    io:format("rb:stop            - stop the rb_server~n").

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

%%-----------------------------------------------------------------
%% call(Request) -> Term
%%-----------------------------------------------------------------
call(Req) ->
    gen_server:call(rb_server, Req, infinity).

%%-----------------------------------------------------------------
%% MAKE SURE THESE TWO FUNCTIONS ARE UPDATED!
%%-----------------------------------------------------------------
print_options() ->
    io:format("      {start_log, FileName}~n"),
    io:format("         - default: standard_io~n"),
    io:format("      {max, MaxNoOfReports}~n"),
    io:format("         - MaxNoOfReports should be an integer or 'all'~n"),
    io:format("         - default: all~n"),
    io:format("      {report_dir, DirString}~n"),
    io:format("         - DirString should be a string without trailing~n"),
    io:format("         - directory delimiter.~n"),
    io:format("         - default: {sasl, error_logger_mf_dir}~n"),
    io:format("      {type, ReportType}~n"),
    io:format("         - ReportType should be a supported type, 'all'~n"),
    io:format("         - or a list of supported types~n"),
    io:format("         - default: all~n"),
    io:format("      {abort_on_error, Bool}~n"),
    io:format("         - Bool: true | false~n"),
    io:format("         - default: false~n").

print_types() ->
    io:format("         - crash_report~n"),
    io:format("         - supervisor_report~n"),
    io:format("         - progress~n"),
    io:format("         - error~n").

print_filters() ->
    io:format("      - {Key, Value}~n"),
    io:format("        includes report containing {Key, Value}~n"),
    io:format("      - {Key, Value, no}~n"),
    io:format("        excludes report containing {Key, Value}~n"),
    io:format("      - {Key, RegExp, re}~n"),
    io:format("        RegExp must be a valid argument for ~n"),
    io:format("        the function re:run/2 or re:run/3.~n"),
    io:format("      - {Key, RegExp, re, no}~n"),
    io:format("        excludes report containing {Key, RegExp}~n").

print_dates() ->
    io:format("      - {StartDate, EndDate}~n"),
    io:format("        StartDate = EndDate = {{Y,M,D},{H,M,S}} ~n"),
    io:format("        prints the reports with date between StartDate and EndDate~n"),
    io:format("      - {StartDate, from}~n"),
    io:format("        prints the reports with date greater than StartDate~n"),
    io:format("      - {EndDate, to}~n"),
    io:format("        prints the reports with date lesser than StartDate~n").

-doc false.
init(Options) ->
    process_flag(priority, low),
    process_flag(trap_exit, true),
    Log = get_option(Options, start_log, standard_io),
    Device = open_log_file(Log),
    Dir = get_report_dir(Options),
    Max = get_option(Options, max, all),
    Type = get_option(Options, type, all),
    Abort = get_option(Options, abort_on_error, false),
    Data = scan_files(Dir ++ "/", Max, Type),
    {ok, #state{dir = Dir ++ "/", data = Data, device = Device,
		max = Max, type = Type, abort = Abort, log = Log}}.

-doc false.
handle_call({rescan, Options}, _From, State) ->
    {Device,Log1} = 
	case get_option(Options, start_log, {undefined}) of
	    {undefined} -> 
		{State#state.device,State#state.log};
	    Log ->
		close_device(State#state.device),
		{open_log_file(Log),Log}
	end,
    Max = get_option(Options, max, State#state.max),
    Type = get_option(Options, type, State#state.type),
    Abort = get_option(Options, abort_on_error, false),
    Data = scan_files(State#state.dir, Max, Type),
    NewState = State#state{data = Data, max = Max, type = Type,
			   device = Device, abort = Abort, log = Log1},
    {reply, ok, NewState};
handle_call(_, _From, #state{data = undefined}) ->
    {reply, {error, no_data}, #state{}};
handle_call({list, Type}, _From, State) ->
    print_list(standard_io, State#state.data, Type),
    {reply, ok, State};
handle_call({log_list, Type}, _From, State) ->
    print_list(State#state.device, State#state.data, Type),
    {reply, ok, State};
handle_call({start_log, FileName}, _From, State) ->
    NewDevice = open_log_file(FileName),
    {reply, ok, State#state{device = NewDevice}};
handle_call(stop_log, _From, State) ->
    close_device(State#state.device),
    {reply, ok, State#state{device = standard_io}};
handle_call({show_number, Number}, _From, State) ->
    #state{dir = Dir, data = Data, device = Device, abort = Abort, log = Log} = State,
    NewDevice = print_report_by_num(Dir, Data, Number, Device, Abort, Log),
    {reply, ok, State#state{device = NewDevice}};
handle_call({show_type, Type}, _From, State) ->
    #state{dir = Dir, data = Data, device = Device, abort = Abort, log = Log} = State,
    NewDevice = print_typed_reports(Dir, Data, Type, Device, Abort, Log),
    {reply, ok, State#state{device = NewDevice}};
handle_call(show, _From, State) ->
    #state{dir = Dir, data = Data, device = Device, abort = Abort, log = Log} = State,
    NewDevice = print_all_reports(Dir, Data, Device, Abort, Log),
    {reply, ok, State#state{device = NewDevice}};
handle_call({grep, RegExp}, _From, State) ->
    #state{dir = Dir, data = Data, device = Device, abort = Abort, log = Log} = State,
    try print_grep_reports(Dir, Data, RegExp, Device, Abort, Log) of
	NewDevice ->
	    {reply, ok, State#state{device = NewDevice}}
    catch
	error:Error ->
	    {reply, {error, Error}, State}
    end;
handle_call({filter, Filters}, _From, State) ->
    #state{dir = Dir, data = Data, device = Device, abort = Abort, log = Log} = State,
    try filter_all_reports(Dir, Data, Filters, Device, Abort, Log) of
	NewDevice ->
	    {reply, ok, State#state{device = NewDevice}}
    catch
	error:Error ->
	    {reply, {error, Error}, State}
    end.

-doc false.
terminate(_Reason, #state{device = Device}) ->
    close_device(Device).

-doc false.
handle_cast(_Msg, State) ->
    {noreply, State}.
-doc false.
handle_info(_Info, State) ->
    {noreply, State}.
-doc false.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%-----------------------------------------------------------------
%% Func: open_log_file/1
%% Args: FileName | standard_io
%% Returns: A Device for later use in call to io:format
%%-----------------------------------------------------------------
open_log_file(standard_io) -> standard_io;
open_log_file(Fd) when is_atom(Fd),Fd=/=standard_error -> 
    case whereis(Fd) of
	undefined -> io:format("rb: Registered name not found '~ts'.~n",
		      [Fd]),
	             io:format("rb: Using standard_io~n"),
                     open_log_file(standard_io);
	Pid       -> open_log_file(Pid)	
    end;
open_log_file(Fd) when is_pid(Fd)-> Fd;
open_log_file(FileName) when is_list(FileName) ->
    case file:open(FileName, [write,append,{encoding,utf8}]) of
	{ok, Fd} -> Fd;
	Error -> 
	    io:format("rb: Cannot open file '~ts' (~w).~n",
		      [FileName, Error]),
	    io:format("rb: Using standard_io~n"),
	    standard_io
    end;
open_log_file(standard_error) -> 
	    io:format("rb: Using standard_io~n"),
	    standard_io.

close_device(Fd) when is_pid(Fd) ->
    catch file:close(Fd);
close_device(_) -> ok.

get_option(Options, Key, Default) ->
    case lists:keysearch(Key, 1, Options) of
	{value, {_Key, Value}} -> Value;
	_ -> Default
    end.

get_report_dir(Options) ->
    case lists:keysearch(report_dir, 1, Options) of
	{value, {_Key, RptDir}} -> RptDir;
	_ ->
	    case catch application:get_env(sasl, error_logger_mf_dir) of
		{ok, Dir} -> Dir;
		_ ->
		    exit("cannot locate report directory")
	    end
    end.

%%-----------------------------------------------------------------
%% Func: scan_files(RptDir, Max, Type)
%% Args: RptDir ::= string().
%%       Max ::= integer() | all, describing how many reports
%5               to read.
%%       Type ::= atom(), describing which reports to read.
%% Purpose: Scan all report files one time, and build a list of
%%          small elements 
%% Returns: Data, where Data is a list of
%%          {Number, Type, ShortDescr, Date, Fname, FilePosition}.
%%-----------------------------------------------------------------
scan_files(RptDir, Max, Type) ->
    case file:open(RptDir ++ "/index", [raw, read]) of
	{ok, Fd} ->
	    case catch file:read(Fd, 1) of
		{ok, [LastWritten]} -> 
		    ok = file:close(Fd),
		    Files = make_file_list(RptDir, LastWritten),
		    scan_files(RptDir, Files, Max, Type);		
		_X ->
		    _ = file:close(Fd),
		    exit("cannot read the index file")
	    end;
	_X -> exit("cannot read the index file")
    end.

make_file_list(Dir, FirstFileNo) ->
    case file:list_dir(Dir) of
	{ok, FileNames} ->
	    FileNumbers = lists:zf(fun(Name) ->
					   case catch list_to_integer(Name) of
					       Int when is_integer(Int) ->
						   {true, Int};
					       _ ->
						   false
					   end
				   end,
				   FileNames),
	    shift(lists:sort(FileNumbers), FirstFileNo);
	_ -> exit({bad_directory, Dir})
    end.
					  
shift(List, First) -> 
    shift(List, First, []).

shift([H | T], H, Res) ->
    [H | Res] ++ lists:reverse(T);
shift([H | T], First, Res) ->
    shift(T, First, [H | Res]);
shift([], _, Res) ->
    Res.

%%-----------------------------------------------------------------
%% Func: scan_files(Dir, Files, Max, Type)
%% Args: Files is a list of FileName.
%% Purpose: Scan the report files in the index variable.
%% Returns: {Number, Type, ShortDescr, Date, FileName, FilePosition}
%%-----------------------------------------------------------------
scan_files(Dir, Files, Max, Type) ->
    scan_files(Dir, 1, Files, [], Max, Type).
scan_files(_Dir, _, [], Res, _Max, _Type) -> Res;
scan_files(_Dir, _, _Files, Res, Max, _Type) when Max =< 0 -> Res;
scan_files(Dir, No, [H|T], Res, Max, Type) ->
    Data = get_report_data_from_file(Dir, No, H, Max, Type),
    Len = length(Data),
    NewMax = dec_max(Max, Len),
    NewNo = No + Len,
    NewData = Data ++ Res,
    scan_files(Dir, NewNo, T, NewData, NewMax, Type).

dec_max(all, _) -> all;
dec_max(X,Y) -> X-Y.

get_report_data_from_file(Dir, No, FileNr, Max, Type) ->	
    Fname = integer_to_list(FileNr),
    FileName = lists:concat([Dir, Fname]),
    case file:open(FileName, [read]) of
	{ok, Fd} when is_pid(Fd) -> read_reports(No, Fd, Fname, Max, Type);
	_ -> [{No, unknown, "Can't open file " ++ Fname, "???", Fname, 0}]
    end.

%%-----------------------------------------------------------------
%% Func: read_reports(No, Fd, Fname, Max, Type)
%% Purpose: Read reports from one report file.
%% Returns: A list of {No, Type, ShortDescr, Date, FileName, FilePosition}
%% Note: We have to read all reports, and then check the max-
%%       variable, because the reports are reversed on the file, and
%%       we may need the last ones.
%%-----------------------------------------------------------------
read_reports(No, Fd, Fname, Max, Type) ->
    io:format("rb: reading report..."),
    case catch read_reports(Fd, [], Type) of
	{ok, Res} -> 
	    ok = file:close(Fd),
	    io:format("done.~n"),
	    NewRes = 
		if
		    length(Res) > Max ->
			lists:sublist(Res, 1, Max);
		    true ->
			Res
		end,
	    add_report_data(NewRes, No, Fname);
	{error, [Problem | Res]} ->
	    _ = file:close(Fd),
	    io:format("Error: ~tp~n",[Problem]),
	    io:format("Salvaged ~p entries from corrupt report file ~ts...~n",
		      [length(Res),Fname]),
	    NewRes = 
		if
		    length([Problem|Res]) > Max ->
			lists:sublist([Problem|Res], 1, Max);
		    true ->
			[Problem|Res]
		end,
	    add_report_data(NewRes, No, Fname);
	Else ->
	    io:format("err ~tp~n", [Else]),
	    [{No, unknown, "Can't read reports from file " ++ Fname,
		  "???", Fname, 0}]
    end.

%%-----------------------------------------------------------------
%% Func: add_report_data(Res, No, FName)
%% Args: Res is a list of {Type, ShortDescr, Date, FilePos}
%% Purpose: Convert a list of {Type, ShortDescr, Date, FilePos} to
%%          a list of {No, Type, ShortDescr, Date, FileName, FilePos}
%% Returns: A list of {No, Type, ShortDescr, Date, FileName, FilePos}
%%-----------------------------------------------------------------
add_report_data(Res, No, FName) ->
    add_report_data(Res, No, FName, []).
add_report_data([{Type, ShortDescr, Date, FilePos}|T], No, FName, Res) ->
    add_report_data(T, No+1, FName,
		    [{No, Type, ShortDescr, Date, FName, FilePos}|Res]);
add_report_data([], _No, _FName, Res) -> Res.

read_reports(Fd, Res, Type) ->
    {ok, FilePos} = file:position(Fd, cur),
    case catch read_report(Fd) of
	{ok, Report} -> 
	    RealType = get_type(Report),
	    {ShortDescr, Date} = get_short_descr(Report),
	    Rep = {RealType, ShortDescr, Date, FilePos},
	    if
		Type == all->
		    read_reports(Fd, [Rep | Res], Type);
		RealType == Type ->
		    read_reports(Fd, [Rep | Res], Type);
		is_list(Type) ->
		    case lists:member(RealType, Type) of
			true ->
			    read_reports(Fd, [Rep | Res], Type);
			_ ->
			    read_reports(Fd, Res, Type)
		    end;
		true ->
		    read_reports(Fd, Res, Type)
	    end;
	{error, Error} ->
	    {error, [{unknown, Error, [], FilePos} | Res]};
	eof ->
	    {ok, Res};
	{'EXIT', Reason} ->
	    [{unknown, Reason, [], FilePos} | Res]
    end.

read_report(Fd) ->
    case io:get_chars(Fd,'',2) of
        [Hi,Lo] ->
            Size = get_int16(Hi,Lo),
            case io:get_chars(Fd,'',Size) of
                eof ->
                    {error,"Premature end of file"};
                List ->
                    Bin = list_to_binary(List),
		    Ref = make_ref(),
		    case (catch {Ref,binary_to_term(Bin)}) of
			{'EXIT',_} ->
			    {error, "Incomplete erlang term in log"};
			{Ref,Term} ->
			    {ok, Term}
		    end
	    end;
        eof ->
            eof
    end.
 
get_int16(Hi,Lo) ->
    ((Hi bsl 8) band 16#ff00) bor (Lo band 16#ff).


%%-----------------------------------------------------------------
%% Update these functions with the reports that should be possible
%% to browse with rb.
%%-----------------------------------------------------------------
get_type({_Time, {error_report, _Pid, {_, crash_report, _}}}) ->
    crash_report;
get_type({_Time, {error_report, _Pid, {_, supervisor_report, _}}}) ->
    supervisor_report;
get_type({_Time, {info_report, _Pid, {_, progress, _}}}) ->
    progress;
get_type({_Time, {Type, _, _}}) -> Type;
get_type(_) -> unknown.

get_short_descr({{Date, Time}, {error_report, Pid, {_, crash_report, Rep}}}) ->
    [OwnRep | _] = Rep,
    Name = 
	case lists:keysearch(registered_name, 1, OwnRep) of
	    {value, {_Key, []}} ->
		case lists:keysearch(initial_call, 1, OwnRep) of
		    {value, {_K, {M,_F,_A}}} -> M;
		    _ -> Pid
		end;
	    {value, {_Key, N}} -> N;
	    _ -> Pid
	end,
    {Name, date_str(Date, Time)};
get_short_descr({{Date, Time}, {error_report, Pid, {_, supervisor_report,Rep}}}) ->
    Name =
	case lists:keysearch(supervisor, 1, Rep) of
	    {value, {_Key, N}} when is_atom(N) -> N;
	    _ -> Pid
	end,
    {Name, date_str(Date,Time)};
get_short_descr({{Date, Time}, {_Type, Pid, _}}) ->
    {Pid, date_str(Date,Time)};
get_short_descr(_) ->
    {'???', "???"}.
    
date_str({Y,Mo,D}=Date,{H,Mi,S}=Time) ->
    case application:get_env(sasl,utc_log) of 
	{ok,true} ->
	    {{YY,MoMo,DD},{HH,MiMi,SS}} = 
		local_time_to_universal_time({Date,Time}),
	    lists:flatten(io_lib:format("~w-~2.2.0w-~2.2.0w ~2.2.0w:"
					"~2.2.0w:~2.2.0w UTC", 
					[YY,MoMo,DD,HH,MiMi,SS]));
	_ ->
	    lists:flatten(io_lib:format("~w-~2.2.0w-~2.2.0w ~2.2.0w:"
					"~2.2.0w:~2.2.0w", 
					[Y,Mo,D,H,Mi,S]))
    end.

local_time_to_universal_time({Date,Time}) ->
    case calendar:local_time_to_universal_time_dst({Date,Time}) of
	[UCT] ->
	    UCT;
	[UCT1,_UCT2] ->
	    UCT1;
	[] -> % should not happen
	    {Date,Time}
    end.


print_list(Fd, Data0, Type) ->
    Modifier = misc_supp:modifier(Fd),
    Header = {"No", "Type", "Process", "Date     Time"},
    {DescrWidth,DateWidth,Data} = find_widths(Data0, Modifier, 7, 13, []),
    Format = lists:concat(["~4s~20s ~", DescrWidth, "s~20s~n"]),
    io:format(Fd, Format, tuple_to_list(Header)),
    io:format(Fd, Format, ["==", "====", "=======", "====     ===="]),
    print_list(Fd, Data, Type, DescrWidth, DateWidth, Modifier).
print_list(_, [], _, _, _, _) -> true;
print_list(Fd, [H|T], Type, Width, DateWidth, Modifier) ->
    print_one_report(Fd, H, Type, Width, DateWidth, Modifier),
    print_list(Fd, T, Type, Width, DateWidth, Modifier).


find_widths([], _Modifier, DescrWidth, DateWidth, Data) ->
    {DescrWidth+1, DateWidth+1, lists:reverse(Data)};
find_widths([H|T], Modifier, DescrWidth, DateWidth, Data) ->
    DescrTerm = element(3,H),
    Descr = io_lib:format("~"++Modifier++"w", [DescrTerm]),
    DescrTry = string:length(Descr),
    NewDescrWidth =
        if
            DescrTry > DescrWidth -> DescrTry;
            true -> DescrWidth
        end,
    DateTry = string:length(element(4, H)),
    NewDateWitdh =
        if
            DateTry > DateWidth -> DateTry;
            true -> DateWidth
        end,
    NewH = setelement(3,H,Descr),
    find_widths(T, Modifier, NewDescrWidth, NewDateWitdh, [NewH|Data]).

print_one_report(Fd, {No, RealType, ShortDescr, Date, _Fname, _FilePos},
		 WantedType,
		 Width, DateWidth, Modifier) ->
    if
	WantedType == all ->
	    print_short_descr(Fd, No, RealType, ShortDescr, Date, Width, 
			      DateWidth, Modifier);
	WantedType == RealType ->
	    print_short_descr(Fd, No, RealType, ShortDescr, Date, Width, 
			      DateWidth, Modifier);
	true -> ok
    end.

print_short_descr(Fd, No, Type, ShortDescr, Date, Width, DateWidth, Modifier) ->
    Format = lists:concat(["~4w~20", Modifier, "w ~", Width, Modifier, "s~",
                           DateWidth, "s~n"]),
    io:format(Fd, Format, [No, Type,  ShortDescr, Date]).

print_report_by_num(Dir, Data, Number, Device, Abort, Log) ->
    {_,Device1} = print_report(Dir, Data, Number, Device, Abort, Log),
    Device1.
    
print_typed_reports(_Dir, [], _Type, Device, _Abort, _Log) ->
    Device;
print_typed_reports(Dir, Data, Type, Device, Abort, Log) ->
    {Next,Device1} = 
	case element(2, hd(Data)) of
	    Type -> 
		print_report(Dir, Data, element(1, hd(Data)), Device, Abort, Log);
	    _ -> 
		{proceed,Device}
	end,
    if Next == abort ->
	    Device1;
       true ->
	    print_typed_reports(Dir, tl(Data), Type, Device1, Abort, Log)
    end.

print_all_reports(_Dir, [], Device, _Abort, _Log) ->
    Device;
print_all_reports(Dir, Data, Device, Abort, Log) ->
    {Next,Device1} = print_report(Dir, Data, element(1, hd(Data)), 
				  Device, Abort, Log),
    if Next == abort ->
	    Device1;
       true ->
	    print_all_reports(Dir, tl(Data), Device1, Abort, Log)
    end.

print_report(Dir, Data, Number, Device, Abort, Log) ->
    case find_report(Data, Number) of
	{Fname, FilePosition} ->
	    FileName = lists:concat([Dir, Fname]),
	    case file:open(FileName, [read]) of
		{ok, Fd} -> 
		    read_rep(Fd, FilePosition, Device, Abort, Log);
		_ -> 
		    io:format("rb: can't open file ~tp~n", [Fname]),
		    {proceed,Device}
	    end;
	no_report ->
	    {proceed,Device}
    end.

find_report([{No, _Type, _Descr, _Date, Fname, FilePosition}|_T], No) ->
    {Fname, FilePosition};
find_report([_H|T], No) -> 
    find_report(T, No);
find_report([], No) ->
    io:format("There is no report with number ~p.~n", [No]),
    no_report.
    
print_grep_reports(_Dir, [], _RegExp, Device, _Abort, _Log) ->
    Device;
print_grep_reports(Dir, Data, RegExp, Device, Abort, Log) ->
    {Next,Device1} = print_grep_report(Dir, Data, element(1, hd(Data)), 
				       Device, RegExp, Abort, Log),
    if Next == abort ->
	    Device1;
       true ->
	    print_grep_reports(Dir, tl(Data), RegExp, Device1, Abort, Log)
    end.

print_grep_report(Dir, Data, Number, Device, RegExp, Abort, Log) ->
    {Fname, FilePosition} = find_report(Data, Number),
    FileName = lists:concat([Dir, Fname]),
    case file:open(FileName, [read]) of
	{ok, Fd} when is_pid(Fd) -> 
	    check_rep(Fd, FilePosition, Device, RegExp, Number, Abort, Log);
	_ -> 
	    io:format("rb: can't open file ~tp~n", [Fname]),
	    {proceed,Device}
    end.

check_rep(Fd, FilePosition, Device, RegExp, Number, Abort, Log) ->
    case read_rep_msg(Fd, FilePosition) of
	{Date, Msg} ->
	    MsgStr = lists:flatten(io_lib:format("~tp",[Msg])),
	    case run_re(MsgStr, RegExp) of
		match ->
		    io:format("Found match in report number ~w~n", [Number]),
		    case catch rb_format_supp:print(Date, Msg, Device) of
			{'EXIT', _} ->
			    handle_bad_form(Date, Msg, Device, Abort, Log);
			_ ->
			    {proceed,Device}
		    end;
		_ ->
		    {proceed,Device}
	    end;
	_ ->
	    io:format("rb: Cannot read from file~n"),
	    {proceed,Device}
    end.

run_re(Subject, {Regexp, Options}) ->
    run_re(Subject, Regexp, Options);
run_re(Subject, Regexp) ->
    run_re(Subject, Regexp, []).

run_re(Subject, Regexp, Options) ->
    case re:run(Subject, Regexp, [unicode|Options--[unicode]]) of
        nomatch ->
            nomatch;
	_ ->
            match
    end.

filter_all_reports(_Dir, [], _Filters, Device, _Abort, _Log) ->
    Device;
filter_all_reports(Dir, Data, Filters, Device, Abort, Log) ->
    {Next,Device1} = filter_report(Dir, Data, Filters, element(1, hd(Data)),
				  Device, Abort, Log),
    if Next == abort ->
	    Device1;
       true ->
	    filter_all_reports(Dir, tl(Data), Filters, Device1, Abort, Log)
    end.

filter_report(Dir, Data, Filters, Number, Device, Abort, Log) ->
    case find_report(Data, Number) of
	{Fname, FilePosition} ->
	    FileName = lists:concat([Dir, Fname]),
	    case file:open(FileName, [read]) of
		{ok, Fd} ->
		    filter_rep(Filters, Fd, FilePosition, Device, Abort, Log);
		_ ->
		    io:format("rb: can't open file ~tp~n", [Fname]),
		    {proceed,Device}
	    end;
	no_report ->
	    {proceed,Device}
    end.

filter_rep({Filters, FDates}, Fd, FilePosition, Device, Abort, Log) ->
    RepMsg = read_rep_msg(Fd, FilePosition),
    case RepMsg of
	{_DateStr, {Date, _Msg}} ->
	    case compare_dates(Date, FDates) of
		true ->
		    print_filter_report(RepMsg, Filters, Device, Abort, Log);
		_ ->
		    {proceed,Device}
	    end;
	_ ->
	    io:format("rb: Cannot read from file~n"),
	    {proceed,Device}
    end;
filter_rep(Filters, Fd, FilePosition, Device, Abort, Log) ->
    RepMsg = read_rep_msg(Fd, FilePosition),
    case RepMsg of
	{Date, Msg} ->
	    print_filter_report({Date, Msg}, Filters, Device, Abort, Log);
	_ ->
	    io:format("rb: Cannot read from file~n"),
	    {proceed,Device}
    end.

filter_report([], _Msg) ->
    true;
filter_report([{Key, Value}|T], Msg) ->
    case proplists:get_value(Key, Msg) of
	Value ->
	    filter_report(T, Msg);
	_ ->
	    false
    end;
filter_report([{Key, Value, no}|T], Msg) ->
    case proplists:get_value(Key, Msg) of
	Value ->
	    false;
	_ ->
	    filter_report(T, Msg)
    end;
filter_report([{Key, RegExp, re}|T], Msg) ->
    case proplists:get_value(Key, Msg) of
	undefined ->
	    false;
	Value ->
	    Subject = lists:flatten(io_lib:format("~tp",[Value])),
	    case run_re(Subject, RegExp) of
		match ->
		    filter_report(T, Msg);
		_ -> false
	    end
    end;
filter_report([{Key, RegExp, re, no}|T], Msg) ->
    case proplists:get_value(Key, Msg) of
	undefined ->
	    true;
	Value ->
	    Subject = lists:flatten(io_lib:format("~tp",[Value])),
	    case run_re(Subject, RegExp) of
		match -> false;
		_ -> filter_report(T, Msg)
	    end
    end.

get_compare_dates(Date, CompareDate) ->
    case application:get_env(sasl, utc_log) of
	{ok, true} ->
	    {local_time_to_universal_time(Date),
	     local_time_to_universal_time(CompareDate)};
	_ ->
	    {Date, CompareDate}
    end.
get_compare_dates(Date, From, To) ->
    case application:get_env(sasl, utc_log) of
	{ok, true} ->
	    {local_time_to_universal_time(Date),
	     local_time_to_universal_time(From),
	     local_time_to_universal_time(To)};
	_ ->
	    {Date, From, To}
    end.

compare_dates(Date, {CompareDate, from}) ->
    {Date2, DateFrom} = get_compare_dates(Date, CompareDate),
    calendar:datetime_to_gregorian_seconds(Date2) >=
	calendar:datetime_to_gregorian_seconds(DateFrom);
compare_dates(Date, {CompareDate, to}) ->
    {Date2, DateTo} = get_compare_dates(Date, CompareDate),
    calendar:datetime_to_gregorian_seconds(Date2) =<
	calendar:datetime_to_gregorian_seconds(DateTo);
compare_dates(Date, {From, To}) ->
    {Date2, DateFrom, DateTo} = get_compare_dates(Date, From, To),
    calendar:datetime_to_gregorian_seconds(Date2) >=
	calendar:datetime_to_gregorian_seconds(DateFrom)
    andalso
    calendar:datetime_to_gregorian_seconds(Date2) =<
	calendar:datetime_to_gregorian_seconds(DateTo).

print_filter_report({Date, Msg}, Filters, Device, Abort, Log) ->
    {_D, M} = Msg,
    {_, _, M2} = M,
    case M2 of
	{_, _, Report} ->
	    case filter_report(Filters, Report) of
		true ->
		    case catch rb_format_supp:print(Date, Msg, Device) of
			{'EXIT', _} ->
			    handle_bad_form(Date, Msg, Device, Abort, Log);
			_ ->
			    {proceed,Device}
		    end;
		_ ->
		    {proceed, Device}
	    end;
	_ ->
	    {proceed,Device}
    end.

read_rep(Fd, FilePosition, Device, Abort, Log) ->
    case read_rep_msg(Fd, FilePosition) of
	{Date, Msg} ->
	    case catch rb_format_supp:print(Date, Msg, Device) of
		{'EXIT', _} ->
		    handle_bad_form(Date, Msg, Device, Abort, Log);
		_ ->
		    {proceed,Device}
	    end;
	_ -> 
	    io:format("rb: Cannot read from file~n"),
	    {proceed,Device}
    end.
    
handle_bad_form(Date, Msg, Device, Abort, Log) ->
    io:format("rb: ERROR! A report on bad form was encountered. " ++
	      "It cannot be printed to the log.~n~n"),
    io:format("Details:~n~p ~tp~n~n", [Date,Msg]),
    case {Abort,Device,open_log_file(Log)} of
	{true,standard_io,standard_io} ->
	    io:format("rb: Logging aborted.~n"),
	    {abort,Device};
	{false,standard_io,standard_io} ->
	    io:format("rb: Logging resumed...~n~n"),
	    {proceed,Device};
	{_,_,standard_io} ->
	    io:format("rb: Can not reopen ~tp. Logging aborted.~n", [Log]),
	    {abort,Device};
	{true,_,NewDevice} ->
	    io:format(NewDevice,
		      "~n~n************************* RB ERROR ************************~n" ++
		      "A report on bad form was encountered here and the logging~n" ++
		      "process was aborted. Note that there may well be remaining~n" ++
		      "reports that haven't yet been logged. Please see the rb~n" ++
		      "manual for more info.~n" ++
		      "***********************************************************~n", []),
	    io:format("rb: Logging aborted.~n"),
	    {abort,NewDevice};
	{false,_,NewDevice} ->
	    io:format(NewDevice, 
		      "~n   ********* RB: UNPRINTABLE REPORT ********~n~n", []),
	    io:format("rb: Logging resumed...~n~n"),	    
	    {proceed,NewDevice}
    end.

read_rep_msg(Fd, FilePosition) ->
    {ok,_} = file:position(Fd, {bof, FilePosition}),
    Res = 
	case catch read_report(Fd) of
	    {ok, Report} ->
		{_ShortDescr, Date} = get_short_descr(Report),
		{Date, Report};
	    _ -> error
	end,
    ok = file:close(Fd),
    Res.
