%% @doc Clojure Agent.
-module('clojerl.Agent').

-include("clojerl.hrl").
-include("clojerl_int.hrl").

-behaviour(gen_server).
-behavior('erlang.io.ICloseable').
-behavior('clojerl.IDeref').
-behavior('clojerl.IEquiv').
-behavior('clojerl.IHash').
-behavior('clojerl.IMeta').
-behavior('clojerl.IReference').
-behavior('clojerl.IStringable').

%% API
-export([?CONSTRUCTOR/1]).

-export([ dispatch/3
        , error/1
        , error_handler/1
        , error_handler/2
        , error_mode/1
        , error_mode/2
        , release_pending_sends/0
        , restart/3
        , validator/1
        , validator/2
        ]).

  Protocols
-export([close/1]).
-export([deref/1]).
-export([equiv/2]).
-export([hash/1]).
-export([ meta/1
        , with_meta/2
        ]).
-export([ alter_meta/3
        , reset_meta/2
        ]).
-export([str/1]).

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

-define(PENDING_SENDS,  '__pending_sends__').

-export_type([type/0]).
-type type()        :: #{ ?TYPE => ?M
                        , id    => binary()
                        , pid   => pid()
                        }.

-type error_mode()  :: continue | fail.

-type agent_state() :: #{ value         => any()
                        , error_handler => any()
                        , error_mode    => error_mode()
                        , error         => any()
                        , validator     => any()
                        , meta          => any()
                        }.

-type state()       :: #{ id      => binary()
                        , owner   => pid()
                        , monitor => reference()
                        , queued  => [{Fn :: any(), Args :: any()}]
                        }.

%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------

  @private
-spec ?CONSTRUCTOR(any()) -> type().
?CONSTRUCTOR(Value) ->
  UUID      = 'erlang.util.UUID':random(),
  Id        = 'erlang.util.UUID':str(UUID),
  {ok, Pid} = 'clojerl.Agent.Server':create(erlang:self(), Id, Value),
  build(Id, Pid).

