%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2021. 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(timer).

-export([apply_after/4,
	 send_after/3, send_after/2,
	 exit_after/3, exit_after/2, kill_after/2, kill_after/1,
	 apply_interval/4, send_interval/3, send_interval/2,
	 cancel/1, sleep/1, tc/1, tc/2, tc/3, now_diff/2,
	 seconds/1, minutes/1, hours/1, hms/3]).

-export([start_link/0, start/0,
	 handle_call/3,  handle_info/2,
	 init/1,
	 code_change/3, handle_cast/2, terminate/2]).

%% Types which can be used by other modules
-export_type([tref/0]).

%% Max value for a receive's after clause.
-define(MAX_RECEIVE_AFTER, 16#ffffffff).

%% Validations
-define(valid_time(T), is_integer(T), T >= 0).
-define(valid_mfa(M, F, A), is_atom(M), is_atom(F), is_list(A)).

%%
%% Time is in milliseconds.
%%
-opaque tref()    :: {type(), reference()}.
-type type()      :: 'once' | 'interval' | 'instant' | 'send_local'.
-type time()      :: non_neg_integer().

%%
%% Interface functions
%%
-spec apply_after(Time, Module, Function, Arguments) ->
          {'ok', TRef} | {'error', Reason}
              when Time :: time(),
                   Module :: module(),
                   Function :: atom(),
                   Arguments :: [term()],
                   TRef :: tref(),
                   Reason :: term().
apply_after(0, M, F, A)
  when ?valid_mfa(M, F, A) ->
    do_apply({M, F, A}),
    {ok, {instant, make_ref()}};
apply_after(Time, M, F, A)
  when ?valid_time(Time),
       ?valid_mfa(M, F, A) ->
    req(apply_once, {system_time(), Time, {M, F, A}});
apply_after(_Time, _M, _F, _A) ->
    {error, badarg}.

-spec send_after(Time, Destination, Message) -> {'ok', TRef} | {'error', Reason}
              when Time :: time(),
                   Destination :: pid() | (RegName :: atom()) | {RegName :: atom(), Node :: node()},
                   Message :: term(),
                   TRef :: tref(),
                   Reason :: term().
send_after(0, PidOrRegName, Message)
  when is_pid(PidOrRegName);
       is_atom(PidOrRegName) ->
    PidOrRegName ! Message,
    {ok, {instant, make_ref()}};
send_after(0, {RegName, Node} = Dest, Message)
  when is_atom(RegName),
       is_atom(Node) ->
    Dest ! Message,
    {ok, {instant, make_ref()}};
send_after(Time, Pid, Message)
  when ?valid_time(Time),
       is_pid(Pid),
       node(Pid) =:= node() ->
    TRef = erlang:send_after(Time, Pid, Message),
    {ok, {send_local, TRef}};
send_after(Time, Pid, Message)
  when is_pid(Pid) ->
    apply_after(Time, ?MODULE, send, [Pid, Message]);
send_after(Time, RegName, Message)
  when is_atom(RegName) ->
    apply_after(Time, ?MODULE, send, [RegName, Message]);
send_after(Time, {RegName, Node} = Dest, Message)
  when is_atom(RegName),
       is_atom(Node) ->
    apply_after(Time, ?MODULE, send, [Dest, Message]);
send_after(_Time, _PidOrRegName, _Message) ->
    {error, badarg}.

-spec send_after(Time, Message) -> {'ok', TRef} | {'error', Reason}
              when Time :: time(),
                   Message :: term(),
                   TRef :: tref(),
                   Reason :: term().
send_after(Time, Message) ->
    send_after(Time, self(), Message).

-spec exit_after(Time, Target, Reason1) -> {'ok', TRef} | {'error', Reason2}
              when Time :: time(),
                   Target :: pid() | (RegName :: atom()),
                   TRef :: tref(),
                   Reason1 :: term(),
                   Reason2 :: term().
exit_after(Time, Pid, Reason) ->
    apply_after(Time, erlang, exit, [Pid, Reason]).

-spec exit_after(Time, Reason1) -> {'ok', TRef} | {'error', Reason2}
              when Time :: time(),
                   TRef :: tref(),
                   Reason1 :: term(),
                   Reason2 :: term().
exit_after(Time, Reason) ->
    exit_after(Time, self(), Reason).

-spec kill_after(Time, Target) -> {'ok', TRef} | {'error', Reason2}
              when Time :: time(),
                   Target :: pid() | (RegName :: atom()),
                   TRef :: tref(),
                   Reason2 :: term().
kill_after(Time, Pid) ->
    exit_after(Time, Pid, kill).

-spec kill_after(Time) -> {'ok', TRef} | {'error', Reason2}
              when Time :: time(),
                   TRef :: tref(),
                   Reason2 :: term().
kill_after(Time) ->
    exit_after(Time, self(), kill).

-spec apply_interval(Time, Module, Function, Arguments) ->
          {'ok', TRef} | {'error', Reason}
              when Time :: time(),
                   Module :: module(),
                   Function :: atom(),
                   Arguments :: [term()],
                   TRef :: tref(),
                   Reason :: term().
apply_interval(Time, M, F, A)
  when ?valid_time(Time),
       ?valid_mfa(M, F, A) ->
    req(apply_interval, {system_time(), Time, self(), {M, F, A}});
apply_interval(_Time, _M, _F, _A) ->
    {error, badarg}.

-spec send_interval(Time, Destination, Message) -> {'ok', TRef} | {'error', Reason}
              when Time :: time(),
                   Destination :: pid() | (RegName :: atom()) | {RegName :: atom(), Node :: node()},
                   Message :: term(),
                   TRef :: tref(),
                   Reason :: term().
send_interval(Time, Pid, Message)
  when ?valid_time(Time),
       is_pid(Pid) ->
    req(apply_interval, {system_time(), Time, Pid, {?MODULE, send, [Pid, Message]}});
send_interval(Time, RegName, Message)
  when ?valid_time(Time),
       is_atom(RegName) ->
    req(apply_interval, {system_time(), Time, RegName, {?MODULE, send, [RegName, Message]}});
send_interval(Time, Dest = {RegName, Node}, Message)
  when ?valid_time(Time),
       is_atom(RegName),
       is_atom(Node) ->
    req(apply_interval, {system_time(), Time, Dest, {?MODULE, send, [Dest, Message]}});
send_interval(_Time, _Pid, _Message) ->
    {error, badarg}.

-spec send_interval(Time, Message) -> {'ok', TRef} | {'error', Reason}
              when Time :: time(),
                   Message :: term(),
                   TRef :: tref(),
                   Reason :: term().
send_interval(Time, Message) ->
    send_interval(Time, self(), Message).

-spec cancel(TRef) -> {'ok', 'cancel'} | {'error', Reason}
              when TRef :: tref(),
                   Reason :: term().
cancel({instant, Ref})
  when is_reference(Ref) ->
    {ok, cancel};
cancel({send_local, Ref})
  when is_reference(Ref) ->
    _ = erlang:cancel_timer(Ref),
    {ok, cancel};
cancel({once, Ref} = TRef)
  when is_reference(Ref) ->
    req(cancel, TRef);
cancel({interval, Ref} = TRef)
  when is_reference(Ref) ->
    req(cancel, TRef);
cancel(_TRef) ->
    {error, badarg}.

-spec sleep(Time) -> 'ok'
              when Time :: timeout().
sleep(T)
  when is_integer(T),
       T > ?MAX_RECEIVE_AFTER ->
    receive
    after ?MAX_RECEIVE_AFTER ->
            sleep(T - ?MAX_RECEIVE_AFTER)
    end;
sleep(T) ->
    receive
    after T -> ok
    end.

%%
%% Measure the execution time (in microseconds) for Fun().
%%
-spec tc(Fun) -> {Time, Value}
              when Fun :: function(),
                   Time :: integer(),
                   Value :: term().
tc(F) ->
    T1 = erlang:monotonic_time(),
    Val = F(),
    T2 = erlang:monotonic_time(),
    Time = erlang:convert_time_unit(T2 - T1, native, microsecond),
    {Time, Val}.

%%
%% Measure the execution time (in microseconds) for Fun(Args).
%%
-spec tc(Fun, Arguments) -> {Time, Value}
              when Fun :: function(),
                   Arguments :: [term()],
                   Time :: integer(),
                   Value :: term().
tc(F, A) ->
    T1 = erlang:monotonic_time(),
    Val = apply(F, A),
    T2 = erlang:monotonic_time(),
    Time = erlang:convert_time_unit(T2 - T1, native, microsecond),
    {Time, Val}.

%%
%% Measure the execution time (in microseconds) for an MFA.
%%
-spec tc(Module, Function, Arguments) -> {Time, Value}
              when Module :: module(),
                   Function :: atom(),
                   Arguments :: [term()],
                   Time :: integer(),
                   Value :: term().
tc(M, F, A) ->
    T1 = erlang:monotonic_time(),
    Val = apply(M, F, A),
    T2 = erlang:monotonic_time(),
    Time = erlang:convert_time_unit(T2 - T1, native, microsecond),
    {Time, Val}.

%%
%% Calculate the time difference (in microseconds) of two
%% erlang:now() timestamps, T2-T1.
%%
-spec now_diff(T2, T1) -> Tdiff
              when T1 :: erlang:timestamp(),
                   T2 :: erlang:timestamp(),
                   Tdiff :: integer().
now_diff({A2, B2, C2}, {A1, B1, C1}) ->
    ((A2-A1)*1000000 + B2-B1)*1000000 + C2-C1.

%%
%% Convert seconds, minutes etc. to milliseconds.
%%
-spec seconds(Seconds) -> MilliSeconds
              when Seconds :: non_neg_integer(),
                   MilliSeconds :: non_neg_integer().
seconds(Seconds) ->
    1000*Seconds.

-spec minutes(Minutes) -> MilliSeconds
              when Minutes :: non_neg_integer(),
                   MilliSeconds :: non_neg_integer().
minutes(Minutes) ->
    1000*60*Minutes.

-spec hours(Hours) -> MilliSeconds
              when Hours :: non_neg_integer(),
                   MilliSeconds :: non_neg_integer().
hours(Hours) ->
    1000*60*60*Hours.

-spec hms(Hours, Minutes, Seconds) -> MilliSeconds
              when Hours :: non_neg_integer(),
                   Minutes :: non_neg_integer(),
                   Seconds :: non_neg_integer(),
                   MilliSeconds :: non_neg_integer().
hms(H, M, S) ->
    hours(H) + minutes(M) + seconds(S).

%%
%%   Start/init functions
%%

-spec start() -> 'ok'.
start() ->
    {ok, _Pid} = do_start(),
    ok.

do_start() ->
    case
	supervisor:start_child(
          kernel_sup,
          #{
            id => timer_server,
            start => {?MODULE, start_link, []},
            restart => permanent,
            shutdown => 1000,
            type => worker,
            modules => [?MODULE]
           }
         )
    of
	{ok, Pid} ->
	    {ok, Pid};
	{ok, Pid, _} ->
	    {ok, Pid};
	{error, {already_started, Pid}} ->
	    {ok, Pid};
	Error ->
	    Error
    end.

