aboutsummaryrefslogblamecommitdiffstats
path: root/lib/stdlib/src/gen_statem.erl
blob: 19d22f24f068be7acb0c409705cdec48a488aae2 (plain) (tree)
1
2
3
4


                   
                                                        
















                                                                           

                       




                                              
                                           
                      

















                              


                        
                                                  

                 
                           
                  



                           
               



                            
 
                                              
                                    
 



                                                                             
               
                                                                   
 
                
                                                           
                                                      
 
                             
 
                       
 
                     




                                                                  
 

                                                            
                                                                     
                                     
 
                            
                                  
                                                              






                                                                       
                        
                                                          
                                               
                                      


                                                                 
                        
                                                                
                                      

                                                 


                                 
                                           


                                                              
                                                             
                                                       









                                                                    







                                                              

                                                 


                          
                                                         
                                                  
                                                           



                                                              







                                                                      

                                                           


                                                        
                                                             
                       
                                     
                                          
 



                                              

                               
 


                                           
                              

                                      
                                  
                                                          
               

                                                                  
               
                           


                                                        
                                                          
                                

                                                                  
                                
                           
                                            

                                          



                                                
                                                

                                                                     
                                                









                                                                         





                                  
          





                                                        
                            
 




                                                                 
                                                       
 
                                                                       
  

                                                              
                                                    




                                                                     
  


                                                           
                     


                                         
                                     
                         
                                   
                              
                                       
  

                                                  
                       

                                
                                  
                              
                              
                         
                                   
                                             
                              
                                  





                                                                  
                              







                                                                  
                              
                               
                                                  
                       

                                                            
                                                                    





                                                              
                         



                                             
                                                       
                                                   
                                                         
      



                                                                    
      

                                                                  
 

 
                            

           

                                                   
  

                              


                                      

        







                          

                   
                                  
          

                     
                         











                                       
        



                
                                                                
 









































                                                                           







                                                                             
           

                                        

                                                       




                                                    

                                                                


                                      
                                 
                                                 





                                                                   
                                                                     
                               

                                                   


                                    
                                                                     
                               

                                                               


                                    
                                                                     
                               

                                                 


                                    
                                                                     
                               

                                                             














                                                                  



                                               
                           
                                            




                               
                                            




                                                                       
                                    

                                                               
                                






                                                                            



                                              
                                  



                                                         



                                                         
                                    
                                                     

                                                               
                                                     

                            
                                       
                     
  
                              
                                                   
                                         












                                                            
                                                                          
                                            
                                    

                                                  

                 
                                                                          
                                         

                                                
                                    
                                                           
      
                                     
                                                                             
               
                                                                        


                 
                                                                          
                                         
                                        
                                            
                                    
                                                         
                                                
                              
                                                              



                                                                             
                                  
                   

                        




                                                       
                                  


                                                                  
                         












                                                                    

                                                             






















                                                                  

                                        



                     
                                                                  
                  
                              
         


                     
 
                                                             
                                                            


                                                       

                                                      
                                  

                                                                 









                                                                    
                                 
                          
                                    
                                     
                                                

                                     
                      

                                                  
        



                                                                             

                                                            
                                                          

                            
                                                                         

                 
                                                                          
                                  
                                                

                                                       

                                        
                                  
                  
                                                   




                                                                             
                                                                
                  





                                                                         







                                                       
            

                                                
                                                  
                                                      

                                          
                                  
                  








                                                                             
                                              
                                                         

                   



                      

                         
                                                          



                            
                                
                



                                         

                                       



                 
                                                       
                      


                     


                         
                                                         
                                                          



                               
                                              







                                                                        
                                         








                                                                             


                                                               
                                             
              
                                                  

                                                        
              
                                                       
                             

                                                     
                                                    
                           
                                                        
                 
                         
                    
                                              
                
                                                              

              
                                              




                                                  
                                                             
                                   
                                                              

        







                                                                             
                                                              

                                               
                                                                           

                                    
                                                                       




                                     
      

                                                           
      




                                                        
 
                                          


                                                                        

              

                                   



                                                                 
                                         






                                                                        


                                                     





                                                                      
                                                           

                                                                           






                                                                           



                                                                     
                                    







                                                                          
                                                                            

                                            



                                                    





                                                              
                                                                 

                                                               


                                                                   
                              
                                                                            


                                                                           
                                                                




                                                                     
                                                                            
                        
                    
                                   








                                                                              




                                                    
 



                                                                 
                    

                                                                          
                                                          
                
                                                           
 

                                                          























                                                                  
                




                                                            
      

                           
                                                              




                                                             
                      
                                                                  

        










                                                                        
                      
                                                                  

        



















                                                                 








                                                                  










                                                           








                                                                  


























































































                                                                

        































                                                                 
                                                                
                             
        
 





                                                              































                                                                       




                                                                 



















































                                                                            

                                                         















                                                                           

                                                         





                                          
                                                             
      
                                                                   




                                           
                                                          










                                                              

                                                   











                                                              

                                               
                   
                                                  
                                      

                                           

                   
                                                   






































                                                              
                  



                                                          
                                        


                                                          



                                                                    


                                                                 

                        




                                              
                    



                                                 
            



                                                            

                                  


                                                            
                   

                                              
                                      




                                                                 
                                           
            


                                                               
                                               





















































































































                                                                 
                                                
                                                               
        
 
 

                                                                             
 
                                                  

                                 
                                                 

                       
                                                  

                                     

        
                                        









                                                                     

                            
                                                                    
                






                                                                           
               
        
 
 
                    
                                                                       
                                 
                          


                                                                  
        
                    
                                                            
                                
       
                            
                              
                                                  
                                    
                                                               

           
                 
                      

                 
                       

                                     

        
 
                                  








                                             











                                          


                                                                
           
 
                                                              
                          
  
                                         

                                                        
  
             


                                                 
             

                                                                 
                   


                                              
                                                                



                                                             
                                                                
                                             

                                    
                                                                
                                            


             
                                                    
                                          

                            

                                        
                                                                 
              
                                   
                                               

                           
                                               
                                 

                                                                         
                       
                                               
                                                      
                                 


                                                                         
                    

                                          

                                                             


                                                                  

                                                       
                                                               



                                                                



                                                                  

                                                            

                                                                


                                                                  
                       
               

        
















                                                                         

                                                   



                                                                  

 
 


                                                                             
                                                                        
                            
                                                                
  
                        
                                                
                                                      
                        
                                                    
             
                                        








                                                              
                                    
                                                             
            
                      


                                                       
                          

        
          
                                   
                                                          
       





                                                          
                         
                                               




                                          
        


                      
                                                             
                       
                                                             
                           
                                                             
                
                                                            

                                    






                                                   



                                                                
           
                            



                                  









                                                           
                               










                                                        







                                                              






                                                   









                                                                       
                                                    
                                                                       
            
                           
                    
                                            


                             







































                                                  

                                                        

              
                                                         

                                                              
                                                             


                                 

                                          
                                                                         

                
                                                   


                                     
                                          
                             

                    
                      
            
                                          
        
 
                                



                                   
 
                                           






                                                                      

                                                                     

                                   
                                                               
                                                                  
              
                 
        
 
                                     

                                                       
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2016-2018. 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(gen_statem).

-include("logger.hrl").

%% API
-export(
   [start/3,start/4,start_link/3,start_link/4,
    stop/1,stop/3,
    cast/2,call/2,call/3,
    enter_loop/4,enter_loop/5,enter_loop/6,
    reply/1,reply/2]).

%% gen callbacks
-export(
   [init_it/6]).

%% sys callbacks
-export(
   [system_continue/3,
    system_terminate/4,
    system_code_change/4,
    system_get_state/1,
    system_replace_state/2,
    format_status/2]).

%% Internal callbacks
-export(
   [wakeup_from_hibernate/3]).

%% logger callback
-export([format_log/1]).

%% Type exports for templates and callback modules
-export_type(
   [event_type/0,
    callback_mode_result/0,
    init_result/1,
    state_enter_result/1,
    event_handler_result/1,
    reply_action/0,
    enter_action/0,
    action/0]).
%% Old types, not advertised
-export_type(
   [state_function_result/0,
    handle_event_result/0]).

%% Type that is exported just to be documented
-export_type([transition_option/0]).

%%%==========================================================================
%%% Interface functions.
%%%==========================================================================

-type from() ::
	{To :: pid(), Tag :: term()}. % Reply-to specifier for call

-type state() ::
	state_name() | % For StateName/3 callback functions
	term(). % For handle_event/4 callback function

-type state_name() :: atom().

-type data() :: term().

-type event_type() ::
        external_event_type() | timeout_event_type() | 'internal'.
-type external_event_type() ::
        {'call',From :: from()} | 'cast' | 'info'.
-type timeout_event_type() ::
        'timeout' | {'timeout', Name :: term()} | 'state_timeout'.

-type callback_mode_result() ::
	callback_mode() | [callback_mode() | state_enter()].
-type callback_mode() :: 'state_functions' | 'handle_event_function'.
-type state_enter() :: 'state_enter'.

