classdef State < handle
    %STATE Holds and broadcasts state of  simulation for scopes
    % A simulation can be driven by Simulink or by a timer
    % Listen to events from Simulink or timer, and transmits to listener
    % as 'sourceRun', 'sourceContinue', 'sourcePause', 'sourceStop', 'sourceClose'
    % Caches simulation state
    % as 'running','stopped','paused','unknown'.
    
    % Author(s): J. Schickler, H. Dannelongue
    % Copyright 2009-2015 The MathWorks, Inc.
    
    properties (SetAccess = protected,Transient)
        CurrentState = 'unknown';
        Callback = [];
        CallbackInProgress = false;
        % Caller managed flag to differentiate between
        % Stop and Halt. Halt is a system action, a brief
        % stop to set parameters
        Halt = false;
        % Caller managed flag to differentiate between
        % Stop and Pause. Pause is a user action.
        Pause = false;
        Listeners = [];
        System;
    end
    
    
    methods
        function this = State
            %STATE Constructor
            mlock;
        end
        
        function success = attachToModel(this, hroot, initialGivenState, inlistener )
            %ATTACHTOMODEL Attach to Simulink model
            % initialGivenState is one of 'running','stopped',...
            %    'paused', 'unknown'
            % Implements basic callback mechanism:
            %    callback_fcn(hco,eventStruct,userArgs)
            % for each possible callback:
            %     stopped, running, closed
            % - hroot : model to attach to
            % - initialGivenState: initialState of model,to trigger proper callback
            % - inlistener : object on which onStateEventHandler will be called
            % eventType is the name of a Simulink event
            %
            
            % store passed-in callback
            if ~isempty(inlistener)
                this.Callback  = inlistener; 
            end
            
            % Note that we attempt to pair-up simulation status flags
            % (here, 'stopped','running', etc) to match the conditions
            % under which the corresponding events (such as StopEvent,
            % StartEvent, etc) are sent.  This may not be 1-to-1.
            % In particular, the StartEvent is paired with no less than
            % 3 distinct simulation flags (run/pause/init).  We cross
            % our fingers... it's the idea of events vs. static status.
            %
            switch initialGivenState
                case 'stopped'
                    this.CurrentState = 'stopped'; % to record initial system state
                    eventType = 'StopEvent';  % for initial callbacks to fire
                case {'running','external'}
                    this.CurrentState = 'running';
                    % Earliest safe event for getting run-time objects
                    % appears to be 'StartEvent'
                    eventType = 'StartEvent';
                case {'initializing','updating'}
                    % If we are in rapid-accelerator, we will not get a
                    % StartEvent when the model starts due to g585051.
                    % Setup the scope now.                 
                    if any(strcmp(get(hroot, 'SimulationMode'), ...
                            {'external', 'rapid-accelerator'})) 
                        this.CurrentState = 'running';
                        eventType = 'StartEvent';
                    else
                        % no data available yet
                        this.CurrentState = 'stopped';
                        eventType = '';
                    end
                case {'paused', 'compiled'}
                    this.CurrentState = 'paused';
                    eventType = 'PauseEvent';
                otherwise
                    this.CurrentState = 'unknown';
                    eventType = '';
            end
           
            % Cache the Simulink System handle
            this.System = hroot;
            if isa(hroot,'double')
                hroot = get_param(hroot,'object');
            end
            % Add listeners to Simulink
            lis = {listener(hroot,'StopEvent',@this.TrapEventsForBDListener); ...
                   listener(hroot,'StartEvent',@this.TrapEventsForBDListener); ...
                   listener(hroot,'CloseEvent',@this.TrapEventsForBDListener); ...
                   listener(hroot,'PauseEvent',@this.TrapEventsForBDListener); ...
                   listener(hroot,'ContinueEvent',@this.TrapEventsForBDListener); ...
                   listener(hroot,'SimStatusChangedEvent',@this.TrapEventsForBDListener)}; 
            if strcmpi(get(hroot,'BlockDiagramType'),'library')
                % Add a lisener to close the Scope when library is 
                lis{7,1} = listener(hroot,'ReadonlyChangedEvent',@this.TrapEventsForBDListener);
            end
            this.Listeners = lis;
            
            % Force callback based on initial simulation state
            if ~isempty(eventType)
                evStruct.Type = eventType;
                TrapEvents(this,evStruct);
            end
            
            success = this.isAttached(); 
        end
        
        function success = attachToTimer(this, timer, initialGivenState, listener)
            %ATTACH Attach to timer
            % attach to a system timer 
            % initialGivenState is one of 'running','stopped','unknown'
            % Implements basic callback mechanism
            % for each possible callback:
            %     startFcn, stopFcn
            %
            
            % store passed-in callback
            if ~isempty(listener)
                this.Callback  = listener; %@(h, ev) onStateEventHandler(listener, ev);
            end
            
            switch initialGivenState
                case 'stopped'
                    initialState = 'stopped'; % to record initial system state
                case 'running'
                    initialState = 'running';
                otherwise
                    initialState = 'unknown';
            end
            this.CurrentState = initialState;
            
            % Add listeners to timer
            set(timer, 'StopFcn', @(h,ev) TrapEvents(this,ev));
            set(timer, 'StartFcn', @(h,ev) TrapEvents(this,ev));
            lis ={timer.StartFcn;timer.StopFcn};
            this.Listeners = lis;
            % Do not force callback based on initial simulation state   
            success = this.isAttached(); 
        end
        
        % The major part used for Block Diagram Listener.
        function TrapEventsForBDListenerImpl(this, ev, eventType)
            % Pre-callback processing
            %
            % Set new state in this, based on event type,
            % then broadcast events with generic names
            % (not Simulink or timer specific)
            % Some minor events are not resent
            
            % Check for hot restart
            isInHotRestartMode = (isfield(ev,'Source') || isa(ev,'EventStringData')) && ...
                uiservices.onOffToLogical(ev.Source.InitializeInteractiveRuns);
            if strcmpi(eventType,'SimStatusChangedEvent')
                
                % We need the stopped event for properly updating the start
                % and stop controls at simulation stop. StopEvent in
                % Simulink gets triggered when simulation status is still
                % terminate and not stopped. We do not refresh the controls
                % when simulaiton status is terminate.
                
                % The DAStudio.EventDispatcher broadcasts
                % SimStatusChangedEvents from all models, so make sure the
                % event we are processing is for the correct model, if not,
                % return.
                
                if ~strcmpi(get(this.System,'Name'),get(ev.Source,'Name'))
                    return;
                end
                
                if strcmp(ev.String,getString(message('Simulink:Engine:SimStatusStopped'))) || ...
                        strcmp(ev.String,getString(message('Simulink:Engine:SimStatusStoppedAsModelRef')))
                    
                    eventType = 'StopEvent';
                    if strcmpi(this.CurrentState,'stopped')
                        return;
                    end
                    
                elseif strcmp(ev.String,getString(message('Simulink:Engine:SimStatusPausedInDebugger'))) || ...
                        strcmp(ev.String,getString(message('Simulink:Engine:SimStatusPausedInDebuggerAsModelRef')))
                    % Paused in debugger is treated as a pause event.
                    eventType = 'PauseEvent';
                elseif isInHotRestartMode && (strcmp(ev.String,...
                        getString(message('Simulink:Engine:SimStatusCompiledAsModelRef'))) || ...
                        strcmp(ev.String,...
                        getString(message('Simulink:Engine:SimStatusPausedAsModelRef'))))
                    % SimStatusPausedAsModelRef is the event that is thrown
                    % when simulation ends in hot restart for a scope in
                    % model reference. This does not effect an actual pause
                    % event
                    eventType = 'StopEvent';
                    
                elseif strcmp(ev.String,...
                        getString(message('Simulink:Engine:SimStatusExternal')))
                    % This eventType will be used by triggers to set the
                    % right state of the Pan button in Rapid Accelerator
                    % Mode.
                    eventType = 'RunningInExternalMode';
                else
                    % Other events such as start, pause etc. are handled by
                    % individual events such as StartEvent, PauseEvent etc.
                    return;
                end
            elseif strcmpi(eventType,'StopEvent') && (isfield(ev, 'Source') || isa(ev,'handle.EventData'))
                % Return early if normal/accelerator simulation. In Rapid
                % accelerator simulation mode, we need the final StopEvent
                % to update simulation controls correctly.
                simMode = ev.Source.SimulationMode;
                isInHotRestartMode = uiservices.onOffToLogical(ev.Source.InitializeInteractiveRuns);
                if ~isInHotRestartMode && any(strcmpi(simMode,{'normal','accelerator'}))
                    return;
                end
            end
            
            rebroadcastEv.Type = 'unknown';
             switch eventType  % the event received
                case {'StopEvent','StopFcn'}
                    this.CurrentState = 'stopped';
                    if this.Halt
                        % short pause, do not broadcast
                        this.CurrentState = 'halted';
                        rebroadcastEv.Type = 'SourceHalt';
                    elseif this.Pause
                        % fake a "pause" for timers
                        this.CurrentState = 'paused';
                        rebroadcastEv.Type = 'SourcePause';
                    else
                        % plain stop
                        rebroadcastEv.Type = 'SourceStop';
                    end
                case {'StartEvent','StartFcn'}
                    % When building for RTW, don't process event
                   %        'building')                        
                        this.CurrentState = 'running';
                        if this.Halt
                            rebroadcastEv.Type = 'unknown';
                        elseif this.Pause
                            % fake a "continue" for timers
                            rebroadcastEv.Type = 'SourceContinue';
                        else
                            rebroadcastEv.Type = 'SourceRun';
                        end
                    %end
                case 'CloseEvent'
                    this.CurrentState = 'closed';
                    rebroadcastEv.Type = 'SourceClose';
                case 'PauseEvent'
                    this.CurrentState = 'paused';
                    rebroadcastEv.Type = 'SourcePause';
                case 'ContinueEvent'
                    this.CurrentState = 'running';
                    rebroadcastEv.Type = 'SourceContinue';
                case 'ReadonlyChangedEvent'
                    % The DAStudio.EventDispatcher broadcasts
                    % ReadOnlyChangedEvent from all models, so make sure the
                    % event we are processing is for the correct model, if not,
                    % return. Return early if library is not locked.
                    %
                    if ~strcmpi(get(this.System,'Name'),get(ev.Source,'Name')) || ...
                            ~uiservices.onOffToLogical(get(this.System,'Lock'))
                        return;
                    end
                    rebroadcastEv.Type = 'LibraryLocked';
                case ''
                    % empty is due to a manual call from reconnect,
                    % where the model is neither running nor stopped
                    % (perhaps it's paused?)  in any case, do nothing:
                    warning(message('Spcuilib:scopes:WarnUnknownModelState'));
                    return
                case 'RunningInExternalMode'
                    rebroadcastEv.Type = 'RunningInExternalMode';
                otherwise
                    error(message('Spcuilib:scopes:ErrorUnsupportedEventType', ev.Type));
            end
            
            
            % Callback processing
            if ~isempty(this.Callback) && ~strcmp(rebroadcastEv.Type,'unknown')
                this.Callback(this, rebroadcastEv);
            end
            
            % Post-callback processing
            %
            if strcmp(this.CurrentState,'closed')
                this.closeTasks;
            end
            
        end
        
        function TrapEventsForBDListener(this, source, event)
            % Callback for registered MCOS BD Listener
            % This function is introduced becaused of the different
            % arguments being passed to MCOS and UDD Listener.
            this.CallbackInProgress = true;
            ev.Source = source;
            ev.Type = event.EventName;
            if isfield(event,'String')
                ev.String = event.String;
            end
            TrapEventsForBDListenerImpl(this, ev, event.EventName);
            this.CallbackInProgress = false;
        end
        
        % -----------------------------------------
        function TrapEvents(this,ev)
            % Execute callbacks, constructing appropriate argument list
            
            % Callback is re-entrant when aborting data connection
            % Ideally, it should not be. 
            % if this.CallbackInProgress, exit quickly

            this.CallbackInProgress = true;

            eventType = ev.Type;

            TrapEventsForBDListenerImpl(this, ev, eventType);
            %reset semaphore
            this.CallbackInProgress = false;
        end
        
        % -----------------------------------------------------------------
        function close(this)
            % CLOSE
            this.Listeners = [];  % clear listeners
            this.CurrentState = 'unknown';
            detach(this);
            
        end
% %         
        function closeTasks(this)
            %CLOSETASKS Called when system closes
            % Differs from disconnect in the following regards:
            %   - clears system handle
            %   - leaves current state alone (as 'closed')
            this.Listeners = []; 

        end
        
        function detach(this)
            % Detach from  model      
            %  do all the work in close
            this.Callback = [];
        end
        
        function b = isAttached(this)
            %ISATTACHED  Return TRUE as long as we're not in the UNKNOWN state
            b = ~strcmp(this.CurrentState,'unknown');
        end
        
        function b = isRunning(this)
            try
            %ISRUNNING   Returns true if running.
            if ~isempty(this.System) && any(strcmpi(get(this.System, 'SimulationMode'), ...
                    {'external', 'rapid-accelerator'}))
                % Covers the simulation state transitions during connect to
                % target and other events for external and rapid-accel mode
                if strcmpi(get(this.System,'ExtModeTargetSimStatus'), 'running')
                    b = true;
                else
                    b = false;
                end
            else
                b = any(strcmpi(this.CurrentState,{'running','initializing'}));
            end
            catch me %#ok
                b = false;
            end
        end
        
        function b = isInitializing(this)
            
            b = any(strcmp(get(this.System, 'SimulationMode'),{'external', 'rapid-accelerator'})) && ...
                any(strcmp(get(this.System,'ExtModeTargetSimStatus'), {'notConnected','building'})) || ...
                strcmpi(this.CurrentState, 'initializing');
        end
        
        function b = isInRapidAcceleratorAndNotRunning(this)
            %ISINRAPIDACCELERATORANDNOTRUNNING Returns true if in external mode
            % and not running
            % Function needed because in rapid accel the start event 
            % is called during building
            
            if (strcmp(get(this.System, 'SimulationMode'),'rapid-accelerator'))
                if any(strcmp(get(this.System,'ExtModeTargetSimStatus'), ... 
                        {'notConnected','building'}))
                    b = true;
                else
                    b = false;
                end
            else
                % If not in external mode, returns false, this should not be
                % called anyway
                b = false;
            end
        end
        
        function b = isStopped(this)
            %ISSTOPPED   Returns true if stopped.
            try
            if ~isempty(this.System) && any(strcmpi(get(this.System, 'SimulationMode'), ...
                    {'external', 'rapid-accelerator'}))
                if ~(strcmpi(get(this.System,'ExtModeTargetSimStatus'), 'running') ...
                        || strcmpi(get(this.System,'ExtModeTargetSimStatus'), 'paused'))
                    b = true;
                else
                    b = false;
                end
            else
                b = any(strcmpi(this.CurrentState,{'stopped','terminating'}));
            end
            catch me %#ok
                b = true;
            end
        end
        
        function b = isPaused(this)
            %ISPAUSED   Returns true if paused.
            try
            if ~isempty(this.System) && any(strcmpi(get(this.System, 'SimulationMode'), ...
                    {'external', 'rapid-accelerator'}))
                if strcmpi(get(this.System,'ExtModeTargetSimStatus'), 'paused')
                    b = true;
                else
                    b = false;
                end
            else
                b = strcmp(this.CurrentState,'paused');
            end
            catch me %#ok
                b = false;
            end
        end
        
        function b = isHalted(this)
            %ISPAUSED   Returns true if paused.
            b = strcmp(this.CurrentState,'halted');
        end
        function b = isDisconnected(this)
            %ISDISCONNECT   Returns true if disconnected
            b = strcmp(this.CurrentState,'unknown');
        end
        
        function setCallback(this, cb)
            %SETCALLBACK Stores the function to be used as event callback
            % backdoor for testing
           this.Callback = cb; 
           
        end
        
        function setHalt(this,tf)
            this.Halt = tf;       
        end
        
        function setPause(this,tf)
            this.Pause = tf;
        end

     end %methods
end
% [EOF]
