% JmAccessFactory - abstract class for MATLAB interactions
% with a ProxiedJobManagerClient

% Copyright 2014-2020 The MathWorks, Inc.

classdef (Hidden, Abstract) JmAccessFactory < parallel.internal.cluster.MJSAccessFactory
    properties (Constant, Access = protected)
        JavaEventIdToStateEnum = iBuildJavaEventIdToStateEnumMap();
    end
        
    properties (Access = protected)
        ProxyToMatlabObjectAdaptor
        ProxyToMatlabObjectAdaptorListener
    end
    
    properties (SetAccess = immutable, GetAccess = protected)
        Cache
    end
    
    methods (Access = protected)
        function obj = JmAccessFactory(cache)
            obj.Cache = cache;
        end
        
        function [jmAccess, accessCleanup] = initJmAccess(obj, newJmAccess, varargin)
            if isempty(obj.MJSAccess) || ~obj.MJSAccess.equals(newJmAccess)
                obj.MJSAccess = newJmAccess;
                if obj.MJSAccess.requireWebLicensing
                    % NB This will actually error if the user doesn't log in
                    % or if the login token hasn't already be created.
                    parallel.internal.mwa.ensureLoggedIn();
                end
                parallel.internal.cluster.MJSSupport.wrapPerformSets(...
                    obj.MJSAccess, ...
                    [], ...
                    parallel.internal.types.Variant.Cluster, ...
                    varargin{:});
                
                obj.initProxyToMatlabObjectAdaptor();
                accessCleanup = onCleanup(@() iRemoveEventAdaptor(...
                    obj.MJSAccess, obj.ProxyToMatlabObjectAdaptor));
            else
                accessCleanup = [];
            end
            jmAccess = obj.MJSAccess;
        end
    end
    
    methods (Access = protected)
        function initProxyToMatlabObjectAdaptor(obj)
        % Get a UDD version of the java object that will dispatch callback events
        % into the UDD objects. This java object is exported such that the jobmanager
        % can send us events.
            
            obj.ProxyToMatlabObjectAdaptor = com.mathworks.toolbox.distcomp.mjs.matlabobjectadaptor.ProxyToMatlabObjectAdaptor;
            % We need a mechanism to get the actual events from a java RMI call directly
            % into matlab - this is done via the java Bean interface of this java object
            % However we also need to be careful of the lifetime of this java object, so
            % We are very explicit in decoupling using listeners rather than matlab
            % Callbacks because this means that we don't store a reference to objectroot
            % in the actual java layer - and hence clear classes will continue to work
            obj.ProxyToMatlabObjectAdaptorListener = addlistener(...
                obj.ProxyToMatlabObjectAdaptor, ...
                'ProxyToMatlabObjectAdaptorEvent', ...
                {@iDispatchJavaEvent, obj.Cache, obj.JavaEventIdToStateEnum});
            % Undocumented option to set the recursion limit on callbacks to 1024 - this
            % is a silly number that will be bounded by the RecursionLimit in MATLAB.
            % Hopefully our demos will continue to work provided the main RecursionLimit
            % is upped appropriately
            set(obj.ProxyToMatlabObjectAdaptorListener, 'RecursionLimit', 1024);
            
            % Add our adaptor to the proxy's event forwarder
            obj.MJSAccess.addEventAdaptor(obj.ProxyToMatlabObjectAdaptor);
        end
    end
    
    methods
        function [ok, msg] = checkTwoWayCommunications(obj, forceCheck)            
            if nargin < 2
                forceCheck = true;
            end
            
            if isempty(obj.MJSAccess)
                ok = false;
                msg = message('parallel:cloud:SupportEmptyCommCheck').getString();
                return
            end
            proxyObject = obj.MJSAccess.getJobManager();
            
            if ~forceCheck && isa(proxyObject, 'com.mathworks.toolbox.distcomp.mjs.jobmanager.TestedJobManagerProxy')
                testResult = proxyObject.getTwoWayCommunicationResults();
                ok = testResult.isSuccess();
            else
                % Creating a tested job manager proxy performs a two-way communication check.
                testedJM = com.mathworks.toolbox.distcomp.mjs.jobmanager.TestedJobManagerProxy.create(proxyObject);
                testResult = testedJM.getTwoWayCommunicationResults();
                ok = testResult.isSuccess();
            end
            if ok
                msg = '';
            else
                msg = char(testResult.getErrorMessage());
            end
        end
        
        function [updated, newAccess, accessCleanup] = updateMJSAccess(obj)
            % Attempt to update the JmAccess backing this object.
            % Returns true if update was successful.
            % Throws an error if a different cluster is found as
            % this means the cluster has restarted "-clean".
            
            updated = true;
            try
                % Try to get a new Proxy
                lookupURL = obj.MJSAccess.getLookupURL();
                name = obj.MJSAccess.getName();
                jmProxy = parallel.internal.cluster.JobManagerFinder.getOneJobManagerProxy(lookupURL, name);
            catch E
                dctSchedulerMessage(1, sprintf('Failed to get a new proxy. LookupURL: %s, Name: %s. Original error:', lookupURL, name), E);
                updated = false;
                newAccess = [];
                accessCleanup = [];
                return;
            end
            
            % Check we are still talking to the same jobmanager
            currentClusterID = obj.MJSAccess.getID;
            newClusterID     = jmProxy.getID;
            if ~isequal(currentClusterID, newClusterID)
                ex = MException(message('parallel:cluster:MJSChanged', char(name), char(lookupURL)));
                throw(ex);
            end
            
            try
                % Create a new Access with the new proxy
                newAccess = obj.MJSAccess.createNewClient(jmProxy);
            catch E %#ok
                updated = false;
                newAccess = [];
                accessCleanup = [];
                return;
            end
            obj.MJSAccess = newAccess;
            obj.initProxyToMatlabObjectAdaptor();
            accessCleanup = onCleanup(@() iRemoveEventAdaptor(...
                newAccess, obj.ProxyToMatlabObjectAdaptor));
        end
    end