-type transition_option() ::
	postpone() | hibernate() |
	event_timeout() | generic_timeout() | state_timeout().
-type postpone() ::
	%% If 'true' postpone the current event
	%% and retry it when the state changes (=/=)
	boolean().
-type hibernate() ::
	%% If 'true' hibernate the server instead of going into receive
	boolean().
-type event_timeout() ::
	%% Generate a ('timeout', EventContent, ...) event
	%% unless some other event is delivered
	Time :: timeout() | integer().
-type generic_timeout() ::
	%% Generate a ({'timeout',Name}, EventContent, ...) event
	Time :: timeout() | integer().
-type state_timeout() ::
	%% Generate a ('state_timeout', EventContent, ...) event
	%% unless the state is changed
	Time :: timeout() | integer().
-type timeout_option() :: {abs,Abs :: boolean()}.

-type action() ::
	%% During a state change:
	%% * NextState and NewData are set.
	%% * All action()s are executed in order of apperance.
	%% * Postponing the current event is performed
	%%   iff 'postpone' is 'true'.
	%% * A state timeout is started iff 'timeout' is set.
	%% * Pending events are handled or if there are
	%%   no pending events the server goes into receive
	%%   or hibernate (iff 'hibernate' is 'true')
	%%
	%% These action()s are executed in order of appearence
	%% in the containing list. The ones that set options
	%% will override any previous so the last of each kind wins.
	%%
	'postpone' |  % Set the postpone option
	{'postpone', Postpone :: postpone()} |
	%%
	%% All 'next_event' events are kept in a list and then
	%% inserted at state changes so the first in the
	%% action() list is the first to be delivered.
	{'next_event', % Insert event as the next to handle
	 EventType :: event_type(),
	 EventContent :: term()} |
	enter_action().
-type enter_action() ::
	'hibernate' | % Set the hibernate option
	{'hibernate', Hibernate :: hibernate()} |
        timeout_action() |
	reply_action().
-type timeout_action() ::
	(Time :: event_timeout()) | % {timeout,Time,Time}
	{'timeout', % Set the event_timeout option
	 Time :: event_timeout(), EventContent :: term()} |
	{'timeout', % Set the event_timeout option
	 Time :: event_timeout(),
	 EventContent :: term(),
	 Options :: (timeout_option() | [timeout_option()])} |
	%%
	{{'timeout', Name :: term()}, % Set the generic_timeout option
	 Time :: generic_timeout(), EventContent :: term()} |
	{{'timeout', Name :: term()}, % Set the generic_timeout option
	 Time :: generic_timeout(),
	 EventContent :: term(),
	 Options :: (timeout_option() | [timeout_option()])} |
	%%
	{'state_timeout', % Set the state_timeout option
	 Time :: state_timeout(), EventContent :: term()} |
	{'state_timeout', % Set the state_timeout option
	 Time :: state_timeout(),
	 EventContent :: term(),
	 Options :: (timeout_option() | [timeout_option()])}.
-type reply_action() ::
	{'reply', % Reply to a caller
	 From :: from(), Reply :: term()}.

-type init_result(StateType) ::
    {ok, State :: StateType, Data :: data()} |
    {ok, State :: StateType, Data :: data(),
     Actions :: [action()] | action()} |
    'ignore' |
    {'stop', Reason :: term()}.

%% Old, not advertised
-type state_function_result() ::
	event_handler_result(state_name()).
-type handle_event_result() ::
	event_handler_result(state()).
%%
-type state_enter_result(State) ::
	{'next_state', % {next_state,NextState,NewData,[]}
	 State,
	 NewData :: data()} |
	{'next_state', % State transition, maybe to the same state
	 State,
	 NewData :: data(),
	 Actions :: [enter_action()] | enter_action()} |
	state_callback_result(enter_action()).
-type event_handler_result(StateType) ::
	{'next_state', % {next_state,NextState,NewData,[]}
	 NextState :: StateType,
	 NewData :: data()} |
	{'next_state', % State transition, maybe to the same state
	 NextState :: StateType,
	 NewData :: data(),
	 Actions :: [action()] | action()} |
	state_callback_result(action()).
-type state_callback_result(ActionType) ::
	{'keep_state', % {keep_state,NewData,[]}
	 NewData :: data()} |
	{'keep_state', % Keep state, change data
	 NewData :: data(),
	 Actions :: [ActionType] | ActionType} |
	'keep_state_and_data' | % {keep_state_and_data,[]}
	{'keep_state_and_data', % Keep state and data -> only actions
	 Actions :: [ActionType] | ActionType} |
	%%
	{'repeat_state', % {repeat_state,NewData,[]}
	 NewData :: data()} |
	{'repeat_state', % Repeat state, change data
	 NewData :: data(),
	 Actions :: [ActionType] | ActionType} |
	'repeat_state_and_data' | % {repeat_state_and_data,[]}
	{'repeat_state_and_data', % Repeat state and data -> only actions
	 Actions :: [ActionType] | ActionType} |
	%%
	'stop' | % {stop,normal}
	{'stop', % Stop the server
	 Reason :: term()} |
	{'stop', % Stop the server
	 Reason :: term(),
	 NewData :: data()} |
	%%
	{'stop_and_reply', % Reply then stop the server
	 Reason :: term(),
	 Replies :: [reply_action()] | reply_action()} |
	{'stop_and_reply', % Reply then stop the server
	 Reason :: term(),
	 Replies :: [reply_action()] | reply_action(),
	 NewData :: data()}.


%% The state machine init function.  It is called only once and
%% the server is not running until this function has returned
%% an {ok, ...} tuple.  Thereafter the state callbacks are called
%% for all events to this server.
-callback init(Args :: term()) -> init_result(state()).

%% This callback shall return the callback mode of the callback module.
%%
%% It is called once after init/0 and code_change/4 but before
%% the first state callback StateName/3 or handle_event/4.
-callback callback_mode() -> callback_mode_result().

%% Example state callback for StateName = 'state_name'
%% when callback_mode() =:= state_functions.
%%
%% In this mode all states has to be of type state_name() i.e atom().
%%
%% Note that the only callbacks that have arity 3 are these
%% StateName/3 callbacks and terminate/3, so the state name
%% 'terminate' is unusable in this mode.
-callback state_name(
	    'enter',
	    OldStateName :: state_name(),
	    Data :: data()) ->
    state_enter_result('state_name');
           (event_type(),
	    EventContent :: term(),
	    Data :: data()) ->
    event_handler_result(state_name()).
%%
%% State callback for all states
%% when callback_mode() =:= handle_event_function.
-callback handle_event(
	    'enter',
	    OldState :: state(),
	    State, % Current state
	    Data :: data()) ->
    state_enter_result(State);
           (event_type(),
	    EventContent :: term(),
	    State :: state(), % Current state
	    Data :: data()) ->
    event_handler_result(state()).

%% Clean up before the server terminates.
-callback terminate(
	    Reason :: 'normal' | 'shutdown' | {'shutdown', term()}
		    | term(),
	    State :: state(),
	    Data :: data()) ->
    any().

%% Note that the new code can expect to get an OldState from
%% the old code version not only in code_change/4 but in the first
%% state callback function called thereafter
-callback code_change(
	    OldVsn :: term() | {'down', term()},
	    OldState :: state(),
	    OldData :: data(),
	    Extra :: term()) ->
    {ok, NewState :: state(), NewData :: data()} |
    (Reason :: term()).

%% Format the callback module state in some sensible that is
%% often condensed way.  For StatusOption =:= 'normal' the preferred
%% return term is [{data,[{"State",FormattedState}]}], and for
%% StatusOption =:= 'terminate' it is just FormattedState.
-callback format_status(
	    StatusOption,
	    [ [{Key :: term(), Value :: term()}] |
	      state() |
	      data()]) ->
    Status :: term() when
      StatusOption :: 'normal' | 'terminate'.

-optional_callbacks(
   [format_status/2, % Has got a default implementation
    terminate/3, % Has got a default implementation
    code_change/4, % Only needed by advanced soft upgrade
    %%
    state_name/3, % Example for callback_mode() =:= state_functions:
    %% there has to be a StateName/3 callback function
    %% for every StateName in your state machine but the state name
    %% 'state_name' does of course not have to be used.
    %%
    handle_event/4 % For callback_mode() =:= handle_event_function
   ]).



%% Type validation functions
-compile(
   {inline,
    [callback_mode/1, state_enter/1,
     event_type/1, from/1, timeout_event_type/1]}).
%%
callback_mode(CallbackMode) ->
    case CallbackMode of
	state_functions -> true;
	handle_event_function -> true;
	_ -> false
    end.
%%
state_enter(StateEnter) ->
    case StateEnter of
        state_enter ->
            true;
        _ ->
            false
    end.
%%
event_type(Type) ->
    case Type of
	{call,From} -> from(From);
        %%
	cast -> true;
	info -> true;
	internal -> true;
        _ -> timeout_event_type(Type)
    end.
%%
from({Pid,_}) when is_pid(Pid) -> true;
from(_) -> false.
%%
timeout_event_type(Type) ->
    case Type of
        timeout -> true;
        state_timeout -> true;
        {timeout,_Name} -> true;
        _ -> false
    end.