%% @doc Dispatch an action to an agent.
-spec dispatch(type(), any(), any()) -> type().
dispatch(#{?TYPE := ?M, id :=  Id, pid := Pid} = Agent, Fun, Args) ->
  case is_running_action() of
    true ->
      add_pending_send(Fun, Args),
      Agent;
    false ->
      case field(Id, error) of
        ?NIL ->
          ok = gen_server:cast(Pid, {send, Fun, Args}),
          Agent;
        Error -> erlang:error(Error)
      end
  end.

%% @doc Returns the error thrown during an asynchronous action of the
%% agent if the agent is failed. Returns `undefined' if the agent is
%% not failed.
-spec error(type()) -> any().
error(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, error).

%% @doc Returns the error-handler of `Agent', or `undefined' if there
%% is none.
-spec error_handler(type()) -> function().
error_handler(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, error_handler).

  @doc Sets the error - handler of ` Agent ' a to ` ErrorHandler ' .
-spec error_handler(type(), function()) -> any().
error_handler(#{?TYPE := ?M, pid := Pid} = _Agent, ErrorHandler) ->
  field(Pid, error_handler, ErrorHandler).

%% @doc Returns the error-mode of `Agent'.
-spec error_mode(type()) -> atom().
error_mode(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, error_mode).

  @doc Sets the error - mode of ` Agent ' a to ` ErrorMode ' , which must be
%% either `fail' or `continue'.
-spec error_mode(type(), fail | continue) -> any().
error_mode(#{?TYPE := ?M, pid := Pid} = _Agent, ErrorMode) ->
  field(Pid, error_mode, ErrorMode).

%% @doc Normally, actions sent directly or indirectly during another
%% action are held until the action completes (changes the agent's
%% state). This function can be used to dispatch any pending sent
%% actions immediately. If no action is occurring, does nothing.
%% Returns the number of actions dispatched.
-spec release_pending_sends() -> non_neg_integer().
release_pending_sends() ->
  Pid = erlang:self(),
  case erlang:get(?PENDING_SENDS) of
    ?NIL -> 0;
    Pending ->
      [ gen_server:cast(Pid, {send, Fun, Args})
        || {Fun, Args} <- lists:reverse(Pending)
      ],
      length(Pending)
  end.

%% @doc When an agent is failed, changes the agent state to new-state
  and then un - fails the agent so that sends are allowed again .
-spec restart(type(), any(), boolean()) -> any().
restart( #{?TYPE := ?M, id := Id, pid := Pid} = _Agent
       , NewValue
       , ClearActions
       ) ->
  case field(Id, error) =:= ?NIL of
    true -> ?ERROR(<<"Agent does not need a restart">>);
    false ->
      case gen_server:call(Pid, {restart, NewValue, ClearActions}) of
        ok -> NewValue;
        {error, Error} -> erlang:error(Error)
      end
  end.

%% @doc Gets the validator function for the `Agent'.
-spec validator(type()) -> any().
validator(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, validator).

%% @doc Sets the validator function for the `Agent'.
-spec validator(type(), function()) -> any().
validator(#{?TYPE := ?M, pid := Pid} = _Agent, Validator) ->
  field(Pid, validator, Validator).

%%------------------------------------------------------------------------------
  Protocols
%%------------------------------------------------------------------------------

%% @see 'clojerl.ICloseable'
  @private
close(#{?TYPE := ?M, pid := Pid}) ->
  ok = gen_server:stop(Pid),
  ?NIL.

  @see ' clojerl . IDeref '
  @private
deref(#{?TYPE := ?M, id := Id}) ->
  field(Id, value).

%% @see 'clojerl.IEquiv'
  @private
equiv( #{?TYPE := ?M, id := Id1}
     , #{?TYPE := ?M, id := Id2}
     ) ->
  Id1 =:= Id2;
equiv(_, _) ->
  false.

  @see ' clojerl . IHash '
  @private
hash(#{?TYPE := ?M, id := Id}) ->
  erlang:phash2(Id).

  @see ' clojerl . IMeta '
  @private
meta(#{?TYPE := ?M, id := Id}) ->
  field(Id, meta).

  @see ' clojerl . IMeta '
  @private
with_meta(#{?TYPE := ?M, pid := Pid} = Agent, Meta) ->
  field(Pid, meta, Meta),
  Agent.

%% @see 'clojerl.IReference'
  @private
-spec alter_meta(type(), any(), any()) -> any().
alter_meta(#{?TYPE := ?M, pid := Pid}, Fun, Args) ->
  case gen_server:call(Pid, {alter_meta, Fun, Args}) of
    {ok, Meta} -> Meta;
    {error, {Class, Reason}} -> erlang:raise(Class, Reason, [])
  end.

%% @see 'clojerl.IReference'
  @private
-spec reset_meta(type(), any()) -> any().
reset_meta(#{?TYPE := ?M, pid := Pid}, Meta) ->
  field(Pid, meta, Meta).

%% @see 'clojerl.IStringable'
  @private
str(#{?TYPE := ?M, id := Id}) ->
  <<"#<clojerl.Agent ", Id/binary, ">">>.

%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------

  @private
-spec start_link(pid(), binary(), any()) -> {ok, pid()} | {error, any()}.
start_link(Owner, Id, Value) ->
  gen_server:start_link(?MODULE, {Owner, Id, Value}, []).

  @private
-spec init({pid(), binary(), any()}) -> {ok, state()}.
init({Owner, Id, Value}) ->
  AgentState = default_state(Value),
  _          = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState}),
  Ref        = erlang:monitor(process, Owner),
  State      = #{ id      => Id
                , owner   => Owner
                , monitor => Ref
                , queued  => []
                },
  {ok, State}.

  @private
-spec handle_call({restart, any(), boolean()}, any(), state()) ->
  {reply, any(), state()}.
handle_call({restart, NewValue, ClearActions}, _From, #{id := Id} = State0) ->
  {_, AgentState0} = clj_utils:ets_get(?AGENT_TABLE, Id),
  Reply = try validate(AgentState0, NewValue) of
            AgentState1 ->
              X = {Id, AgentState1#{error => ?NIL}},
              _ = clj_utils:ets_save(?AGENT_TABLE, X),
              ok
          catch _:E -> {error, E}
          end,
  %% Handle queued actions
  State1 = case Reply of
             ok when ClearActions ->
               State0#{queued => []};
             ok ->
               Queued = maps:get(queued, State0),
               %% Re-cast queued actions
               [ gen_server:cast(erlang:self(), {send, Fn, Args})
                 || {Fn, Args} <- lists:reverse(Queued)
               ],
               State0#{queued => []};
             _ ->
               State0
           end,
  {reply, Reply, State1};
handle_call({set_field, Name, Value}, _From, #{id := Id} = State) ->
  {_, AgentState} = clj_utils:ets_get(?AGENT_TABLE, Id),
  _ = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState#{Name => Value}}),
  {reply, ok, State};
handle_call({alter_meta, Fun, Args}, _From, #{id := Id} = State) ->
  {_, AgentState0}  = clj_utils:ets_get(?AGENT_TABLE, Id),
  #{meta := Meta0} = AgentState0,
  Reply = try
            Meta1 = clj_rt:apply(Fun, clj_rt:cons(Meta0, Args)),
            AgentState1 = AgentState0#{meta => Meta1},
              _ = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState1}),
            {ok, Meta1}
          catch Class:Reason ->
              {error, {Class, Reason}}
          end,
  {reply, Reply, State}.

  @private
-spec handle_cast({send, function(), any()}, state()) ->
  {noreply, state()}.
handle_cast({send, Fun, Args}, #{id := Id} = State0) ->
  {_, AgentState0} = clj_utils:ets_get(?AGENT_TABLE, Id),
  case AgentState0 of
    #{error := ?NIL} ->
      AgentState1 = apply_action(Id, AgentState0, Fun, Args),
      _           = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState1}),
      {noreply, State0};
    _ ->
      Queued = maps:get(queued, State0),
      State1 = State0#{queued => [{Fun, Args} | Queued]},
      {noreply, State1}
  end.

  @private
-spec handle_info(any(), state()) -> {noreply, state()}.
handle_info({'DOWN', Ref, _, _, Reason}, #{monitor := Ref} = State) ->
  {stop, Reason, State};
handle_info(_Msg, State) ->
  {noreply, State}.

  @private
-spec terminate(any(), state()) -> {ok, state()}.
terminate(_Msg, State) ->
  {ok, State}.

  @private
-spec code_change(any(), any(), state()) ->
  {ok, state()}.
code_change(_Msg, _From, State) ->
  {ok, State}.

%%------------------------------------------------------------------------------
  Internal
%%------------------------------------------------------------------------------

-spec default_state(any()) -> agent_state().
default_state(Value) ->
  #{ value         => Value
   , error_handler => ?NIL
   , error_mode    => continue
   , error         => ?NIL
   , validator     => ?NIL
   , meta          => ?NIL
   }.

-spec build(binary(), pid()) -> type().
build(Id, Pid) ->
  #{ ?TYPE => ?M
   , id    => Id
   , pid   => Pid
   }.

-spec init_pending_sends() -> ok.
init_pending_sends() ->
  erlang:put(?PENDING_SENDS, []),
  ok.

-spec clear_pending_sends() -> ok.
clear_pending_sends() ->
  erlang:erase(?PENDING_SENDS),
  ok.

-spec is_running_action() -> boolean().
is_running_action() ->
  clj_rt:boolean(erlang:get(?PENDING_SENDS)).

-spec add_pending_send(any(), any()) -> ok.
add_pending_send(Fun, Args) ->
  Pending = erlang:get(?PENDING_SENDS),
  erlang:put(?PENDING_SENDS, [{Fun, Args} | Pending]).

-spec apply_action(binary(), any(), function(), any()) -> any().
apply_action(Id, #{value := Value0} = AgentState0, Fun, Args) ->
  try
    init_pending_sends(),
    Value1      = clj_rt:apply(Fun, clj_rt:cons(Value0, Args)),
    AgentState1 = validate(AgentState0, Value1),
    'clojerl.Agent':release_pending_sends(),
    AgentState1
  catch
    _:Error ->
      clear_pending_sends(),
      handle_error(Id, AgentState0, Error),
      handle_error_mode(AgentState0, Error)
  after
      clear_pending_sends()
  end.

-spec validate(any(), any()) -> any().
validate(#{validator := ?NIL} = AgentState, Value) ->
  AgentState#{value => Value};
validate(#{validator := Validator} = AgentState, Value) ->
  case clj_rt:apply(Validator, [Value]) of
    true  -> AgentState#{value => Value};
    false -> ?ERROR(validation_failed)
  end.

-spec handle_error(binary(), any(), any()) -> any().
handle_error(_Id, #{error_handler := ?NIL}, _Error) ->
  ok;
handle_error(Id, #{error_handler := ErrorHandler}, Error) ->
  try
    Agent = build(Id, erlang:self()),
    clj_rt:apply(ErrorHandler, [Agent, Error])
  ignore ErrorHandler errors
  end.

-spec handle_error_mode(any(), any()) -> any().
handle_error_mode(#{error_mode := continue} = AgentState, _Error) ->
  AgentState;
handle_error_mode(#{error_mode := fail} = AgentState, Error) ->
  AgentState#{error => Error}.

-spec field(binary(), atom()) -> any().
field(Id, Name) ->
  {_, AgentState} = clj_utils:ets_get(?AGENT_TABLE, Id),
  maps:get(Name, AgentState, ?NIL).

-spec field(pid(), atom(), any()) -> any().
field(Pid, Name, Value) ->
  ok = gen_server:call(Pid, {set_field, Name, Value}),
  Value.

 @doc Clojure Agent.
 API
 gen_server callbacks
------------------------------------------------------------------------------
 API
------------------------------------------------------------------------------
 @doc Dispatch an action to an agent.
 @doc Returns the error thrown during an asynchronous action of the
 agent if the agent is failed. Returns `undefined' if the agent is
 not failed.
 @doc Returns the error-handler of `Agent', or `undefined' if there
 is none.
 @doc Returns the error-mode of `Agent'.
 either `fail' or `continue'.
 @doc Normally, actions sent directly or indirectly during another
 action are held until the action completes (changes the agent's
 state). This function can be used to dispatch any pending sent
 actions immediately. If no action is occurring, does nothing.
 Returns the number of actions dispatched.
 @doc When an agent is failed, changes the agent state to new-state
 @doc Gets the validator function for the `Agent'.
 @doc Sets the validator function for the `Agent'.
------------------------------------------------------------------------------
------------------------------------------------------------------------------
 @see 'clojerl.ICloseable'
 @see 'clojerl.IEquiv'
 @see 'clojerl.IReference'
 @see 'clojerl.IReference'
 @see 'clojerl.IStringable'
------------------------------------------------------------------------------
 gen_server callbacks
------------------------------------------------------------------------------
 Handle queued actions
 Re-cast queued actions
------------------------------------------------------------------------------
------------------------------------------------------------------------------
-module('clojerl.Agent').

-include("clojerl.hrl").
-include("clojerl_int.hrl").

-behaviour(gen_server).
-behavior('erlang.io.ICloseable').
-behavior('clojerl.IDeref').
-behavior('clojerl.IEquiv').
-behavior('clojerl.IHash').
-behavior('clojerl.IMeta').
-behavior('clojerl.IReference').
-behavior('clojerl.IStringable').

-export([?CONSTRUCTOR/1]).

-export([ dispatch/3
        , error/1
        , error_handler/1
        , error_handler/2
        , error_mode/1
        , error_mode/2
        , release_pending_sends/0
        , restart/3
        , validator/1
        , validator/2
        ]).

  Protocols
-export([close/1]).
-export([deref/1]).
-export([equiv/2]).
-export([hash/1]).
-export([ meta/1
        , with_meta/2
        ]).
-export([ alter_meta/3
        , reset_meta/2
        ]).
-export([str/1]).

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

-define(PENDING_SENDS,  '__pending_sends__').

-export_type([type/0]).
-type type()        :: #{ ?TYPE => ?M
                        , id    => binary()
                        , pid   => pid()
                        }.

-type error_mode()  :: continue | fail.

-type agent_state() :: #{ value         => any()
                        , error_handler => any()
                        , error_mode    => error_mode()
                        , error         => any()
                        , validator     => any()
                        , meta          => any()
                        }.

-type state()       :: #{ id      => binary()
                        , owner   => pid()
                        , monitor => reference()
                        , queued  => [{Fn :: any(), Args :: any()}]
                        }.


  @private
-spec ?CONSTRUCTOR(any()) -> type().
?CONSTRUCTOR(Value) ->
  UUID      = 'erlang.util.UUID':random(),
  Id        = 'erlang.util.UUID':str(UUID),
  {ok, Pid} = 'clojerl.Agent.Server':create(erlang:self(), Id, Value),
  build(Id, Pid).

-spec dispatch(type(), any(), any()) -> type().
dispatch(#{?TYPE := ?M, id :=  Id, pid := Pid} = Agent, Fun, Args) ->
  case is_running_action() of
    true ->
      add_pending_send(Fun, Args),
      Agent;
    false ->
      case field(Id, error) of
        ?NIL ->
          ok = gen_server:cast(Pid, {send, Fun, Args}),
          Agent;
        Error -> erlang:error(Error)
      end
  end.

-spec error(type()) -> any().
error(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, error).

-spec error_handler(type()) -> function().
error_handler(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, error_handler).

  @doc Sets the error - handler of ` Agent ' a to ` ErrorHandler ' .
-spec error_handler(type(), function()) -> any().
error_handler(#{?TYPE := ?M, pid := Pid} = _Agent, ErrorHandler) ->
  field(Pid, error_handler, ErrorHandler).

-spec error_mode(type()) -> atom().
error_mode(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, error_mode).

  @doc Sets the error - mode of ` Agent ' a to ` ErrorMode ' , which must be
-spec error_mode(type(), fail | continue) -> any().
error_mode(#{?TYPE := ?M, pid := Pid} = _Agent, ErrorMode) ->
  field(Pid, error_mode, ErrorMode).

-spec release_pending_sends() -> non_neg_integer().
release_pending_sends() ->
  Pid = erlang:self(),
  case erlang:get(?PENDING_SENDS) of
    ?NIL -> 0;
    Pending ->
      [ gen_server:cast(Pid, {send, Fun, Args})
        || {Fun, Args} <- lists:reverse(Pending)
      ],
      length(Pending)
  end.

  and then un - fails the agent so that sends are allowed again .
-spec restart(type(), any(), boolean()) -> any().
restart( #{?TYPE := ?M, id := Id, pid := Pid} = _Agent
       , NewValue
       , ClearActions
       ) ->
  case field(Id, error) =:= ?NIL of
    true -> ?ERROR(<<"Agent does not need a restart">>);
    false ->
      case gen_server:call(Pid, {restart, NewValue, ClearActions}) of
        ok -> NewValue;
        {error, Error} -> erlang:error(Error)
      end
  end.

-spec validator(type()) -> any().
validator(#{?TYPE := ?M, id := Id} = _Agent) ->
  field(Id, validator).

-spec validator(type(), function()) -> any().
validator(#{?TYPE := ?M, pid := Pid} = _Agent, Validator) ->
  field(Pid, validator, Validator).

  Protocols

  @private
close(#{?TYPE := ?M, pid := Pid}) ->
  ok = gen_server:stop(Pid),
  ?NIL.

  @see ' clojerl . IDeref '
  @private
deref(#{?TYPE := ?M, id := Id}) ->
  field(Id, value).

  @private
equiv( #{?TYPE := ?M, id := Id1}
     , #{?TYPE := ?M, id := Id2}
     ) ->
  Id1 =:= Id2;
equiv(_, _) ->
  false.

  @see ' clojerl . IHash '
  @private
hash(#{?TYPE := ?M, id := Id}) ->
  erlang:phash2(Id).

  @see ' clojerl . IMeta '
  @private
meta(#{?TYPE := ?M, id := Id}) ->
  field(Id, meta).

  @see ' clojerl . IMeta '
  @private
with_meta(#{?TYPE := ?M, pid := Pid} = Agent, Meta) ->
  field(Pid, meta, Meta),
  Agent.

  @private
-spec alter_meta(type(), any(), any()) -> any().
alter_meta(#{?TYPE := ?M, pid := Pid}, Fun, Args) ->
  case gen_server:call(Pid, {alter_meta, Fun, Args}) of
    {ok, Meta} -> Meta;
    {error, {Class, Reason}} -> erlang:raise(Class, Reason, [])
  end.

  @private
-spec reset_meta(type(), any()) -> any().
reset_meta(#{?TYPE := ?M, pid := Pid}, Meta) ->
  field(Pid, meta, Meta).

  @private
str(#{?TYPE := ?M, id := Id}) ->
  <<"#<clojerl.Agent ", Id/binary, ">">>.


  @private
-spec start_link(pid(), binary(), any()) -> {ok, pid()} | {error, any()}.
start_link(Owner, Id, Value) ->
  gen_server:start_link(?MODULE, {Owner, Id, Value}, []).

  @private
-spec init({pid(), binary(), any()}) -> {ok, state()}.
init({Owner, Id, Value}) ->
  AgentState = default_state(Value),
  _          = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState}),
  Ref        = erlang:monitor(process, Owner),
  State      = #{ id      => Id
                , owner   => Owner
                , monitor => Ref
                , queued  => []
                },
  {ok, State}.

  @private
-spec handle_call({restart, any(), boolean()}, any(), state()) ->
  {reply, any(), state()}.
handle_call({restart, NewValue, ClearActions}, _From, #{id := Id} = State0) ->
  {_, AgentState0} = clj_utils:ets_get(?AGENT_TABLE, Id),
  Reply = try validate(AgentState0, NewValue) of
            AgentState1 ->
              X = {Id, AgentState1#{error => ?NIL}},
              _ = clj_utils:ets_save(?AGENT_TABLE, X),
              ok
          catch _:E -> {error, E}
          end,
  State1 = case Reply of
             ok when ClearActions ->
               State0#{queued => []};
             ok ->
               Queued = maps:get(queued, State0),
               [ gen_server:cast(erlang:self(), {send, Fn, Args})
                 || {Fn, Args} <- lists:reverse(Queued)
               ],
               State0#{queued => []};
             _ ->
               State0
           end,
  {reply, Reply, State1};
handle_call({set_field, Name, Value}, _From, #{id := Id} = State) ->
  {_, AgentState} = clj_utils:ets_get(?AGENT_TABLE, Id),
  _ = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState#{Name => Value}}),
  {reply, ok, State};
handle_call({alter_meta, Fun, Args}, _From, #{id := Id} = State) ->
  {_, AgentState0}  = clj_utils:ets_get(?AGENT_TABLE, Id),
  #{meta := Meta0} = AgentState0,
  Reply = try
            Meta1 = clj_rt:apply(Fun, clj_rt:cons(Meta0, Args)),
            AgentState1 = AgentState0#{meta => Meta1},
              _ = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState1}),
            {ok, Meta1}
          catch Class:Reason ->
              {error, {Class, Reason}}
          end,
  {reply, Reply, State}.

  @private
-spec handle_cast({send, function(), any()}, state()) ->
  {noreply, state()}.
handle_cast({send, Fun, Args}, #{id := Id} = State0) ->
  {_, AgentState0} = clj_utils:ets_get(?AGENT_TABLE, Id),
  case AgentState0 of
    #{error := ?NIL} ->
      AgentState1 = apply_action(Id, AgentState0, Fun, Args),
      _           = clj_utils:ets_save(?AGENT_TABLE, {Id, AgentState1}),
      {noreply, State0};
    _ ->
      Queued = maps:get(queued, State0),
      State1 = State0#{queued => [{Fun, Args} | Queued]},
      {noreply, State1}
  end.

  @private
-spec handle_info(any(), state()) -> {noreply, state()}.
handle_info({'DOWN', Ref, _, _, Reason}, #{monitor := Ref} = State) ->
  {stop, Reason, State};
handle_info(_Msg, State) ->
  {noreply, State}.

  @private
-spec terminate(any(), state()) -> {ok, state()}.
terminate(_Msg, State) ->
  {ok, State}.

  @private
-spec code_change(any(), any(), state()) ->
  {ok, state()}.
code_change(_Msg, _From, State) ->
  {ok, State}.

  Internal

-spec default_state(any()) -> agent_state().
default_state(Value) ->
  #{ value         => Value
   , error_handler => ?NIL
   , error_mode    => continue
   , error         => ?NIL
   , validator     => ?NIL
   , meta          => ?NIL
   }.

-spec build(binary(), pid()) -> type().
build(Id, Pid) ->
  #{ ?TYPE => ?M
   , id    => Id
   , pid   => Pid
   }.

-spec init_pending_sends() -> ok.
init_pending_sends() ->
  erlang:put(?PENDING_SENDS, []),
  ok.

-spec clear_pending_sends() -> ok.
clear_pending_sends() ->
  erlang:erase(?PENDING_SENDS),
  ok.

-spec is_running_action() -> boolean().
is_running_action() ->
  clj_rt:boolean(erlang:get(?PENDING_SENDS)).

-spec add_pending_send(any(), any()) -> ok.
add_pending_send(Fun, Args) ->
  Pending = erlang:get(?PENDING_SENDS),
  erlang:put(?PENDING_SENDS, [{Fun, Args} | Pending]).

-spec apply_action(binary(), any(), function(), any()) -> any().
apply_action(Id, #{value := Value0} = AgentState0, Fun, Args) ->
  try
    init_pending_sends(),
    Value1      = clj_rt:apply(Fun, clj_rt:cons(Value0, Args)),
    AgentState1 = validate(AgentState0, Value1),
    'clojerl.Agent':release_pending_sends(),
    AgentState1
  catch
    _:Error ->
      clear_pending_sends(),
      handle_error(Id, AgentState0, Error),
      handle_error_mode(AgentState0, Error)
  after
      clear_pending_sends()
  end.

-spec validate(any(), any()) -> any().
validate(#{validator := ?NIL} = AgentState, Value) ->
  AgentState#{value => Value};
validate(#{validator := Validator} = AgentState, Value) ->
  case clj_rt:apply(Validator, [Value]) of
    true  -> AgentState#{value => Value};
    false -> ?ERROR(validation_failed)
  end.

-spec handle_error(binary(), any(), any()) -> any().
handle_error(_Id, #{error_handler := ?NIL}, _Error) ->
  ok;
handle_error(Id, #{error_handler := ErrorHandler}, Error) ->
  try
    Agent = build(Id, erlang:self()),
    clj_rt:apply(ErrorHandler, [Agent, Error])
  ignore ErrorHandler errors
  end.

-spec handle_error_mode(any(), any()) -> any().
handle_error_mode(#{error_mode := continue} = AgentState, _Error) ->
  AgentState;
handle_error_mode(#{error_mode := fail} = AgentState, Error) ->
  AgentState#{error => Error}.

-spec field(binary(), atom()) -> any().
field(Id, Name) ->
  {_, AgentState} = clj_utils:ets_get(?AGENT_TABLE, Id),
  maps:get(Name, AgentState, ?NIL).

-spec field(pid(), atom(), any()) -> any().
field(Pid, Name, Value) ->
  ok = gen_server:call(Pid, {set_field, Name, Value}),
  Value.
