%%% @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.