-define(
   STACKTRACE(),
   element(2, erlang:process_info(self(), current_stacktrace))).

-define(not_sys_debug, []).
%%
%% This is a macro to only evaluate arguments if Debug =/= [].
%% Debug is evaluated multiple times.
-define(
   sys_debug(Debug, NameState, Entry),
   case begin Debug end of
       ?not_sys_debug ->
           begin Debug end;
       _ ->
           sys_debug(begin Debug end, begin NameState end, begin Entry end)
    end).

-record(state,
        {callback_mode = undefined :: callback_mode() | undefined,
         state_enter = false :: boolean(),
         module :: atom(),
         name :: atom(),
         state :: term(),
         data :: term(),
         postponed = [] :: [{event_type(),term()}],
         %%
         timer_refs = #{} :: % timer ref => the timer's event type
           #{reference() => timeout_event_type()},
         timer_types = #{} ::  % timer's event type => timer ref
           #{timeout_event_type() => reference()},
         cancel_timers = 0 :: non_neg_integer(),
         %% We add a timer to both timer_refs and timer_types
         %% when we start it.  When we request an asynchronous
         %% timer cancel we remove it from timer_types.  When
         %% the timer cancel message arrives we remove it from
         %% timer_refs.
         %%
         hibernate = false :: boolean(),
         hibernate_after = infinity :: timeout()}).

-record(trans_opts,
        {hibernate = false,
         postpone = false,
         timeouts_r = [],
         next_events_r = []}).

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

-type server_name() ::
      {'global', GlobalName :: term()}
      | {'via', RegMod :: module(), Name :: term()}
      | {'local', atom()}.
-type server_ref() ::
      pid()
      | (LocalName :: atom())
      | {Name :: atom(), Node :: atom()}
      | {'global', GlobalName :: term()}
      | {'via', RegMod :: module(), ViaName :: term()}.
-type debug_opt() ::
	{'debug',
	 Dbgs ::
	   ['trace' | 'log' | 'statistics' | 'debug'
	    | {'logfile', string()}]}.
-type hibernate_after_opt() ::
	{'hibernate_after', HibernateAfterTimeout :: timeout()}.
-type start_opt() ::
	debug_opt()
      | {'timeout', Time :: timeout()}
	  | hibernate_after_opt()
      | {'spawn_opt', [proc_lib:spawn_option()]}.
-type start_ret() ::  {'ok', pid()} | 'ignore' | {'error', term()}.



%% Start a state machine
-spec start(
	Module :: module(), Args :: term(), Opts :: [start_opt()]) ->
		   start_ret().
start(Module, Args, Opts) ->
    gen:start(?MODULE, nolink, Module, Args, Opts).
%%
-spec start(
	ServerName :: server_name(),
	Module :: module(), Args :: term(), Opts :: [start_opt()]) ->
		   start_ret().
start(ServerName, Module, Args, Opts) ->
    gen:start(?MODULE, nolink, ServerName, Module, Args, Opts).

%% Start and link to a state machine
-spec start_link(
	Module :: module(), Args :: term(), Opts :: [start_opt()]) ->
		   start_ret().
start_link(Module, Args, Opts) ->
    gen:start(?MODULE, link, Module, Args, Opts).
%%
-spec start_link(
	ServerName :: server_name(),
	Module :: module(), Args :: term(), Opts :: [start_opt()]) ->
		   start_ret().
start_link(ServerName, Module, Args, Opts) ->
    gen:start(?MODULE, link, ServerName, Module, Args, Opts).

%% Stop a state machine
-spec stop(ServerRef :: server_ref()) -> ok.
stop(ServerRef) ->
    gen:stop(ServerRef).
%%
-spec stop(
	ServerRef :: server_ref(),
	Reason :: term(),
	Timeout :: timeout()) -> ok.
stop(ServerRef, Reason, Timeout) ->
    gen:stop(ServerRef, Reason, Timeout).

%% Send an event to a state machine that arrives with type 'event'
-spec cast(ServerRef :: server_ref(), Msg :: term()) -> ok.
cast(ServerRef, Msg) when is_pid(ServerRef) ->
    send(ServerRef, wrap_cast(Msg));
cast(ServerRef, Msg) when is_atom(ServerRef) ->
    send(ServerRef, wrap_cast(Msg));
cast({global,Name}, Msg) ->
    try	global:send(Name, wrap_cast(Msg)) of
	_ -> ok
    catch
	_:_ -> ok
    end;
cast({via,RegMod,Name}, Msg) ->
    try	RegMod:send(Name, wrap_cast(Msg)) of
	_ -> ok
    catch
	_:_ -> ok
    end;
cast({Name,Node} = ServerRef, Msg) when is_atom(Name), is_atom(Node) ->
    send(ServerRef, wrap_cast(Msg)).

%% Call a state machine (synchronous; a reply is expected) that
%% arrives with type {call,From}
-spec call(ServerRef :: server_ref(), Request :: term()) -> Reply :: term().
call(ServerRef, Request) ->
    call(ServerRef, Request, infinity).
%%
-spec call(
	ServerRef :: server_ref(),
	Request :: term(),
	Timeout ::
	  timeout() |
	  {'clean_timeout',T :: timeout()} |
	  {'dirty_timeout',T :: timeout()}) ->
		  Reply :: term().
call(ServerRef, Request, infinity = T = Timeout) ->
    call_dirty(ServerRef, Request, Timeout, T);
call(ServerRef, Request, {dirty_timeout, T} = Timeout) ->
    call_dirty(ServerRef, Request, Timeout, T);
call(ServerRef, Request, {clean_timeout, T} = Timeout) ->
    call_clean(ServerRef, Request, Timeout, T);
call(ServerRef, Request, {_, _} = Timeout) ->
    erlang:error(badarg, [ServerRef,Request,Timeout]);
call(ServerRef, Request, Timeout) ->
    call_clean(ServerRef, Request, Timeout, Timeout).

%% Reply from a state machine callback to whom awaits in call/2
-spec reply([reply_action()] | reply_action()) -> ok.
reply({reply,From,Reply}) ->
    reply(From, Reply);
reply(Replies) when is_list(Replies) ->
    replies(Replies).
%%
-compile({inline, [reply/2]}).
-spec reply(From :: from(), Reply :: term()) -> ok.
reply({To,Tag}, Reply) when is_pid(To) ->
    Msg = {Tag,Reply},
    try To ! Msg of
	_ ->
	    ok
    catch
	_:_ -> ok
    end.

%% Instead of starting the state machine through start/3,4
%% or start_link/3,4 turn the current process presumably
%% started by proc_lib into a state machine using
%% the same arguments as you would have returned from init/1
-spec enter_loop(
	Module :: module(), Opts :: [debug_opt() | hibernate_after_opt()],
	State :: state(), Data :: data()) ->
			no_return().
enter_loop(Module, Opts, State, Data) ->
    enter_loop(Module, Opts, State, Data, self()).
%%
-spec enter_loop(
	Module :: module(), Opts :: [debug_opt() | hibernate_after_opt()],
	State :: state(), Data :: data(),
	Server_or_Actions ::
	  server_name() | pid() | [action()]) ->
			no_return().
enter_loop(Module, Opts, State, Data, Server_or_Actions) ->
    if
	is_list(Server_or_Actions) ->
	    enter_loop(Module, Opts, State, Data, self(), Server_or_Actions);
	true ->
	    enter_loop(Module, Opts, State, Data, Server_or_Actions, [])
    end.
%%
-spec enter_loop(
	Module :: module(), Opts :: [debug_opt() | hibernate_after_opt()],
	State :: state(), Data :: data(),
	Server :: server_name() | pid(),
	Actions :: [action()] | action()) ->
			no_return().
enter_loop(Module, Opts, State, Data, Server, Actions) ->
    is_atom(Module) orelse error({atom,Module}),
    Parent = gen:get_parent(),
    enter(Module, Opts, State, Data, Server, Actions, Parent).

%%---------------------------------------------------------------------------
%% API helpers

-compile({inline, [wrap_cast/1]}).
wrap_cast(Event) ->
    {'$gen_cast',Event}.

call_dirty(ServerRef, Request, Timeout, T) ->
    try gen:call(ServerRef, '$gen_call', Request, T) of
        {ok,Reply} ->
            Reply
    catch
        Class:Reason:Stacktrace ->
            erlang:raise(
              Class,
              {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}},
              Stacktrace)
    end.

call_clean(ServerRef, Request, Timeout, T) ->
    %% Call server through proxy process to dodge any late reply
    Ref = make_ref(),
    Self = self(),
    Pid = spawn(
            fun () ->
                    Self !
                        try gen:call(
                              ServerRef, '$gen_call', Request, T) of
                            Result ->
                                {Ref,Result}
                        catch Class:Reason:Stacktrace ->
                                {Ref,Class,Reason,Stacktrace}
                        end
            end),
    Mref = monitor(process, Pid),
    receive
        {Ref,Result} ->
            demonitor(Mref, [flush]),
            case Result of
                {ok,Reply} ->
                    Reply
            end;
        {Ref,Class,Reason,Stacktrace} ->
            demonitor(Mref, [flush]),
            erlang:raise(
              Class,
              {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}},
              Stacktrace);
        {'DOWN',Mref,_,_,Reason} ->
            %% There is a theoretical possibility that the
            %% proxy process gets killed between try--of and !
            %% so this clause is in case of that
            exit(Reason)
    end.

