% This class manages the various resource set objects that we need to
% implement SPMD. The main interface to this class is the static method
% chooseResourceSet
% <<singleton>>

% Copyright 2008-2017 The MathWorks, Inc.

classdef ResourceSetMgr < handle

    properties ( Access = private )
        TrivialSet          % The one true "trivial" resource set
        WorldSet            % The "world" set from which an inner set can be built
        ActiveNonTrivialSet % The "inner" resource set.
    end

    methods ( Access = private, Static )
        % Check one of the numeric arguments to SPMD
        function tf = sIsValidSpmdArg( x )
            tf = numel( x ) == 1 && ...
                 isnumeric( x ) && ...
                 isequal( x, round( x ) );
        end

        % return the singleton instance
        function obj = getMgr()
            persistent Singleton
            if isempty( Singleton )
                Singleton = spmdlang.ResourceSetMgr();
            end
            obj = Singleton;
        end
    end

    %% Most methods of the ResourceSetMgr are private, the public interface is the static one.
    methods (Access = private)
        function obj = ResourceSetMgr()
            obj.TrivialSet = spmdlang.TrivialResourceSet();
            obj.WorldSet   = [];          % This will be constructed on demand
            obj.ActiveNonTrivialSet = [];
        end
        
        % private function to actually choose a resource set
        % sendParcelCell - a cell array of any sendable parcels (transmission form of Remotes)
        % minN and maxN are the resolved integer arguments to the SPMD block.
        function [rs, pool] = pChooseResourceSet( obj, sendParcelCell, minN, maxN )

        % If any of our stashed resource sets are no longer valid, drop them. (This
        % happens when the pool is closed
            if ~isempty( obj.WorldSet ) && ~isValid( obj.WorldSet )
                obj.WorldSet.invalidate( false ); % shouldn't really be necessary.
                obj.WorldSet = [];
            end
            if ~isempty( obj.ActiveNonTrivialSet ) && ~isValid( obj.ActiveNonTrivialSet )
                obj.ActiveNonTrivialSet.invalidate( false );
                obj.ActiveNonTrivialSet = [];
            end
            
            pool = gcp('nocreate');
            if isempty(pool)
                poolSize = 0;
                spmdSupported = true; % trivially
            else
                poolSize = pool.NumWorkers;
                % hEnableSpmd enables SPMD if possible (and not already enabled)
                spmdSupported = pool.hInitializeSpmd();
            end
            % Error out immediately if the pool does not support SPMD and a non-trivial SPMD
            % is being requested.
            if maxN > 0 && ~ spmdSupported
                error(message('parallel:lang:spmd:SpmdNotSupportedWithThisPool'));
            end
            if isempty( obj.WorldSet ) && poolSize > 0
                obj.WorldSet = spmdlang.RemoteResourceSet.buildWorldSet(pool);
            end

            %% Logic from flow chart in sect. 1.3.2.
            if ~isempty( sendParcelCell ) % "Does the block contain Remotes as input variables?"
                rs = obj.chooseResourceSetFromRemotes( sendParcelCell, minN, maxN );
            else
                if maxN == 0 % "Is this asking for spmd(0)?"
                    rs = obj.TrivialSet;
                else

                    if ~isempty( obj.ActiveNonTrivialSet ) % "Does there exist an inner parallel resource set?"
                        if ~obj.ActiveNonTrivialSet.isReferenced()
                            % Treat this case as if there wasn't really an active set
                            [rs, pool] = obj.buildFromPoolIfPossibleOrTrivial( minN, maxN, poolSize );
                        else
                            rs = obj.ActiveNonTrivialSet;
                            if rs.satisfiesConstraints( minN, maxN ) % "Does it match the constraints?"
                                % OK
                            else
                                rs = obj.returnTrivialOrError( minN, maxN, poolSize );
                            end
                        end
                    else
                        [rs, pool] = obj.buildFromPoolIfPossibleOrTrivial( minN, maxN, poolSize );
                    end
                end
            end
        end

        % Can we derive a resource set from a cell array of sendable Remote-type objects?
        % This will return either the active non-trivial set, or the trivial set, or error.
        % sendParcelCell is a cell array of sendable parcels
        % minN and maxN are the SPMD block constraints
        % Throws a variety of errors if there are mismatches, or the supplied
        function rs = chooseResourceSetFromRemotes( obj, sendParcelCell, minN, maxN )
            v1 = sendParcelCell{1}{1};
            rs = v1.getResourceSet();
            if ~rs.isValid()
                error(message('parallel:lang:spmd:InvalidRemote'));
                % If other Remotes have invalid resource sets, that'll be picked up
                % because of the mismatch between that resource set and "rs".
            end

            % Do all Remotes have the same resource set?
            if all( cellfun( @(x)( isequal( rs, getResourceSet(x{1}) ) ), sendParcelCell ) )
                %"...and does that resource set match the constraints?"
                if rs.satisfiesConstraints( minN, maxN )
                    % OK
                else
                    
                    if minN == maxN
                        blockSizeDescr = num2str( minN );
                    else
                        blockSizeDescr = sprintf( '(%d, %d)', minN, maxN );
                    end
                    
                    error(message('parallel:lang:spmd:RemoteMismatchWithBlock', rs.numlabs, blockSizeDescr));
                end
            else
                error(message('parallel:lang:spmd:RemoteMismatchMixture'));
            end
            if isequal( rs, obj.ActiveNonTrivialSet ) || isequal( rs, obj.TrivialSet )
                % Ok
            else
                error(message('parallel:lang:spmd:ResourceSetConsistency'));
            end
        end

        % Return either a resource set built from the pool if we can match the
        % constraints, or else defer to returnTrivialOrError. In the special
        % case where an existing active set matches precisely what we'd
        % build, return that to avoid repeatedly destroying and re-creating
        % resource sets.
        function [rs, pool] = buildFromPoolIfPossibleOrTrivial( obj, minN, maxN, poolSize )
            if poolSize == 0
                % If there is no pool running, then try to auto-create one and
                % then build the new world set for it.
                pool = parallel.internal.pool.getOrCreatePool( ...
                    parallel.internal.pool.PoolPurpose.Spmd );

                if ~isempty(pool)
                    % We created a pool, so we need a new world set to go with it.
                    if ~isempty( obj.WorldSet ) 
                        obj.WorldSet.invalidate( false ); % shouldn't really be necessary.
                    end
                    
                    obj.WorldSet = spmdlang.RemoteResourceSet.buildWorldSet(pool);
                end
            else
                pool = gcp('nocreate');
            end
            
            
            if ~isempty(pool) && pool.NumWorkers >= minN % "Can I use the pool to create a new resource set...?"
                poolSize = pool.NumWorkers;
                sizeToCreate = min( maxN, poolSize );
                didReuseActive = false;

                % Must delete the old set here if it doesn't precisely match what we would build.
                if ~isempty( obj.ActiveNonTrivialSet )
                    if obj.ActiveNonTrivialSet.numlabs == sizeToCreate
                        % No need to re-create, we can re-use
                        rs = obj.ActiveNonTrivialSet;
                        didReuseActive = true;
                    else
                        % Don't invalidate the current "world" set though.
                        if ~isequal( obj.ActiveNonTrivialSet, obj.WorldSet )
                            % This may involve a remote call to free MPI comms
                            obj.ActiveNonTrivialSet.invalidate( true );
                        end
                        obj.ActiveNonTrivialSet = [];
                    end
                end

                if ~didReuseActive
                    % Split the world set to create the new set
                    rs = buildRemoteResourceSet( obj.WorldSet, sizeToCreate );
                    obj.ActiveNonTrivialSet = rs;
                end
            else
                rs = obj.returnTrivialOrError( minN, maxN, poolSize );
            end
        end
        
        % If the trivial resource set matches the constraints, use that - else
        % format a descriptive error message. This is the final failure mode
        % of resource set selection other than those to do with Remote
        % object mismatches.
        function rs = returnTrivialOrError( obj, minN, maxN, poolSize )
        % Function used to implement flow chart 1.3.2 "Does the trivial parallel resource set satisfy the constraints?"
            if obj.TrivialSet.satisfiesConstraints( minN, maxN )
                % Yes, we can simply use the trivial resource set
                rs = obj.TrivialSet;
            else
                % Format a descriptive error message, including the current active size.
                if isempty( obj.ActiveNonTrivialSet )
                    activeSetMsg = '';
                else
                    activeSetMsg = sprintf( ', the active SPMD context is of size: %d', obj.ActiveNonTrivialSet.numlabs );
                end
                
                if minN == maxN
                    requestedSizeStr = sprintf( '%d', minN );
                else
                    requestedSizeStr = sprintf( '(%d, %d)', minN, maxN );
                end
                
                error(message('parallel:lang:spmd:NoResource', requestedSizeStr, poolSize, activeSetMsg));
            end
        end

    end

    methods (Static, Hidden)

        % Sole public interface to ResourceSetMgr - pass me a cell array of Remotes,
        % and other SPMD args.
        % inputsCell - this is a cell array of SendableParcels that are going to be
        % transmitted to the SPMD block.
        % varargin - these are the SPMD "args", typically zero to two integer
        % values, but there's a backdoor where a resource set can be sent
        % in.
        function [rs, isPoolRunning, pool] = chooseResourceSet( inputsCell, varargin )

             % Back-door for various resource set operations:
            if nargin == 2 && isa( varargin{1}, 'spmdlang.AbstractResourceSet' )
                rs = varargin{1};
                pool = gcp('nocreate');
                isPoolRunning = ~isempty(pool) && pool.Connected;
                return
            end

            narginchk(1, 3);
            switch nargin
              case 1
                minN = 0; maxN = Inf;
              case 2
                minN = varargin{1}; maxN = minN;
              case 3
                minN = varargin{1}; maxN = varargin{2};
            end

            try
                if spmdlang.ResourceSetMgr.sIsValidSpmdArg( minN ) && ...
                        spmdlang.ResourceSetMgr.sIsValidSpmdArg( maxN )
                    
                    if minN > maxN
                        error(message('parallel:lang:spmd:BadResourceSpecMinExceedsMax', minN, maxN));
                    else
                         % ok	 
                         obj = spmdlang.ResourceSetMgr.getMgr();	 
                         % Pick out Remotes from inputs cell array (they *are* packed at this stage)	 
                         varIdxs = cellfun( @(x)(~isempty( x ) && isa( x{1}, 'spmdlang.SendableParcel' )), inputsCell );	 
                         [rs, pool]  = obj.pChooseResourceSet( inputsCell(varIdxs), minN, maxN );
                         isPoolRunning = ~isempty(pool) && pool.Connected;
                    end                    
                else
                    error(message('parallel:lang:spmd:BadResourceSpecification'));
                end
            catch E
                % Strip stack of internals
                throw( E );
            end
        end
    end
end

