src/ecrn_control.erl

%%% @copyright Erlware, LLC. All Rights Reserved.
%%%
%%% This file is provided to you under the BSD License; you may not use
%%% this file except in compliance with the License.
%%%-------------------------------------------------------------------
%%% @doc
%%%  Provides testing/fast forward control for the system
-module(ecrn_control).

-behaviour(gen_server).

%% API
-export([start_link/0,
         cancel/1,
         datetime/0,
         datetime/1,
         ref_datetime/0,
         ref_datetime/1,
         set_datetime/1,
         set_datetime/2,
         reset_datetime/0,
         multi_set_datetime/2]).

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

-define(SERVER, ?MODULE).

-include("internal.hrl").

-record(state, {ref_time :: calendar:datetime(),
                epoch_at_ref :: erlcron:seconds()}).

%%%===================================================================
%%% API
%%%===================================================================

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

-spec cancel(erlcron:job_ref()) -> boolean().
cancel(AlarmRef) ->
    ecrn_reg:cancel(AlarmRef).

%% @doc
%% Returns current datetime with reference adjustment in universal time zone.
-spec datetime() -> {calendar:datetime(), erlcron:milliseconds()}.
datetime() ->
    gen_server:call(?SERVER, datetime).

%% @doc
%% Returns current datetime with reference adjustment in universal/local time zone.
-spec datetime(local|universal) -> {calendar:datetime(), erlcron:milliseconds()}.
datetime(universal) ->
    datetime();
datetime(local) ->
    {DT, Epoch} = datetime(),
    {erlang:universaltime_to_localtime(DT), Epoch}.

%% @doc Returns reference datetime in universal time zone.
-spec ref_datetime() -> {calendar:datetime(), erlcron:milliseconds()}.
ref_datetime() ->
    gen_server:call(?SERVER, ref_datetime).

%% @doc Returns reference datetime in universal or local time zone.
-spec ref_datetime(local|universal) -> {calendar:datetime(), erlcron:milliseconds()}.
ref_datetime(universal) ->
    ref_datetime();
ref_datetime(local) ->
    {DT, Epoch} = ref_datetime(),
    {erlang:universaltime_to_localtime(DT), Epoch}.

%% @doc sets the date-time for the erlcron
-spec set_datetime(calendar:datetime()) -> ok | {error, term()}.
set_datetime(DateTime) ->
    set_datetime(DateTime, local).

-spec set_datetime(calendar:datetime(), local|universal) -> ok | {error, term()}.
set_datetime(DT={_,_}, local) ->
    gen_server:call(?SERVER, {set_datetime, erlang:localtime_to_universaltime(DT)}, infinity);
set_datetime(DateTime={_,_}, universal) ->
    gen_server:call(?SERVER, {set_datetime, DateTime}, infinity).

%% @doc Reset reference datetime to current epoch datetime
-spec reset_datetime() -> ok | {error, term()}.
reset_datetime() ->
    gen_server:call(?SERVER, reset_datetime, infinity).

%% @doc sets the date-time with the erlcron on all nodes
-spec multi_set_datetime([node()], calendar:datetime()) -> {Replies, BadNodes} when
    Replies :: [{node(), ok | {error, term()}}],
    BadNodes :: [node()].
multi_set_datetime(Nodes, DateTime={_,_}) ->
    gen_server:multi_call(Nodes, ?SERVER, {set_datetime, DateTime}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%% @private
init([]) ->
    DateTime = erlang:universaltime(),
    {ok, #state{ref_time=DateTime,
                epoch_at_ref=ecrn_util:epoch_milliseconds()}}.

%% @private
handle_call(datetime, _From, State = #state{ref_time = DateTime,
                                            epoch_at_ref = Actual}) ->
    DT     = erlang:universaltime_to_posixtime(DateTime),
    Now    = ecrn_util:epoch_milliseconds(),
    Diff   = Now - Actual,
    DiffS  = to_seconds(Diff),
    RefNow = DT   + DiffS,
    Msecs  = Diff - DiffS*1000,
    NowDT  = erlang:posixtime_to_universaltime(RefNow),
    {reply, {NowDT, RefNow*1000 + Msecs}, State};
handle_call(ref_datetime, _From, State = #state{ref_time = DateTime,
                                                epoch_at_ref = Actual}) ->
    {reply, {DateTime, Actual}, State};
handle_call({set_datetime, DateTime}, _From, State) ->
    NewState = State#state{ref_time=DateTime,
                           epoch_at_ref=ecrn_util:epoch_milliseconds()},
    {reply, call_all(NewState), NewState};

handle_call(reset_datetime, _From, State) ->
    Now      = ecrn_util:epoch_milliseconds(),
    DateTime = erlang:posixtime_to_universaltime(to_seconds(Now)),
    NewState = State#state{ref_time=DateTime, epoch_at_ref=Now},
    {reply, call_all(NewState), NewState}.

%% @private
handle_cast(_Msg, State) ->
    {noreply, State}.

%% @private
handle_info(_Info, State) ->
    {noreply, State}.

%% @private
terminate(_Reason, _State) ->
    ok.

%% @private
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
%notify_all(#state{ref_time=DateTime, epoch_at_ref=Now}=State) ->
%    [ecrn_agent:set_datetime(P, DateTime, Now, universal) || P <- ecrn_reg:get_all_pids()],
%    State.

call_all(#state{ref_time=DateTime, epoch_at_ref=Now}) ->
    Res = lists:foldl(fun(P, A) ->
        case ecrn_agent:set_datetime(P, DateTime, Now, universal) of
            ok  ->
                A;
            {error, Err} ->
                Ref = try
                          ecrn_reg:get_refs(P)
                      catch _:_ ->
                          []
                      end,
                [{Ref, P, Err} | A]
        end
    end, [], ecrn_reg:get_all_pids()),
    case Res of
        [] -> ok;
        _  -> {error, {failed_to_set_time, Res}}
    end.

to_seconds(MilliSec) ->
    MilliSec div 1000.