replies([{reply,From,Reply}|Replies]) ->
    reply(From, Reply),
    replies(Replies);
replies([]) ->
    ok.

%% Might actually not send the message in case of caught exception
send(Proc, Msg) ->
    try erlang:send(Proc, Msg)
    catch
        error:_ -> ok
    end,
    ok.

%% Here the init_it/6 and enter_loop/5,6,7 functions converge
enter(Module, Opts, State, Data, Server, Actions, Parent) ->
    %% The values should already have been type checked
    Name = gen:get_proc_name(Server),
    Debug = gen:debug_options(Name, Opts),
    HibernateAfterTimeout = gen:hibernate_after(Opts),
    Events = [],
    Event = {internal,init_state},
    %% We enforce {postpone,false} to ensure that
    %% our fake Event gets discarded, thought it might get logged
    NewActions = listify(Actions) ++ [{postpone,false}],
    S =
        #state{
           module = Module,
           name = Name,
           state = State,
           data = Data,
           hibernate_after = HibernateAfterTimeout},
    CallEnter = true,
    NewDebug = ?sys_debug(Debug, {Name,State}, {enter,Event,State}),
    case call_callback_mode(S) of
	#state{} = NewS ->
	    loop_event_actions_list(
	      Parent, NewDebug, NewS,
	      Events, Event, State, Data, false,
              NewActions, CallEnter);
	[Class,Reason,Stacktrace] ->
	    terminate(
	      Class, Reason, Stacktrace, NewDebug,
	      S, [Event|Events])
    end.

%%%==========================================================================
%%%  gen callbacks

init_it(Starter, self, ServerRef, Module, Args, Opts) ->
    init_it(Starter, self(), ServerRef, Module, Args, Opts);
init_it(Starter, Parent, ServerRef, Module, Args, Opts) ->
    try Module:init(Args) of
	Result ->
	    init_result(Starter, Parent, ServerRef, Module, Result, Opts)
    catch
	Result ->
	    init_result(Starter, Parent, ServerRef, Module, Result, Opts);
	Class:Reason:Stacktrace ->
	    Name = gen:get_proc_name(ServerRef),
	    gen:unregister_name(ServerRef),
	    proc_lib:init_ack(Starter, {error,Reason}),
	    error_info(
	      Class, Reason, Stacktrace,
	      #state{name = Name},
	      []),
	    erlang:raise(Class, Reason, Stacktrace)
    end.

%%---------------------------------------------------------------------------
%% gen callbacks helpers

init_result(Starter, Parent, ServerRef, Module, Result, Opts) ->
    case Result of
	{ok,State,Data} ->
	    proc_lib:init_ack(Starter, {ok,self()}),
	    enter(Module, Opts, State, Data, ServerRef, [], Parent);
	{ok,State,Data,Actions} ->
	    proc_lib:init_ack(Starter, {ok,self()}),
	    enter(Module, Opts, State, Data, ServerRef, Actions, Parent);
	{stop,Reason} ->
	    gen:unregister_name(ServerRef),
	    proc_lib:init_ack(Starter, {error,Reason}),
	    exit(Reason);
	ignore ->
	    gen:unregister_name(ServerRef),
	    proc_lib:init_ack(Starter, ignore),
	    exit(normal);
	_ ->
	    Name = gen:get_proc_name(ServerRef),
	    gen:unregister_name(ServerRef),
	    Error = {bad_return_from_init,Result},
	    proc_lib:init_ack(Starter, {error,Error}),
	    error_info(
	      error, Error, ?STACKTRACE(),
	      #state{name = Name},
	      []),
	    exit(Error)
    end.

%%%==========================================================================
%%% sys callbacks

system_continue(Parent, Debug, S) ->
    loop(Parent, Debug, S).

system_terminate(Reason, _Parent, Debug, S) ->
    terminate(exit, Reason, ?STACKTRACE(), Debug, S, []).

system_code_change(
  #state{
     module = Module,
     state = State,
     data = Data} = S,
  _Mod, OldVsn, Extra) ->
    case
	try Module:code_change(OldVsn, State, Data, Extra)
	catch
	    Result -> Result
	end
    of
	{ok,NewState,NewData} ->
	    {ok,
	     S#state{
               callback_mode = undefined,
               state = NewState,
               data = NewData}};
	{ok,_} = Error ->
	    error({case_clause,Error});
	Error ->
	    Error
    end.

