/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   Written 2020-2024 by Markus Triska (triska@metalevel.at)
   Part of Scryer Prolog.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

/** This library provides predicates for reasoning about time.
*/

:- module(time, [max_sleep_time/1,
                 sleep/1,
                 time/1,
                 current_time/1,
                 format_time//2,
                 statistics/2
                ]).

:- use_module(library(format)).
:- use_module(library(iso_ext)).
:- use_module(library(error)).
:- use_module(library(dcgs)).
:- use_module(library(lists)).
:- use_module(library(charsio), [read_from_chars/2]).


%% current_time(-T)
%
%  Yields the current system time _T_ in an opaque form, called a
%  _time stamp_. Use `format_time//2` to describe strings that contain
%  attributes of the time stamp.

current_time(T) :-
        '$current_time'(T0),
        read_from_chars(T0, T).

%% format_time(FormatString, TimeStamp)//
%
% The nonterminal format_time//2 describes a list of characters that
% are formatted according to a format string. Usage:
%
% ```
%    phrase(format_time(FormatString, TimeStamp), Cs)
% ```
%
% TimeStamp represents a moment in time in an opaque form, as for
% example obtained by `current_time/1`.
%
% FormatString is a list of characters that are interpreted literally,
% except for the following specifiers (and possibly more in the future):
%
% |  `%Y` |  year of the time stamp. Example: 2020.                |
% |  `%m` |  month number (01-12), zero-padded to 2 digits         |
% |  `%d` |  day number (01-31), zero-padded to 2 digits           |
% |  `%H` |  hour number (00-24), zero-padded to 2 digits          |
% |  `%M` |  minute number (00-59), zero-padded to 2 digits        |
% |  `%S` |  second number (00-60), zero-padded to 2 digits        |
% |  `%b` |  abbreviated month name, always 3 letters              |
% |  `%a` |  abbreviated weekday name, always 3 letters            |
% |  `%A` |  full weekday name                                     |
% |  `%j` |  day of the year (001-366), zero-padded to 3 digits    |
% |  `%%` |  the literal `%`                                       |
%
% Example:
%
% ```
%    ?- current_time(T), phrase(format_time("%d.%m.%Y (%H:%M:%S)", T), Cs).
%       T = [...], Cs = "11.06.2020 (00:24:32)".
% ```

format_time([], _) --> [].
format_time(['%','%'|Fs], T) --> !, "%", format_time(Fs, T).
format_time(['%',Spec|Fs], T) --> !,
        (   { member(Spec=Value, T) } ->
            seq(Value)
        ;   { domain_error(time_specifier, Spec, format_time//2) }
        ),
        format_time(Fs, T).
format_time([F|Fs], T) --> [F], format_time(Fs, T).

%% max_sleep_time(T)
%
%  The maximum admissible time span for `sleep/1`.

max_sleep_time(0xfffffffffffffbff).


%% sleep(S)
%
%  Sleeps for S seconds (a floating point number or integer).

sleep(T) :-
    builtins:must_be_number(T, sleep),
    (   T < 0 ->
        domain_error(not_less_than_zero, T, sleep/1)
    ;   max_sleep_time(N), T > N ->
        throw(error(representation_error(max_sleep_time), sleep/1))
    ;   '$sleep'(T)
    ).


%% statistics(?Keyword, ?List)
%
%  Preliminary support for statistics/2, yielding timing information.
%  The only supported `Keyword` is `runtime`. The first element of
%  `List` is the CPU time in milliseconds, the second element is
%  currently not supported.

statistics(runtime, [T,unsupported]) :-
        '$cpu_now'(T0),
        T is T0*1000.

:- meta_predicate(time(0)).

:- dynamic(time_id/1).
:- dynamic(time_state/3).

time_next_id(N) :-
        (   retract(time_id(N0)) ->
            N is N0 + 1
        ;   N = 0
        ),
        asserta(time_id(N)).


%% time(Goal)
%
%  Reports the execution time of Goal.

time(Goal) :-
        cputime_inferences(T0, I0),
        time_next_id(ID),
        setup_call_cleanup(asserta(time_state(ID, T0, I0)),
                           (   call_cleanup(catch(Goal, E, (report_time(ID),throw(E))),
                                            Det = true),
                               time_true(ID),
                               (   Det == true -> !
                               ;   true
                               )
                           ;   report_time(ID),
                               false
                           ),
                           retract(time_state(ID, _, _))).

cputime_inferences(T, I) :-
        '$cpu_now'(T),
        '$inference_count'(I).

time_true(ID) :-
        report_time(ID).
time_true(ID)  :-
        % on backtracking, update the stored CPU time for this ID
        retract(time_state(ID, _, _)),
        cputime_inferences(T0, I0),
        asserta(time_state(ID, T0, I0)),
        false.

report_time(ID) :-
        time_state(ID, T0, I0),
        cputime_inferences(T, I),
        Time is T - T0,
        Inferences0 is I - I0,
        % we must subtract the number of inferences that time/1 itself takes;
        % this may have to be adapted if the implementation changes,
        % so that (for example) true/1 takes exactly 1 inference.
        (   bb_get('$answer_count', 0) ->
            Inferences is Inferences0 - 60,
            Pre = "   ", Post = ""
        ;   Inferences is Inferences0 - 9,
            Pre = "", Post = "   "
        ),
        phrase((Pre,"% CPU time: ", format_("~3f", [Time]), "s, ",
                format_("~U", [Inferences])," inference",s_if_necessary(Inferences),"\n",
                Post), Cs),
        format("~s", [Cs]).

s_if_necessary(Inferences) -->
        { compare(C, 1, Inferences) },
        s_(C).

s_(=) --> "".
s_(<) --> "s".
s_(>) --> " (exception?)".

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
?- time((true;false)).
   % CPU time: 0.000s, 1 inference
   true
;  % CPU time: 0.000s, 0 inference (exception?)
   false.

:- time(use_module(library(clpz))).
   % CPU time: 0.343s, 409_874 inferences
   true.

:- time(use_module(library(lists))).
   % CPU time: 0.000s, 19 inferences
   true.

?- time(member(X, "abc")).
   % CPU time: 0.000s, 1 inference
   X = a
;  % CPU time: 0.000s, 3 inferences
   X = b
;  % CPU time: 0.000s, 3 inferences
   X = c.

?- time((repeat,false)).
   % CPU time: 2.726s, 53_330_502 inferences
   error('$interrupt_thrown',repl/0).
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