-spec start_link() -> {'ok', pid()} | {'error', term()}.
start_link() ->
    gen_server:start_link({local, timer_server}, ?MODULE, [], []).

-spec init([]) -> {'ok', ets:tid()}.
init([]) ->
    process_flag(trap_exit, true),
    Tab = ets:new(?MODULE, []),
    {ok, Tab}.

%% server calls

%% Try sending a call. If it fails with reason noproc,
%% try starting the timer server and try once again.
req(Req, Arg) ->
    try
	maybe_req(Req, Arg)
    catch
	exit:{noproc, _} ->
	    {ok, _Pid} = do_start(),
	    maybe_req(Req, Arg)
    end.

maybe_req(Req, Arg) ->
    gen_server:call(timer_server, {Req, Arg}, infinity).

%% Call handling.
-spec handle_call(term(), term(), Tab) ->
          {'reply', term(), Tab} | {'noreply', Tab} when
      Tab :: ets:tid().
%% Start a one-shot timer.
handle_call({apply_once, {Started, Time, MFA}}, _From, Tab) ->
    Timeout = Started + Time,
    Reply = try
                erlang:start_timer(
                  Timeout,
                  self(),
                  {apply_once, MFA},
                  [{abs, true}]
                 )
            of
                SRef ->
                    ets:insert(Tab, {SRef, SRef}),
                    {ok, {once, SRef}}
            catch
                error:badarg ->
                    {error, badarg}
            end,
    {reply, Reply, Tab};