system_get_state(#state{state = State, data = Data}) ->
    {ok,{State,Data}}.

system_replace_state(
  StateFun,
  #state{
     state = State,
     data = Data} = S) ->
    {NewState,NewData} = Result = StateFun({State,Data}),
    {ok,Result,S#state{state = NewState, data = NewData}}.

format_status(
  Opt,
  [PDict,SysState,Parent,Debug,
   #state{name = Name, postponed = P} = S]) ->
    Header = gen:format_status_header("Status for state machine", Name),
    Log = sys:get_debug(log, Debug, []),
    [{header,Header},
     {data,
      [{"Status",SysState},
       {"Parent",Parent},
       {"Logged Events",Log},
       {"Postponed",P}]} |
     case format_status(Opt, PDict, S) of
	 L when is_list(L) -> L;
	 T -> [T]
     end].

%%---------------------------------------------------------------------------
%% Format debug messages.  Print them as the call-back module sees
%% them, not as the real erlang messages.  Use trace for that.
%%---------------------------------------------------------------------------

sys_debug(Debug, NameState, Entry) ->
  sys:handle_debug(Debug, fun print_event/3, NameState, Entry).

print_event(Dev, {in,Event}, {Name,State}) ->
    io:format(
      Dev, "*DBG* ~tp receive ~ts in state ~tp~n",
      [Name,event_string(Event),State]);
print_event(Dev, {out,Reply,{To,_Tag}}, {Name,State}) ->
    io:format(
      Dev, "*DBG* ~tp send ~tp to ~p from state ~tp~n",
      [Name,Reply,To,State]);
print_event(Dev, {terminate,Reason}, {Name,State}) ->
    io:format(
      Dev, "*DBG* ~tp terminate ~tp in state ~tp~n",
      [Name,Reason,State]);
print_event(Dev, {Tag,Event,NextState}, {Name,State}) ->
    StateString =
	case NextState of
	    State ->
		io_lib:format("~tp", [State]);
	    _ ->
		io_lib:format("~tp => ~tp", [State,NextState])
	end,
    io:format(
      Dev, "*DBG* ~tp ~tw ~ts in state ~ts~n",
      [Name,Tag,event_string(Event),StateString]).

event_string(Event) ->
    case Event of
	{{call,{Pid,_Tag}},Request} ->
	    io_lib:format("call ~tp from ~w", [Request,Pid]);
	{EventType,EventContent} ->
	    io_lib:format("~tw ~tp", [EventType,EventContent])
    end.

%%%==========================================================================
%%% Internal callbacks

wakeup_from_hibernate(Parent, Debug, S) ->
    %% It is a new message that woke us up so we have to receive it now
    loop_receive(Parent, Debug, S).

%%%==========================================================================
%%% State Machine engine implementation of proc_lib/gen server

%% Server loop, consists of all loop* functions
%% and detours through sys:handle_system_message/7 and proc_lib:hibernate/3

%% Entry point for system_continue/3
loop(Parent, Debug, #state{hibernate = true, cancel_timers = 0} = S) ->
    loop_hibernate(Parent, Debug, S);
loop(Parent, Debug, S) ->
    loop_receive(Parent, Debug, S).

loop_hibernate(Parent, Debug, S) ->
    %%
    %% Does not return but restarts process at
    %% wakeup_from_hibernate/3 that jumps to loop_receive/3
    %%
    proc_lib:hibernate(
      ?MODULE, wakeup_from_hibernate, [Parent,Debug,S]),
    error(
      {should_not_have_arrived_here_but_instead_in,
       {wakeup_from_hibernate,3}}).

%% Entry point for wakeup_from_hibernate/3
loop_receive(
  Parent, Debug, #state{hibernate_after = HibernateAfterTimeout} = S) ->
    %%
    receive
	Msg ->
	    case Msg of
		{system,Pid,Req} ->
		    %% Does not return but tail recursively calls
		    %% system_continue/3 that jumps to loop/3
		    sys:handle_system_msg(
		      Req, Pid, Parent, ?MODULE, Debug, S,
		      S#state.hibernate);
		{'EXIT',Parent,Reason} = EXIT ->
		    %% EXIT is not a 2-tuple therefore
		    %% not an event but this will stand out
		    %% in the crash report...
		    Q = [EXIT],
		    terminate(exit, Reason, ?STACKTRACE(), Debug, S, Q);
		{timeout,TimerRef,TimerMsg} ->
		    #state{
                       timer_refs = TimerRefs,
                       timer_types = TimerTypes} = S,
		    case TimerRefs of
			#{TimerRef := TimerType} ->
			    %% We know of this timer; is it a running
			    %% timer or a timer being cancelled that
			    %% managed to send a late timeout message?
			    case TimerTypes of
				#{TimerType := TimerRef} ->
				    %% The timer type maps back to this
				    %% timer ref, so it was a running timer
				    %% Unregister the triggered timeout
				    NewTimerRefs =
					maps:remove(TimerRef, TimerRefs),
				    NewTimerTypes =
					maps:remove(TimerType, TimerTypes),
				    loop_receive_result(
				      Parent, Debug,
				      S#state{
					timer_refs = NewTimerRefs,
					timer_types = NewTimerTypes},
				      TimerType, TimerMsg);
				_ ->
				    %% This was a late timeout message
				    %% from timer being cancelled, so
				    %% ignore it and expect a cancel_timer
				    %% msg shortly
				    loop_receive(Parent, Debug, S)
			    end;
			_ ->
			    %% Not our timer; present it as an event
			    loop_receive_result(Parent, Debug, S, info, Msg)
		    end;
		{cancel_timer,TimerRef,_} ->
		    #state{
                       timer_refs = TimerRefs,
                       cancel_timers = CancelTimers,
                       hibernate = Hibernate} = S,
		    case TimerRefs of
			#{TimerRef := _} ->
			    %% We must have requested a cancel
			    %% of this timer so it is already
			    %% removed from TimerTypes
			    NewTimerRefs =
				maps:remove(TimerRef, TimerRefs),
			    NewCancelTimers = CancelTimers - 1,
			    NewS =
				S#state{
				  timer_refs = NewTimerRefs,
				  cancel_timers = NewCancelTimers},
			    if
				Hibernate =:= true, NewCancelTimers =:= 0 ->
				    %% No more cancel_timer msgs to expect;
				    %% we can hibernate
				    loop_hibernate(Parent, Debug, NewS);
				NewCancelTimers >= 0 -> % Assert
				    loop_receive(Parent, Debug, NewS)
			    end;
			_ ->
			    %% Not our cancel_timer msg;
			    %% present it as an event
			    loop_receive_result(Parent, Debug, S, info, Msg)
		    end;
		_ ->
		    %% External msg
                    case Msg of
                        {'$gen_call',From,Request} ->
                            loop_receive_result(
                              Parent, Debug, S, {call,From}, Request);
                        {'$gen_cast',Cast} ->
                            loop_receive_result(Parent, Debug, S, cast, Cast);
                        _ ->
                            loop_receive_result(Parent, Debug, S, info, Msg)
                    end
	    end
    after
	    HibernateAfterTimeout ->
		    loop_hibernate(Parent, Debug, S)
    end.

loop_receive_result(Parent, ?not_sys_debug, S, Type, Content) ->
    %% Here is the queue of not yet handled events created
    Events = [],
    loop_event(Parent, ?not_sys_debug, S, Events, Type, Content);
loop_receive_result(
  Parent, Debug, #state{name = Name, state = State} = S, Type, Content) ->
    NewDebug = sys_debug(Debug, {Name,State}, {in,{Type,Content}}),
    %% Here is the queue of not yet handled events created
    Events = [],
    loop_event(Parent, NewDebug, S, Events, Type, Content).

%% Entry point for handling an event, received or enqueued
loop_event(
  Parent, Debug, #state{hibernate = Hibernate} = S,
  Events, Type, Content) ->
    %%
    case Hibernate of
        true ->
            %%
            %% If (this old) Hibernate is true here it can only be
            %% because it was set from an event action
            %% and we did not go into hibernation since there were
            %% events in queue, so we do what the user
            %% might rely on i.e collect garbage which
            %% would have happened if we actually hibernated
            %% and immediately was awakened.
            %%
            _ = garbage_collect(),
            loop_event_state_function(
              Parent, Debug, S, Events, Type, Content);
        false ->
            loop_event_state_function(
              Parent, Debug, S, Events, Type, Content)
    end.

%% Call the state function
loop_event_state_function(
  Parent, Debug,
  #state{state = State, data = Data} = S,
  Events, Type, Content) ->
    %%
    %% The field 'hibernate' in S is now invalid and will be
    %% restored when looping back to loop/3 or loop_event/6.
    %%
    Event = {Type,Content},
    TransOpts = false,
    case call_state_function(S, Type, Content, State, Data) of
        {Result, NewS} ->
            loop_event_result(
              Parent, Debug, NewS,
              Events, Event, State, Data, TransOpts, Result);
	[Class,Reason,Stacktrace] ->
	    terminate(
	      Class, Reason, Stacktrace, Debug, S, [Event|Events])
    end.

%% Make a state enter call to the state function
loop_event_state_enter(
  Parent, Debug, #state{state = PrevState} = S,
  Events, Event, NextState, NewData, TransOpts) ->
    %%
    case call_state_function(S, enter, PrevState, NextState, NewData) of
        {Result, NewS} ->
            loop_event_result(
              Parent, Debug, NewS,
              Events, Event, NextState, NewData, TransOpts, Result);
	[Class,Reason,Stacktrace] ->
	    terminate(
	      Class, Reason, Stacktrace, Debug, S, [Event|Events])
    end.

%% Process the result from the state function.
%% When TransOpts =:= false it was a state function call,
%% otherwise it is an option tuple and it was a state enter call.
%%
loop_event_result(
  Parent, Debug, S,
  Events, Event, State, Data, TransOpts, Result) ->
    %%
    case Result of
	{next_state,State,NewData} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, NewData, TransOpts,
              [], false);
	{next_state,NextState,NewData}
          when TransOpts =:= false ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, NextState, NewData, TransOpts,
              [], true);
	{next_state,_NextState,_NewData} ->
            terminate(
              error,
              {bad_state_enter_return_from_state_function,Result},
              ?STACKTRACE(), Debug,
              S#state{
                state = State, data = Data,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events]);
	{next_state,State,NewData,Actions} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, NewData, TransOpts,
              Actions, false);
	{next_state,NextState,NewData,Actions}
          when TransOpts =:= false ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, NextState, NewData, TransOpts,
              Actions, true);
	{next_state,_NextState,_NewData,_Actions} ->
            terminate(
              error,
              {bad_state_enter_return_from_state_function,Result},
              ?STACKTRACE(), Debug,
              S#state{
                state = State, data = Data,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events]);
        %%
        {keep_state,NewData} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, NewData, TransOpts,
              [], false);
        {keep_state,NewData,Actions} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, NewData, TransOpts,
              Actions, false);
        %%
        keep_state_and_data ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, Data, TransOpts,
              [], false);
        {keep_state_and_data,Actions} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, Data, TransOpts,
              Actions, false);
        %%
        {repeat_state,NewData} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, NewData, TransOpts,
              [], true);
        {repeat_state,NewData,Actions} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, NewData, TransOpts,
              Actions, true);
        %%
        repeat_state_and_data ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, Data, TransOpts,
              [], true);
        {repeat_state_and_data,Actions} ->
            loop_event_actions(
              Parent, Debug, S,
              Events, Event, State, Data, TransOpts,
              Actions, true);
        %%
	stop ->
            terminate(
              exit, normal, ?STACKTRACE(), Debug,
              S#state{
                state = State, data = Data,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events]);
	{stop,Reason} ->
            terminate(
              exit, Reason, ?STACKTRACE(), Debug,
              S#state{
                state = State, data = Data,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events]);
	{stop,Reason,NewData} ->
            terminate(
              exit, Reason, ?STACKTRACE(), Debug,
              S#state{
                state = State, data = NewData,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events]);
	%%
	{stop_and_reply,Reason,Replies} ->
            reply_then_terminate(
              exit, Reason, ?STACKTRACE(), Debug,
              S#state{
                state = State, data = Data,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events], Replies);
	{stop_and_reply,Reason,Replies,NewData} ->
            reply_then_terminate(
              exit, Reason, ?STACKTRACE(), Debug,
              S#state{
                state = State, data = NewData,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events], Replies);
	%%
	_ ->
            terminate(
              error,
              {bad_return_from_state_function,Result},
              ?STACKTRACE(), Debug,
              S#state{
                state = State, data = Data,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events])
    end.

%% Ensure that Actions are a list
loop_event_actions(
  Parent, Debug, S,
  Events, Event, NextState, NewerData, TransOpts,
  Actions, CallEnter) ->
    loop_event_actions_list(
      Parent, Debug, S,
      Events, Event, NextState, NewerData, TransOpts,
      listify(Actions), CallEnter).

