% Helper class implementing get() and set()

% Copyright 2011-2020 The MathWorks, Inc.

classdef ( Hidden, Sealed ) GetSetImpl

    methods ( Static, Access = private )
        function setMultipleProperties( obj, useGetSet, vectorizeSet, propNames, values )
            import parallel.internal.customattr.Reflection

            % Check all values
            newValues = cellfun( @(n,v) Reflection.deriveAndCheckConstraint( obj, n, v ), ...
                     propNames, values, 'UniformOutput', false );

            if vectorizeSet
                % Set properties in one pass
                obj.hSetProperty( propNames(useGetSet), newValues(useGetSet) );
            else
                % hSetProperty of instance does not support vectorized
                % call, so iterate.
                if isscalar( obj )
                    obj.hSetProperty( propNames(useGetSet), newValues(useGetSet) );
                else
                    for idx = 1:numel( obj )
                        o = obj(idx);
                        oValues = cellfun(@(val) val{idx}, newValues, 'UniformOutput', false);
                        o.hSetProperty( propNames(useGetSet), oValues(useGetSet) );
                    end
                end
            end

            % Set the remainder using dot notation
            for ii = find( ~(useGetSet(:).') )
                propValues = newValues{ii};
                if isscalar(obj)
                    obj.(propNames{ii}) = propValues;
                else
                    for idx = 1:numel( obj )
                        o = obj(idx);
                        o.(propNames{ii}) = propValues{idx};
                    end
                end
            end
        end
    end

    methods ( Hidden, Static )
        
        function errorIfNonPublicSettableProps( objOrObjs, settableProps, userProps )
            % Given an array of objects and a list of valid public-settable
            % properties on the object(s), throw an error if any of the
            % userProps are not on the valid list.
            parallel.internal.customattr.GetSetImpl.errorIfInvalidProps(objOrObjs, settableProps, userProps, 'parallel:cluster:GetSetBadPropAccess');
        end
        
        function errorIfInvalidProps( objOrObjs, validProps, userProps, errorID )
            % Given an array of objects, and a list of valid properties on
            % the objects, throw an error if any of the userProps are not
            % on that valid list. Takes an optional argument for the
            % errorID to use.
            
            if nargin < 4
                errorID = 'parallel:cluster:GetSetBadProps';
            end
            
            if ~iscellstr( userProps ) %#ok<ISCLSTR>
                error( message( 'parallel:cluster:GetSetPropNameString' ) );
            end
            badProps = setdiff( userProps, validProps );
            if ~isempty( badProps )
                badPropsStr = strtrim( sprintf( '%s ', badProps{:} ) );
                error( message( errorID, class( objOrObjs ), badPropsStr) );
            end
        end
        
        function vcell = getImpl( objOrObjs, props )
        % "get" is vectorized over both objOrObjs and props
        % - objOrObjs can be a scalar or vector
        % - props can be a string or a cellstr defining the properties
        %
        % in the case where objOrObjs is an array, the properties must be
        % applicable to the array type - in effect the parent "Heterogeneous"
        % type.

            import parallel.internal.customattr.Reflection;
            import parallel.internal.customattr.GetSetImpl;

            validateattributes( objOrObjs, {'parallel.internal.customattr.CustomPropTypes'}, {} );
            iErrorIfAnyInvalidHandles( objOrObjs );
            
            % Make objOrObjs the right shape
            objOrObjs = objOrObjs(:);
            
            % use "getAllPublicProperties" so we can "get" Hidden properties.
            [validProps, useGetSet] = Reflection.getAllPublicProperties( objOrObjs );

            % return a structure array in the case of 1-arg get
            returnStruct = ( nargin == 1 );
            returnCell   = true; % default
            if nargin == 1
                % 1-arg "get" just shows visible properties, i.e. we omit the
                % Hidden properties.  This form of get is not allowed to error.
                props = Reflection.getPublicProperties( objOrObjs );
                allowGetErrors = false;
            else
                if ~iscell( props )
                    props = { props };
                    returnCell = false;
                end
                GetSetImpl.errorIfInvalidProps( objOrObjs, validProps, props );
                allowGetErrors = true;
            end
            
            % Pre-allocate the return.
            vcell = cell( numel( objOrObjs ), numel( props ) );
            if isempty(vcell)
                return;
            end

            % Pre-computation of which things we can "hGetProperty", and which
            % we must access directly.
            propsToGet       = intersect( props, validProps(useGetSet) );
            propsToAccess    = setdiff( props, validProps(useGetSet) );
            uniqueNames      = [ propsToGet, propsToAccess ];
            [~, reverseIdxs] = ismember( props, uniqueNames );
            
            vals_access_default  = cell(1,numel(propsToAccess));
            handled_default      = num2cell( false(1,numel(propsToAccess)) );
            
            if numel( objOrObjs ) > 1
                % Try vectorized and if that fails fall back to a loop.
                try
                    vcell = iGetAllPropertiesVec( objOrObjs, propsToGet, propsToAccess, ...
                        allowGetErrors, handled_default, vals_access_default, reverseIdxs );
                catch err %#ok<NASGU> - If this error was not caused by vectorisation it will be thrown again below.
                    % A vectorized get may not be supported for all operations.
                    % For example certain operations on heterogeneous arrays
                    % are supported, but most are not. Also vectorized gets on
                    % profiles are not supported. Therefore, if an error is
                    % thrown above we fall back to the old looping code.
                    
                    % If the error thrown was due to an OutOfMemoryError,
                    % reverting to non-vectorized may succeed.
                    vcell = iGetAllProperties( objOrObjs, propsToGet, propsToAccess, ...
                        allowGetErrors, handled_default, vals_access_default, reverseIdxs, vcell );
                end
            else
                vcell = iGetAllProperties( objOrObjs, propsToGet, propsToAccess, ...
                    allowGetErrors, handled_default, vals_access_default, reverseIdxs, vcell );
            end
            
            % Structure the return as appropriate.
            if returnStruct
                vcell = cell2struct( vcell, props, 2 );
            elseif numel( objOrObjs ) == 1 && ~returnCell
                vcell = vcell{1,1};
            end
        end
        
        function varargout = setImpl( allowNonPublicSet, vectorizeSet, objOrObjs, varargin )
        % set one or more properties, using right-most-wins semantics. No
        % properties "append". So, set(obj, 'Foo', 1, 'Foo', 2) is equivalent to
        % set(obj, 'Foo', 2).
        % Support vectorized sets with set(objOrObjs, 'Foo', values) where
        % values is a cell of equal length to objOrObjs.
            import parallel.internal.customattr.Reflection;
            import parallel.internal.customattr.GetSetImpl;
            import parallel.internal.customattr.PropSet;

            if isempty(objOrObjs)
                return
            end
            
            validateattributes( objOrObjs, {'parallel.internal.customattr.CustomPropTypes'}, {} );
            iErrorIfAnyInvalidHandles( objOrObjs );

            if nargout <= 1 && nargin == 3
                % a = set( thing ); return a struct with all the settable
                % property names and {} as the value.
                settableProps = Reflection.getUserSettableProperties( objOrObjs );
                empties = repmat( {{}}, 1, numel( settableProps ) );
                varargout{1} = cell2struct( empties, settableProps, 2 );
                return
            elseif nargout <= 1 && nargin == 4
                % [a] = set( thing, 'Prop' ); - we should list allowed values,
                % but in fact simply return {}
                if isstruct( varargin{1} )
                    % User is trying to give a single struct where we should set
                    % names and values. But we don't support that.
                    error(message('parallel:cluster:SetStructNotSupported'));
                end
                if ~ischar( varargin{1} )
                    error(message('parallel:cluster:PropertyNameMustBeChar'));
                end
                varargout{1} = {};
                return
            else
                % set( thing, 'Prop', value, 'Prop2', value2, ... );
                narginchk( 5, Inf );
                nargoutchk( 0, 0 );
            end

            if nargin == 5 && iscell( varargin{1} ) && iscell( varargin{2} )
                % set( thing, {'Prop1', 'Prop2'}, {value1, value2} );
                if numel( varargin{1} ) ~= numel( varargin{2} )
                    error(message('parallel:cluster:SetCellArrayLengthConsistency'));
                end
                % Convert the cell arrays into P-V pairs
                pvPairs = [varargin{1}(:)'; varargin{2}(:)'];
                pvPairs = pvPairs(:).';
            else
                % Here handle disallowed cases. Must be P-V pairs.
                if mod(numel(varargin),2) ~= 0
                    error(message('parallel:cluster:PropertyValueInPairs'));
                end
                if any( ~cellfun( @ischar, varargin(1:2:end) ) )
                    error(message('parallel:cluster:PropertyNameMustBeChar'));
                end
                pvPairs = varargin;
            end

            try
                % Enforce rightmost-wins for set() here.
                [p, v] = PropSet.amalgamate( {}, pvPairs{:} );
            catch E
                % error in the case where not in pairs, or not strings
                throw( E );
            end

            % Get all properties to allow set() on Hidden properties.
            if allowNonPublicSet
                allProps = Reflection.getNonPublicProperties( objOrObjs );
                settableProps = allProps;
            else
                [settableProps, allProps] = Reflection.getAllUserSettableProperties( objOrObjs );
            end
            GetSetImpl.errorIfInvalidProps( objOrObjs, allProps, p );
            if ~allowNonPublicSet
                GetSetImpl.errorIfNonPublicSettableProps( objOrObjs, settableProps, p );
            end
            useGetSet       = Reflection.propertyUsesGetSet( objOrObjs, p );
            
            if ~isscalar( objOrObjs )
                % Ensure that there is a value for each entry in objOrObjs.
                for idx = 1:numel( p )
                    assert( numel( objOrObjs ) == numel( v{idx} ), ...
                    ['For vectorized sets the number of values must equal' ...
                    ' the number of objects.'] );
                end
            end
            GetSetImpl.setMultipleProperties( objOrObjs, useGetSet, vectorizeSet, p, v );
        end
    end
end

function vcell = iGetAllPropertiesVec( objOrObjs, propsToGet, propsToAccess, ...
    allowGetErrors, handled_default, vals_access_default, reverseIdxs )
if ~isempty(propsToGet)
    vals_get     = iGetProperties( objOrObjs, propsToGet, allowGetErrors );
else
    vals_get     = [];
end
if ~isempty(propsToAccess)
    vals_access  = iAccessProperties( objOrObjs, propsToAccess, ...
        handled_default, vals_access_default, allowGetErrors );
else
    vals_access  = {};
end

allVals = [vals_get, vals_access{:}];
vcell   = allVals( :, reverseIdxs );
end

function vcell = iGetAllProperties( objOrObjs, propsToGet, propsToAccess, ...
    allowGetErrors, handled_default, vals_access_default, reverseIdxs, vcell )
for ii = 1:numel( objOrObjs )
    if ~isempty(propsToGet)
        vals_get     = iGetProperties( objOrObjs(ii), propsToGet, allowGetErrors );
    else
        vals_get     = {};
    end
    if ~isempty(propsToAccess)
        vals_access  = iAccessProperties( objOrObjs(ii), propsToAccess, ...
            handled_default, vals_access_default, allowGetErrors );
    else
        vals_access  = {};
    end
    allVals          = [vals_get, vals_access];
    vcell( ii, : )   = allVals( reverseIdxs );
end
end

%-----------------------------------------------------
function iErrorIfAnyInvalidHandles( objOrObjs )
    if ~all( isvalid( objOrObjs ) )
        error( message( 'MATLAB:class:InvalidHandle' ) );
    end
end

%-----------------------------------------------------
function v = iGetProperties( obj, name, allowErrors )
    try
        v = hGetProperty( obj, name );
    catch err
        % We get the UnsealedMethod error if obj is a heterogeneous array. 
        % We cannot make a vectorized call with heterogeneous array and we
        % want to deal with this one level up.
        if allowErrors || strcmp( err.identifier, 'MATLAB:class:UnsealedMethod' )
            rethrow(err);
        else
            % Do each one individually
            v = cellfun( @(x) iGetSinglePropertyNoErrors( obj, x ), ...
                name, 'UniformOutput', false );
        end

    end
end

%-----------------------------------------------------
function v = iAccessProperties( obj, name, handled, existingValue, allowErrors )
    try
        v = hVectorisedGet( obj, name, handled, existingValue );
    catch err
        % See iGetProperties about UnsealedMethod.
        if allowErrors || strcmp( err.identifier, 'MATLAB:class:UnsealedMethod' )
            rethrow( err );
        else
            % Access each one individually
            accessFunc = @(x, y, z) iAccessSinglePropertyNoErrors( obj, x, y, z );
            v = cellfun( accessFunc, name, handled, existingValue, ...
                'UniformOutput', false);
        end
    end
end

%-----------------------------------------------------
function v = iGetSinglePropertyNoErrors( obj, name )
    v = [];
    try
        v = obj.hGetProperty( name );
    catch err
        dctSchedulerMessage( 6, 'Failed to get the value for ''%s'': %s', ...
            name, err.getReport() );
    end
end

%-----------------------------------------------------
function v = iAccessSinglePropertyNoErrors( obj, name, handled, existingValue )
    v = [];
    try
        v = obj.hVectorisedGet( {name}, {handled}, {existingValue} );
        v = v{1};
    catch err
        dctSchedulerMessage( 6, 'Failed to get the value for ''%s'': %s', ...
            name, err.getReport() );
    end
end