%% Start an interval timer.
handle_call({apply_interval, {Started, Time, Pid, MFA}}, _From, Tab) ->
    NextTimeout = Started + Time,
    TRef = monitor(process, Pid),
    Reply = try
                erlang:start_timer(
                  NextTimeout,
                  self(),
                  {apply_interval, NextTimeout, Time, TRef, MFA},
                  [{abs, true}]
                 )
            of
                SRef ->
                    ets:insert(Tab, {TRef, SRef}),
                    {ok, {interval, TRef}}
            catch
                error:badarg ->
                    demonitor(TRef, [flush]),
                    {error, badarg}
            end,
    {reply, Reply, Tab};
%% Cancel a one-shot timer.
handle_call({cancel, {once, TRef}}, _From, Tab) ->
    _ = remove_timer(TRef, Tab),
    {reply, {ok, cancel}, Tab};
%% Cancel an interval timer.
handle_call({cancel, {interval, TRef}}, _From, Tab) ->
    _ = case remove_timer(TRef, Tab) of
            true ->
                demonitor(TRef, [flush]);
            false ->
                ok
        end,
    {reply, {ok, cancel}, Tab};
%% Unexpected.
handle_call(_Req, _From, Tab) ->
    {noreply, Tab}.

%% Info handling.
-spec handle_info(term(), Tab) -> {'noreply', Tab}
              when Tab :: ets:tid().