%% Process actions from the state function
loop_event_actions_list(
  Parent, Debug, #state{state_enter = StateEnter} = S,
  Events, Event, NextState, NewerData, TransOpts,
  Actions, CallEnter) ->
    %%
    case parse_actions(TransOpts, Debug, S, Actions) of
        {NewDebug,NewTransOpts}
          when StateEnter, CallEnter ->
            loop_event_state_enter(
              Parent, NewDebug, S,
              Events, Event, NextState, NewerData, NewTransOpts);
        {NewDebug,NewTransOpts} ->
            loop_event_done(
              Parent, NewDebug, S,
              Events, Event, NextState, NewerData, NewTransOpts);
        [Class,Reason,Stacktrace,NewDebug] ->
            terminate(
              Class, Reason, Stacktrace, NewDebug,
              S#state{
                state = NextState,
                data = NewerData,
                hibernate = hibernate_in_trans_opts(TransOpts)},
              [Event|Events])
    end.

-compile({inline, [hibernate_in_trans_opts/1]}).
hibernate_in_trans_opts(false) ->
    (#trans_opts{})#trans_opts.hibernate;
hibernate_in_trans_opts(#trans_opts{hibernate = Hibernate}) ->
    Hibernate.

parse_actions(false, Debug, S, Actions) ->
    parse_actions(true, Debug, S, Actions, #trans_opts{});
parse_actions(TransOpts, Debug, S, Actions) ->
    parse_actions(false, Debug, S, Actions, TransOpts).
%%
parse_actions(_StateCall, Debug, _S, [], TransOpts) ->
    {Debug,TransOpts};
parse_actions(StateCall, Debug, S, [Action|Actions], TransOpts) ->
    case Action of
	%% Actual actions
	{reply,From,Reply} ->
            parse_actions_reply(
              StateCall, Debug, S, Actions, TransOpts, From, Reply);
	%%
	%% Actions that set options
	{hibernate,NewHibernate} when is_boolean(NewHibernate) ->
            parse_actions(
              StateCall, Debug, S, Actions,
              TransOpts#trans_opts{hibernate = NewHibernate});
	hibernate ->
            parse_actions(
              StateCall, Debug, S, Actions,
              TransOpts#trans_opts{hibernate = true});
	%%
	{postpone,NewPostpone} when not NewPostpone orelse StateCall ->
            parse_actions(
              StateCall, Debug, S, Actions,
              TransOpts#trans_opts{postpone = NewPostpone});
	postpone when StateCall ->
            parse_actions(
              StateCall, Debug, S, Actions,
              TransOpts#trans_opts{postpone = true});
	postpone ->
            [error,
             {bad_state_enter_action_from_state_function,Action},
             ?STACKTRACE(),
             Debug];
	%%
	{next_event,Type,Content} ->
            parse_actions_next_event(
              StateCall, Debug, S, Actions, TransOpts, Type, Content);
	%%
        _ ->
            parse_actions_timeout(
              StateCall, Debug, S, Actions, TransOpts, Action)
    end.

parse_actions_reply(
  StateCall, ?not_sys_debug, S, Actions, TransOpts,
  From, Reply) ->
    %%
    case from(From) of
        true ->
            reply(From, Reply),
            parse_actions(StateCall, ?not_sys_debug, S, Actions, TransOpts);
        false ->
            [error,
             {bad_action_from_state_function,{reply,From,Reply}},
             ?STACKTRACE(),
             ?not_sys_debug]
    end;
parse_actions_reply(
  StateCall, Debug, #state{name = Name, state = State} = S,
  Actions, TransOpts, From, Reply) ->
    %%
    case from(From) of
        true ->
            reply(From, Reply),
            NewDebug = sys_debug(Debug, {Name,State}, {out,Reply,From}),
            parse_actions(StateCall, NewDebug, S, Actions, TransOpts);
        false ->
            [error,
             {bad_action_from_state_function,{reply,From,Reply}},
             ?STACKTRACE(),
             Debug]
    end.

parse_actions_next_event(
  StateCall, ?not_sys_debug, S,
  Actions, TransOpts, Type, Content) ->
    case event_type(Type) of
        true when StateCall ->
            NextEventsR = TransOpts#trans_opts.next_events_r,
            parse_actions(
              StateCall, ?not_sys_debug, S, Actions,
              TransOpts#trans_opts{
                next_events_r = [{Type,Content}|NextEventsR]});
        _ ->
            [error,
             {bad_state_enter_action_from_state_function,
	      {next_event,Type,Content}},
             ?STACKTRACE(),
             ?not_sys_debug]
    end;
parse_actions_next_event(
  StateCall, Debug, #state{name = Name, state = State} = S,
  Actions, TransOpts, Type, Content) ->
    case event_type(Type) of
        true when StateCall ->
            NewDebug = sys_debug(Debug, {Name,State}, {in,{Type,Content}}),
            NextEventsR = TransOpts#trans_opts.next_events_r,
            parse_actions(
              StateCall, NewDebug, S, Actions,
              TransOpts#trans_opts{
                next_events_r = [{Type,Content}|NextEventsR]});
        _ ->
            [error,
             {bad_state_enter_action_from_state_function,
	      {next_event,Type,Content}},
             ?STACKTRACE(),
             Debug]
    end.

parse_actions_timeout(
  StateCall, Debug, S, Actions, TransOpts,
  {TimeoutType,Time,TimerMsg,TimerOpts} = AbsoluteTimeout) ->
    %%
    case classify_timeout(TimeoutType, Time, listify(TimerOpts)) of
        absolute ->
            parse_actions_timeout_add(
              StateCall, Debug, S, Actions,
              TransOpts, AbsoluteTimeout);
        relative ->
            RelativeTimeout = {TimeoutType,Time,TimerMsg},
            parse_actions_timeout_add(
              StateCall, Debug, S, Actions,
              TransOpts, RelativeTimeout);
        badarg ->
            [error,
             {bad_action_from_state_function,AbsoluteTimeout},
             ?STACKTRACE(),
             Debug]
    end;
parse_actions_timeout(
  StateCall, Debug, S, Actions, TransOpts,
  {TimeoutType,Time,_} = RelativeTimeout) ->
    case classify_timeout(TimeoutType, Time, []) of
        relative ->
            parse_actions_timeout_add(
              StateCall, Debug, S, Actions,
              TransOpts, RelativeTimeout);
        badarg ->
            [error,
             {bad_action_from_state_function,RelativeTimeout},
             ?STACKTRACE(),
             Debug]
    end;
parse_actions_timeout(
  StateCall, Debug, S, Actions, TransOpts,
  Time) ->
    case classify_timeout(timeout, Time, []) of
        relative ->
            RelativeTimeout = {timeout,Time,Time},
            parse_actions_timeout_add(
              StateCall, Debug, S, Actions,
              TransOpts, RelativeTimeout);
        badarg ->
            [error,
             {bad_action_from_state_function,Time},
             ?STACKTRACE(),
             Debug]
    end.

parse_actions_timeout_add(
  StateCall, Debug, S, Actions,
  #trans_opts{timeouts_r = TimeoutsR} = TransOpts, Timeout) ->
    parse_actions(
      StateCall, Debug, S, Actions,
      TransOpts#trans_opts{timeouts_r = [Timeout|TimeoutsR]}).

%% Do the state transition
loop_event_done(
  Parent, ?not_sys_debug,
  #state{postponed = P} = S,
  Events, Event, NextState, NewData,
  #trans_opts{
     postpone = Postpone, hibernate = Hibernate,
     timeouts_r = [], next_events_r = []}) ->
    %%
    %% Optimize the simple cases
    %% i.e no timer changes, no inserted events and no debug,
    %% by duplicate stripped down code
    %%
    %% Fast path
    %%
    case Postpone of
        true ->
            loop_event_done_fast(
              Parent, Hibernate,
              S,
              Events, [Event|P], NextState, NewData);
        false ->
            loop_event_done_fast(
              Parent, Hibernate,
              S,
              Events, P, NextState, NewData)
    end;
loop_event_done(
  Parent, Debug_0,
  #state{
     state = State, postponed = P_0,
     timer_refs = TimerRefs_0, timer_types = TimerTypes_0,
     cancel_timers = CancelTimers_0} = S,
  Events_0, Event_0, NextState, NewData,
  #trans_opts{
     hibernate = Hibernate, timeouts_r = TimeoutsR,
     postpone = Postpone, next_events_r = NextEventsR}) ->
    %%
    %% All options have been collected and next_events are buffered.
    %% Do the actual state transition.
    %%
    %% Full feature path
    %%
    [Debug_1|P_1] = % Move current event to postponed if Postpone
	case Postpone of
	    true ->
		[?sys_debug(
                    Debug_0,
                    {S#state.name,State},
                    {postpone,Event_0,State}),
		 Event_0|P_0];
	    false ->
		[?sys_debug(
                    Debug_0,
                    {S#state.name,State},
                    {consume,Event_0,State})|P_0]
	end,
    {Events_2,P_2,Timers_2} =
	%% Move all postponed events to queue,
        %% cancel the event timer,
        %% and cancel the state timeout if the state changes
	if
	    NextState =:= State ->
		{Events_0,P_1,
                 cancel_timer_by_type(
                   timeout, {TimerTypes_0,CancelTimers_0})};
	    true ->
		{lists:reverse(P_1, Events_0),
		 [],
		 cancel_timer_by_type(
		   state_timeout,
                   cancel_timer_by_type(
                     timeout, {TimerTypes_0,CancelTimers_0}))}
		    %% The state timer is removed from TimerTypes
		    %% but remains in TimerRefs until we get
		    %% the cancel_timer msg
	end,
    {TimerRefs_3,{TimerTypes_3,CancelTimers_3},TimeoutEvents} =
	%% Stop and start timers
	parse_timers(TimerRefs_0, Timers_2, TimeoutsR),
    %% Place next events last in reversed queue
    Events_3R = lists:reverse(Events_2, NextEventsR),
    %% Enqueue immediate timeout events
    Events_4R =	prepend_timeout_events(TimeoutEvents, Events_3R),
    loop_event_done(
      Parent, Debug_1,
      S#state{
        state = NextState,
        data = NewData,
        postponed = P_2,
        timer_refs = TimerRefs_3,
        timer_types = TimerTypes_3,
        cancel_timers = CancelTimers_3,
        hibernate = Hibernate},
      lists:reverse(Events_4R)).

%% Fast path
%%
loop_event_done_fast(
  Parent, Hibernate,
  #state{
     state = NextState,
     timer_types = #{timeout := _} = TimerTypes,
     cancel_timers = CancelTimers} = S,
  Events, P, NextState, NewData) ->
    %%
    %% Same state, event timeout active
    %%
    loop_event_done_fast(
      Parent, Hibernate, S,
      Events, P, NextState, NewData,
      cancel_timer_by_type(
        timeout, {TimerTypes,CancelTimers}));
loop_event_done_fast(
  Parent, Hibernate,
  #state{state = NextState} = S,
  Events, P, NextState, NewData) ->
    %%
    %% Same state
    %%
    loop_event_done(
      Parent, ?not_sys_debug,
      S#state{
        data = NewData,
        postponed = P,
        hibernate = Hibernate},
      Events);
loop_event_done_fast(
  Parent, Hibernate,
  #state{
     timer_types = #{timeout := _} = TimerTypes,
     cancel_timers = CancelTimers} = S,
  Events, P, NextState, NewData) ->
    %%
    %% State change, event timeout active
    %%
    loop_event_done_fast(
      Parent, Hibernate, S,
      lists:reverse(P, Events), [], NextState, NewData,
        cancel_timer_by_type(
          state_timeout,
          cancel_timer_by_type(
            timeout, {TimerTypes,CancelTimers})));
loop_event_done_fast(
  Parent, Hibernate,
  #state{
     timer_types = #{state_timeout := _} = TimerTypes,
     cancel_timers = CancelTimers} = S,
  Events, P, NextState, NewData) ->
    %%
    %% State change, state timeout active
    %%
    loop_event_done_fast(
      Parent, Hibernate, S,
      lists:reverse(P, Events), [], NextState, NewData,
        cancel_timer_by_type(
          state_timeout,
          cancel_timer_by_type(
            timeout, {TimerTypes,CancelTimers})));
loop_event_done_fast(
  Parent, Hibernate,
  #state{} = S,
  Events, P, NextState, NewData) ->
    %%
    %% State change, no timeout to automatically cancel
    %%
    loop_event_done(
      Parent, ?not_sys_debug,
      S#state{
        state = NextState,
        data = NewData,
        postponed = [],
        hibernate = Hibernate},
      lists:reverse(P, Events)).
