
%

%   Copyright 2011-2020 The MathWorks, Inc.

classdef MJSCallbackMixin < handle
        
    properties( Access = protected )
        RegistrationCount = 0
    end
    
    properties( Constant, Access = protected )
        NoOpCallback = @iNoOpCallback;
    end
    
    % The template methods that subclasses must implement
    methods( Abstract, Access = protected )
        doRegisterForEvents(obj)
        doUnregisterForEvents(obj);
        doUnregisterForEventsAfterDestroyed(obj);
    end
            
    methods( Access = protected )
                        
        function callback = setCallbackFcn( obj, callbackName, listener, callback )
            callback = parallel.internal.customattr.checkSimpleConstraint( 'callback', callbackName, callback );
            [wrappedCallback, enabled] = iWrapCallback( callback );
            
            previousEnabled = listener.Enabled;          
            listener.Callback = wrappedCallback;
            listener.Enabled = enabled;
                        
            if listener.Enabled && ~previousEnabled
                % We enabled a previously disabled listener
                obj.registerForEventsIfNeeded();
            elseif previousEnabled && ~listener.Enabled
                % We disabled a previously enabled listener
                obj.unregisterForEventsIfNeeded();
            end                                    
        end             
        
        function eventFired = waitForEvent( obj, eventName, timeout, waitRequiredFcn )
            % Will wait for an event if the waitRequired function returns true.
            % The waitRequired function is called initially as a short-circuit in case
            % a wait is not required at all. It is called a second time as 
            % close to the waitForEvent call as possible to try and reduce 
            % a race condition, which might cause the waiter to wait until 
            % the timeout if the state has changed between the check occurring
            % and the wait starting (g791656).
            
            if ~waitRequiredFcn( obj )
                eventFired = true;
                return;
            end
            
            % Need to wait here if the cluster is unusable as event registration
            % cannot happen in that case.
            deadline = datetime + seconds(timeout);
            if datetime >= deadline
                eventFired = false;
                return;
            end
            
            obj.registerForEventsIfNeeded()
            try 
                theListener = event.listener( obj, eventName, obj.NoOpCallback );
                waiter = distcomp.eventwaiter( theListener );
                waitRequired = waitRequiredFcn( obj );
                if waitRequired
                    obj.hWarnIfNotSubmitted();
                    isClusterAvailable = @() obj.hIsClusterAvailable();
                    remainingTimeout = deadline - datetime;
                    if remainingTimeout <= 0
                        eventFired = false;
                        return;
                    end
 
                    eventFired = waiter.waitForEvent( isClusterAvailable, seconds(remainingTimeout) );
                    if ~eventFired && ~isClusterAvailable()
                        error(message('parallel:convenience:CouldNotGetJobState'));
                    end
                else
                    % The event occurred before we reached waitForEvent so return true.
                    eventFired = true;
                end
            catch E
                iUnregisterForEventsIfNeededAndStillValid( obj );
                rethrow( E );
            end
            iUnregisterForEventsIfNeededAndStillValid( obj );            
        end        

        function unregisterForEventsAfterDestroyed( obj )
            if obj.RegistrationCount > 0
                obj.doUnregisterForEventsAfterDestroyed();
            end
        end
        
    end
    
    methods( Access = private )        
        function registerForEventsIfNeeded( obj )
            registrationCount = obj.RegistrationCount;
            if registrationCount == 0
                obj.doRegisterForEvents();
            end
            obj.RegistrationCount = registrationCount + 1;
        end
        
        function unregisterForEventsIfNeeded( obj )
            % If not registered then there is no need to do anything.
            if obj.RegistrationCount == 0
                return;                
            end
            
            registrationCount = obj.RegistrationCount - 1;            
            if registrationCount == 0
                obj.doUnregisterForEvents();
            end
            obj.RegistrationCount = registrationCount;
        end
        
    end        
end

function [wrappedCallback, enabled] = iWrapCallback( callback )
    if isempty( callback )
        wrappedCallback = @iNoOpCallback;
        enabled = false;
    elseif isa( callback, 'function_handle' )
        wrappedCallback = callback;
        enabled = true;
    elseif ischar( callback )
        wrappedCallback = @(~, ~)evalin( 'base', callback );
        enabled = true;
    elseif iscell( callback )
        wrappedCallback = @(src, evt)feval( callback{1}, src, evt, callback{2:end} );
        enabled = true;
    end
end

function iUnregisterForEventsIfNeededAndStillValid( obj )
    if isvalid( obj )
        obj.unregisterForEventsIfNeeded();
    end
end
    

function iNoOpCallback(~, ~)    
end