%% One-shot timer timeout.
handle_info({timeout, TRef, {apply_once, MFA}}, Tab) ->
    case ets:take(Tab, TRef) of
        [{TRef, _SRef}] ->
            do_apply(MFA);
        [] ->
            ok
    end,
    {noreply, Tab};
%% Interval timer timeout.
handle_info({timeout, _, {apply_interval, CurTimeout, Time, TRef, MFA}}, Tab) ->
    case ets:member(Tab, TRef) of
	true ->
	    NextTimeout = CurTimeout + Time,
	    SRef = erlang:start_timer(
                     NextTimeout,
                     self(),
                     {apply_interval, NextTimeout, Time, TRef, MFA},
                     [{abs, true}]
                    ),
	    ets:update_element(Tab, TRef, {2, SRef}),
	    do_apply(MFA);
	false ->
	    ok
    end,
    {noreply, Tab};
%% A process related to an interval timer died.
handle_info({'DOWN', TRef, process, _Pid, _Reason}, Tab) ->
    _ = remove_timer(TRef, Tab),
    {noreply, Tab};
%% Unexpected.
handle_info(_Req, Tab) ->
    {noreply, Tab}.

%% Cast handling.
-spec handle_cast(term(), Tab) -> {'noreply', Tab}
              when Tab :: ets:tid().
%% Unexpected.
handle_cast(_Req, Tab) ->
    {noreply, Tab}.

-spec terminate(term(), _Tab) -> 'ok'.
terminate(_Reason, _Tab) ->
    ok.

-spec code_change(term(), State, term()) -> {'ok', State}.
code_change(_OldVsn, Tab, _Extra) ->
    %% According to the man for gen server no timer can be set here.
    {ok, Tab}.

%% Remove a timer.
remove_timer(TRef, Tab) ->
    case ets:take(Tab, TRef) of
	[{TRef, SRef}] ->
	    ok = erlang:cancel_timer(SRef, [{async, true}, {info, false}]),
	    true;
	[] -> % TimerReference does not exist, do nothing
	    false
    end.

%% Help functions

%% If send op. send directly (faster than spawn)
do_apply({?MODULE, send, A}) ->
    catch send(A);
%% If exit op. resolve registered name
do_apply({erlang, exit, [Name, Reason]}) ->
    catch exit(get_pid(Name), Reason);
do_apply({M,F,A}) ->
    catch spawn(M, F, A).

%% Get current time in milliseconds,
%% ceil'ed to the next millisecond.
system_time() ->
    (erlang:monotonic_time(microsecond) + 999) div 1000.

send([Pid, Msg]) ->
    Pid ! Msg.

%% Resolve a registered name.
get_pid(Name) when is_pid(Name) ->
    Name;
get_pid(undefined) ->
    undefined;
get_pid(Name) when is_atom(Name) ->
    get_pid(whereis(Name));
get_pid(_) ->
    undefined.