%%
%% Fast path
%%
loop_event_done_fast(
  Parent, Hibernate, S,
  Events, P, NextState, NewData,
  {TimerTypes,CancelTimers}) ->
    %%
    loop_event_done(
      Parent, ?not_sys_debug,
      S#state{
        state = NextState,
        data = NewData,
        postponed = P,
        timer_types = TimerTypes,
        cancel_timers = CancelTimers,
        hibernate = Hibernate},
      Events).

loop_event_done(Parent, Debug, S, Q) ->
    case Q of
        [] ->
            %% Get a new event
            loop(Parent, Debug, S);
        [{Type,Content}|Events] ->
	    %% Loop until out of enqueued events
	    loop_event(Parent, Debug, S, Events, Type, Content)
    end.


%%---------------------------------------------------------------------------
%% Server loop helpers

call_callback_mode(#state{module = Module} = S) ->
    try Module:callback_mode() of
	CallbackMode ->
	    callback_mode_result(S, CallbackMode)
    catch
	CallbackMode ->
	    callback_mode_result(S, CallbackMode);
	Class:Reason:Stacktrace ->
	    [Class,Reason,Stacktrace]
    end.

callback_mode_result(S, CallbackMode) ->
    callback_mode_result(
      S, CallbackMode, listify(CallbackMode), undefined, false).
%%
callback_mode_result(_S, CallbackMode, [], undefined, _StateEnter) ->
    [error,
     {bad_return_from_callback_mode,CallbackMode},
     ?STACKTRACE()];
callback_mode_result(S, _CallbackMode, [], CBMode, StateEnter) ->
    S#state{callback_mode = CBMode, state_enter = StateEnter};
callback_mode_result(S, CallbackMode, [H|T], CBMode, StateEnter) ->
    case callback_mode(H) of
	true ->
            callback_mode_result(S, CallbackMode, T, H, StateEnter);
	false ->
	    case state_enter(H) of
		true ->
                    callback_mode_result(S, CallbackMode, T, CBMode, true);
		false ->
                    [error,
                     {bad_return_from_callback_mode,CallbackMode},
                     ?STACKTRACE()]
	    end
    end.


call_state_function(
  #state{callback_mode = undefined} = S, Type, Content, State, Data) ->
    case call_callback_mode(S) of
	#state{} = NewS ->
	    call_state_function(NewS, Type, Content, State, Data);
	Error ->
	    Error
    end;
call_state_function(
  #state{callback_mode = CallbackMode, module = Module} = S,
  Type, Content, State, Data) ->
    try
	case CallbackMode of
	    state_functions ->
		Module:State(Type, Content, Data);
	    handle_event_function ->
		Module:handle_event(Type, Content, State, Data)
	end
    of
	Result ->
	    {Result,S}
    catch
	Result ->
	    {Result,S};
	Class:Reason:Stacktrace ->
	    [Class,Reason,Stacktrace]
    end.


%% -> absolute | relative | badarg
classify_timeout(TimeoutType, Time, Opts) ->
    case timeout_event_type(TimeoutType) of
        true ->
            classify_time(false, Time, Opts);
        false ->
            badarg
    end.

classify_time(Abs, Time, []) ->
    case Abs of
        true when
              is_integer(Time);
              Time =:= infinity ->
            absolute;
        false when
              is_integer(Time), 0 =< Time;
              Time =:= infinity ->
            relative;
        _ ->
            badarg
    end;
classify_time(_, Time, [{abs,Abs}|Opts]) when is_boolean(Abs) ->
    classify_time(Abs, Time, Opts);
classify_time(_, _, Opts) when is_list(Opts) ->
    badarg.

%% Stop and start timers as well as create timeout zero events
%% and pending event timer
%%
%% Stop and start timers non-event timers
parse_timers(TimerRefs, Timers, TimeoutsR) ->
    parse_timers(TimerRefs, Timers, TimeoutsR, #{}, []).
%%
parse_timers(
  TimerRefs, Timers, [], _Seen, TimeoutEvents) ->
    %%
    {TimerRefs,Timers,TimeoutEvents};
parse_timers(
  TimerRefs, Timers, [Timeout|TimeoutsR], Seen, TimeoutEvents) ->
    %%
    case Timeout of
	{TimerType,Time,TimerMsg,TimerOpts} ->
	    %% Absolute timer
	    parse_timers(
	      TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
	      TimerType, Time, TimerMsg, listify(TimerOpts));
	%% Relative timers below
	{TimerType,0,TimerMsg} ->
	    parse_timers(
	      TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
	      TimerType, zero, TimerMsg, []);
	{TimerType,Time,TimerMsg} ->
	    parse_timers(
	      TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
	      TimerType, Time, TimerMsg, [])
    end.

parse_timers(
  TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents,
  TimerType, Time, TimerMsg, TimerOpts) ->
    case Seen of
	#{TimerType := _} ->
	    %% Type seen before - ignore
	    parse_timers(
              TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents);
	#{} ->
	    %% Unseen type - handle
	    NewSeen = Seen#{TimerType => true},
	    case Time of
		infinity ->
		    %% Cancel any running timer
		    parse_timers(
		      TimerRefs, cancel_timer_by_type(TimerType, Timers),
                      TimeoutsR, NewSeen, TimeoutEvents);
		zero ->
		    %% Cancel any running timer
		    %% Handle zero time timeouts later
		    parse_timers(
		      TimerRefs, cancel_timer_by_type(TimerType, Timers),
                      TimeoutsR, NewSeen,
                      [{TimerType,TimerMsg}|TimeoutEvents]);
		_ ->
		    %% (Re)start the timer
		    TimerRef =
			erlang:start_timer(
			  Time, self(), TimerMsg, TimerOpts),
		    case Timers of
			{#{TimerType := OldTimerRef} = TimerTypes,
                         CancelTimers} ->
			    %% Cancel the running timer
			    cancel_timer(OldTimerRef),
			    NewCancelTimers = CancelTimers + 1,
			    %% Insert the new timer into
			    %% both TimerRefs and TimerTypes
			    parse_timers(
			      TimerRefs#{TimerRef => TimerType},
			      {TimerTypes#{TimerType => TimerRef},
                               NewCancelTimers},
                              TimeoutsR, NewSeen, TimeoutEvents);
			{#{} = TimerTypes,CancelTimers} ->
			    %% Insert the new timer into
			    %% both TimerRefs and TimerTypes
			    parse_timers(
			      TimerRefs#{TimerRef => TimerType},
			      {TimerTypes#{TimerType => TimerRef},
                               CancelTimers},
                              TimeoutsR, NewSeen, TimeoutEvents)
		    end
	    end
    end.

%% Enqueue immediate timeout events (timeout 0 events)
%%
%% Event timer timeout 0 events gets special treatment since
%% an event timer is cancelled by any received event,
%% so if there are enqueued events before the event timer
%% timeout 0 event - the event timer is cancelled hence no event.
%%
%% Other (state_timeout) timeout 0 events that are after
%% the event timer timeout 0 events are considered to
%% belong to timers that were started after the event timer
%% timeout 0 event fired, so they do not cancel the event timer.
%%
prepend_timeout_events([], EventsR) ->
    EventsR;
prepend_timeout_events([{timeout,_} = TimeoutEvent|TimeoutEvents], []) ->
    prepend_timeout_events(TimeoutEvents, [TimeoutEvent]);
prepend_timeout_events([{timeout,_}|TimeoutEvents], EventsR) ->
    %% Ignore since there are other events in queue
    %% so they have cancelled the event timeout 0.
    prepend_timeout_events(TimeoutEvents, EventsR);
prepend_timeout_events([TimeoutEvent|TimeoutEvents], EventsR) ->
    %% Just prepend all others
    prepend_timeout_events(TimeoutEvents, [TimeoutEvent|EventsR]).



%%---------------------------------------------------------------------------
%% Server helpers

reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, Replies) ->
    do_reply_then_terminate(
      Class, Reason, Stacktrace, Debug, S, Q, listify(Replies)).
%%
do_reply_then_terminate(
  Class, Reason, Stacktrace, Debug, S, Q, []) ->
    terminate(Class, Reason, Stacktrace, Debug, S, Q);
do_reply_then_terminate(
  Class, Reason, Stacktrace, Debug, S, Q, [R|Rs]) ->
    case R of
	{reply,{_To,_Tag}=From,Reply} ->
            reply(From, Reply),
            NewDebug =
                ?sys_debug(
                   Debug,
                   begin
                       #state{name = Name, state = State} = S,
                       {Name,State}
                   end,
                   {out,Reply,From}),
	    do_reply_then_terminate(
	      Class, Reason, Stacktrace, NewDebug, S, Q, Rs);
	_ ->
	    terminate(
	      error,
	      {bad_reply_action_from_state_function,R},
	      ?STACKTRACE(),
	      Debug, S, Q)
    end.

terminate(
  Class, Reason, Stacktrace, Debug,
  #state{module = Module, state = State, data = Data} = S,
  Q) ->
    case erlang:function_exported(Module, terminate, 3) of
	true ->
	    try Module:terminate(Reason, State, Data) of
		_ -> ok
	    catch
		_ -> ok;
		C:R:ST ->
		    error_info(C, R, ST, S, Q),
		    sys:print_log(Debug),
		    erlang:raise(C, R, ST)
	    end;
	false ->
	    ok
    end,
    _ =
	case Reason of
	    normal ->
                terminate_sys_debug(Debug, S, State, Reason);
	    shutdown ->
                terminate_sys_debug(Debug, S, State, Reason);
	    {shutdown,_} ->
                terminate_sys_debug(Debug, S, State, Reason);
	    _ ->
		error_info(Class, Reason, Stacktrace, S, Q),
		sys:print_log(Debug)
	end,
    case Stacktrace of
	[] ->
	    erlang:Class(Reason);
	_ ->
	    erlang:raise(Class, Reason, Stacktrace)
    end.

terminate_sys_debug(Debug, S, State, Reason) ->
    ?sys_debug(Debug, {S#state.name,State}, {terminate,Reason}).


error_info(
  Class, Reason, Stacktrace,
  #state{
     name = Name,
     callback_mode = CallbackMode,
     state_enter = StateEnter,
     postponed = P} = S,
  Q) ->
    ?LOG_ERROR(#{label=>{gen_statem,terminate},
                 name=>Name,
                 queue=>Q,
                 postponed=>P,
                 callback_mode=>CallbackMode,
                 state_enter=>StateEnter,
                 state=>format_status(terminate, get(), S),
                 reason=>{Class,Reason,Stacktrace}},
               #{domain=>[otp],
                 report_cb=>fun gen_statem:format_log/1,
                 error_logger=>#{tag=>error}}).

format_log(#{label:={gen_statem,terminate},
             name:=Name,
             queue:=Q,
             postponed:=P,
             callback_mode:=CallbackMode,
             state_enter:=StateEnter,
             state:=FmtData,
             reason:={Class,Reason,Stacktrace}}) ->
    {FixedReason,FixedStacktrace} =
	case Stacktrace of
	    [{M,F,Args,_}|ST]
	      when Class =:= error, Reason =:= undef ->
		case code:is_loaded(M) of
		    false ->
			{{'module could not be loaded',M},ST};
		    _ ->
			Arity =
			    if
				is_list(Args) ->
				    length(Args);
				is_integer(Args) ->
				    Args
			    end,
			case erlang:function_exported(M, F, Arity) of
			    true ->
				{Reason,Stacktrace};
			    false ->
				{{'function not exported',{M,F,Arity}},
				 ST}
			end
		end;
	    _ -> {Reason,Stacktrace}
	end,
    [LimitedP, LimitedFmtData, LimitedFixedReason] =
        [error_logger:limit_term(D) || D <- [P, FmtData, FixedReason]],
    CBMode =
	 case StateEnter of
	     true ->
		 [CallbackMode,state_enter];
	     false ->
		 CallbackMode
	 end,
    {"** State machine ~tp terminating~n" ++
         case Q of
             [] -> "";
             _ -> "** Last event = ~tp~n"
         end ++
         "** When server state  = ~tp~n" ++
         "** Reason for termination = ~w:~tp~n" ++
         "** Callback mode = ~p~n" ++
         case Q of
             [_,_|_] -> "** Queued = ~tp~n";
             _ -> ""
         end ++
         case P of
             [] -> "";
             _ -> "** Postponed = ~tp~n"
         end ++
         case FixedStacktrace of
             [] -> "";
             _ -> "** Stacktrace =~n**  ~tp~n"
         end,
     [Name |
      case Q of
          [] -> [];
          [Event|_] -> [Event]
      end] ++
         [LimitedFmtData,
          Class,LimitedFixedReason,
          CBMode] ++
         case Q of
             [_|[_|_] = Events] -> [Events];
             _ -> []
         end ++
         case P of
             [] -> [];
             _ -> [LimitedP]
         end ++
         case FixedStacktrace of
             [] -> [];
             _ -> [FixedStacktrace]
         end}.

%% Call Module:format_status/2 or return a default value
format_status(
  Opt, PDict,
  #state{module = Module, state = State, data = Data}) ->
    case erlang:function_exported(Module, format_status, 2) of
	true ->
	    try Module:format_status(Opt, [PDict,State,Data])
	    catch
		Result -> Result;
		_:_ ->
		    format_status_default(
		      Opt, State,
		      atom_to_list(Module) ++ ":format_status/2 crashed")
	    end;
	false ->
	    format_status_default(Opt, State, Data)
    end.

%% The default Module:format_status/2
format_status_default(Opt, State, Data) ->
    StateData = {State,Data},
    case Opt of
	terminate ->
	    StateData;
	_ ->
	    [{data,[{"State",StateData}]}]
    end.

-compile({inline, [listify/1]}).
listify(Item) when is_list(Item) ->
    Item;
listify(Item) ->
    [Item].

%% Cancel timer if running, otherwise no op
%%
%% This is an asynchronous cancel so the timer is not really cancelled
%% until we get a cancel_timer msg i.e {cancel_timer,TimerRef,_}.
%% In the mean time we might get a timeout message.
%%
%% Remove the timer from TimerTypes.
%% When we get the cancel_timer msg we remove it from TimerRefs.
-compile({inline, [cancel_timer_by_type/2]}).
cancel_timer_by_type(TimerType, {TimerTypes,CancelTimers} = TT_CT) ->
    case TimerTypes of
	#{TimerType := TimerRef} ->
            ok = erlang:cancel_timer(TimerRef, [{async,true}]),
	    {maps:remove(TimerType, TimerTypes),CancelTimers + 1};
	#{} ->
	    TT_CT
    end.

-compile({inline, [cancel_timer/1]}).
cancel_timer(TimerRef) ->
    ok = erlang:cancel_timer(TimerRef, [{async,true}]).