end

% --------------------------------------------------------------------------
% iDispatchJavaEvent - the callback for the ProxyToMatlabObjectAdaptorListener
function iDispatchJavaEvent(src, event, cache, javaEventIdToStateEnum)
% Make sure that once this function is finished we let the java layer
% know that we are finished
o = onCleanup(@() src.matlabEventFinished(event));
try
    uuidStr = char( event.getSource );
    if cache.isKey( uuidStr )
        jobOrTask = cache.retrieve( uuidStr );
        stateEnum = javaEventIdToStateEnum( event.getID );
        iFireStateEvent( jobOrTask, stateEnum );
    end
catch err
    % Do nothing - swallow the error silently
    dctSchedulerMessage(5, 'Error dispatching event.', err);
end
end

% --------------------------------------------------------------------------
% iFireStateEvent - fire the state event
function iFireStateEvent( jobOrTask, stateEnum )

% If the callback throws an error the warning contains whatever is in
% lasterror unless it has been reset. See G776395.
lastError        = lasterror( 'reset' );                   %#ok<LERR>
restoreLastError = onCleanup( @()lasterror( lastError ) ); %#ok<LERR>

% Fire the event with a clean last error.
jobOrTask.hFireStateEvent( stateEnum );

end

% --------------------------------------------------------------------------
% iRemoveEventAdaptor - remove the event adaptor associated with jmAccess
function iRemoveEventAdaptor(jmAccess, proxyToMatlabObjectAdaptor)
jmAccess.removeEventAdaptor(proxyToMatlabObjectAdaptor)
end

% --------------------------------------------------------------------------
% iBuildJavaEventIdToStateEnumMap - build the map used to look up the State
% enum value for a Java event ID.
function m = iBuildJavaEventIdToStateEnumMap()
import com.mathworks.toolbox.distcomp.mjs.workunit.DistcompListenable;
import parallel.internal.types.States;
m = containers.Map('KeyType', 'double', 'ValueType', 'any');
m(DistcompListenable.EVENT_QUEUED_STATE) = States.Queued;
m(DistcompListenable.EVENT_RUNNING_STATE) = States.Running;
m(DistcompListenable.EVENT_FINISHED_STATE) = States.Finished;
end
