% CustomSettingsGetSet - Class to provide get/set functionality for objects that require
% properties to be store in Settings

%   Copyright 2011-2020 The MathWorks, Inc.

classdef (Hidden) CustomSettingsGetSet < parallel.internal.customattr.CustomPropTypes
    properties (Abstract, Access = protected)
        % Note that NodeFacade needs to be an Abstract property in order
        % that we can have a "default" constructor to support the singleton
        % behavior of parallel.Settings (since superclass constructors cannot
        % be called conditionally from subclass constructors)
        NodeFacade
    end

    methods (Sealed)
        function vcell = get(objOrObjs, varargin)
        % values = get(objOrObjs, props) - get one or more properties at the collapsed level
        % - objOrObjs can be a scalar or vector
        % - props can be a string or a cellstr defining the properties
        %
        % values = get(objOrObjs, prop, level) - get one property at the specified level
        % - objOrObjs can be a scalar or vector
        % - props must be a single string
        % - level is a single string
        %
        % aStruct = get(objOrObjs) - get a structure of all public gettable properties and their values
            narginchk(1, 3);
            try
                if nargin == 3
                    % v = get(thing, prop, level)
                    prop = varargin{1};
                    level = varargin{2};
                    vcell = objOrObjs.getSinglePropertyWithLevels(prop, level);
                else
                    vcell = cell(1, numel(objOrObjs));
                    for i = 1:numel(objOrObjs)
                        vcell{i} = parallel.internal.customattr.GetSetImpl.getImpl(objOrObjs(i), varargin{:});
                    end
                    vcell = vertcat(vcell{:});
                end
            catch err
                % make sure errors come from this method.
                throw(err);
            end
        end
        
        function varargout = set(objOrObjs, varargin)
        % set(objOrObjs, props, values) - set one or more properties at the user level
        % - objOrObjs can be a scalar or vector
        % - props can be a string or a cellstr defining the properties
        % - values can be a scalar or cell defining the values.  If props is a cell 
        %   array, then values must also be a cell array of the same size.
        %
        % set(objOrObs, prop1, value1, ..., propN, valueN) - set multiple properties at the user level
        % - objOrObjs can be a scalar or vector
        % - propN must be a string
        % - valueN must be a string
        %
        % set(objOrObjs, prop, value, level) - set a single property at the specified level
        % - objOrObjs can be a scalar or vector
        % - prop must be a string
        % - value must be a scalar
        % - level must be a string
        %
        % aStruct = set(objOrObjs) - get a structure with all the settable property names.
        % The values of the structure are {}
            import parallel.internal.customattr.GetSetImpl
            try
                if nargin == 4
                    % [a] = set(thing, prop, value, level)
                    objOrObjs.setSinglePropertyWithLevels(varargin{:});
                else
                    allowNonPublicSet = false;
                    vectorizeSet = all(objOrObjs.checkVectorizedSetSupport());
                    [varargout{1:nargout} ] = GetSetImpl.setImpl(allowNonPublicSet, vectorizeSet, objOrObjs, varargin{:});
                end
            catch err
                % make sure errors come from this method.
                throw(err);
            end
        end
        
        function unset(objOrObjs, props, level)
        % unset(objOrObjs, props) - unset one or more properties at the user level
        % - objOrObjs can be a scalar or vector
        % - props can be a string or a cellstr defining the properties
        %
        % unset(objOrObjs, prop, level) - unset a single property at the specified level
        % - objOrObjs can be a scalar or vector
        % - prop must be a string
        % - level must be a string
            import parallel.internal.types.SettingsLevel
            narginchk(2, 3);
            action = 'unset';
            if nargin < 3
                if ~iscell(props)
                    props = {props};
                end
                levels = repmat(SettingsLevel.User, size(props));
                userSpecifiedLevel = false;
            else
                iErrorIfPropAndLevelNotString(objOrObjs, props, level, action);
                levels = SettingsLevel.fromNameI(level);
                SettingsLevel.errorIfLevelsNotSettable(levels);
                props = {props};
                userSpecifiedLevel = true;
            end
            objOrObjs.errorIfPropsNotSettable(props, action, userSpecifiedLevel);
            try
                arrayfun(@(x) x.NodeFacade.unsetKeys(props, levels), objOrObjs);
            catch err
                % make sure errors come from this method.
                throw(err);
            end
        end
        
        function tf = isSet(objOrObjs, props, level)
        % tf = isSet(objOrObjs, props) - determine if one or more properties is set at the user level
        % - objOrObjs can be a scalar or vector
        % - props can be a string or a cellstr defining the properties
        %
        % tf = isSet(objOrObjs, prop, level) - determine if one property is set at the specified level
        % - objOrObjs can be a scalar or vector
        % - prop must be a string
        % - level must be a string
            import parallel.internal.types.SettingsLevel
            narginchk(2, 3);
            action = 'isSet';
            if nargin < 3
                if ~iscell(props)
                    props = {props};
                end
                levels = repmat(SettingsLevel.User, size(props));
                userSpecifiedLevel = false;
            else
                iErrorIfPropAndLevelNotString(objOrObjs, props, level, action);
                levels = SettingsLevel.fromNameI(level);
                SettingsLevel.errorIfCannotIsSetLevels(levels);
                props = {props};
                userSpecifiedLevel = true;
            end
            objOrObjs.errorIfPropsNotGettable(props, action, userSpecifiedLevel);
            tf = false(numel(objOrObjs), numel(props));
            try
                for ii = 1:numel(objOrObjs)
                    currObj = objOrObjs(ii);
                    if currObj.NodeFacade.IsValid
                        % Call isSet only on those keys that actually exist
                        keyExists = currObj.NodeFacade.existKeys(props);
                        % Ensure that values are returned in a column vector
                        tf(ii, keyExists) = currObj.NodeFacade.areKeysSet(props(keyExists), levels(keyExists));
                    else
                        warning(message('parallel:settings:GetSetNodeNotValid', currObj.NodeFacade.NodeName, class( currObj )));
                    end
                end
            catch err
                % make sure errors come from this method.
                throw(err);
            end
        end
        
        function verify(objOrObjs)
            % verify(objOrObjs) - verifies all properties of the object or objects
            % against their constraint values.
            % - objOrObjs can be a scalar or vector
            
            for k = 1:numel(objOrObjs)
                objOrObjs(k).verifyAndCorrectPropertyValues();
            end
        end
    end

    methods (Hidden) %Implementations of abstract methods defined in CustomPropTypes
        function value = hGetProperty(obj, props, levels)
        % hGetProperty can handle multiple props
            import parallel.internal.customattr.Reflection
            import parallel.internal.settings.SettingsDataMassager
            import parallel.internal.types.SettingsLevel
            narginchk(2, 3);
            returnCell = true;
            if ~iscell(props)
                props = {props};
                returnCell = false;
            end
            if nargin < 3
                % default is to get collapsed value when no level is supplied.
                levels = repmat(SettingsLevel.Collapsed, size(props));
            else
                % NB It's basically impossible to get here with multiple props and levels.
                % Prop and level should be scalar.
                validateattributes(levels, {'parallel.internal.types.SettingsLevel'}, {'numel', numel(props)});
            end
            
            value = cell(size(props));
            if obj.NodeFacade.IsValid
                % Call get only on those keys that actually exist
                keyExists = obj.NodeFacade.existKeys(props);
                value(keyExists) = obj.NodeFacade.getKeys(props(keyExists), levels(keyExists));
            else
                warning(message('parallel:settings:GetSetNodeNotValid', obj.NodeFacade.NodeName, class( obj )));
            end
            % Ensure we convert the data stored in settings to the actual values
            constraints = cellfun(@(x) Reflection.getConstraint(obj, x), props, ...
                'UniformOutput', false);
            value = SettingsDataMassager.convertFromSettings(props, value, constraints);
            if ~returnCell
                value = value{1};
            end
        end
        
        function hSetProperty(obj, props, values, levels)
        % hSetProperty supports cellstr for props
            import parallel.internal.customattr.Reflection
            import parallel.internal.settings.SettingsDataMassager
            import parallel.internal.types.SettingsLevel
            narginchk(3, 4);
            if ~iscell( props )
                props  = {props};
                values = {values};
            end
            if nargin < 4
                % Default is to use user level when no level is supplied.
                levels = repmat(SettingsLevel.User, size(props));
            else
                % NB It's basically impossible to get here with multiple props and levels.
                % Prop and level should be scalar.
                validateattributes(levels, {'parallel.internal.types.SettingsLevel'}, {'numel', numel(props)});
            end
            for ii = 1:length(props)
                prop = props{ii};
                value = values{ii};
                level = levels(ii);
                % Convert the value to a supported type for storage, if necessary
                constraint = Reflection.getConstraint(obj, prop);
                value = SettingsDataMassager.convertToSettings(prop, value, constraint);
                obj.hSetPropertyNoCheck(prop, value, level);
            end
        end

        function [ values, handled ] = hVectorisedGet( obj, propsToAccess, handled, existingValues )
        % Overridable method to allow classes to implement their own
        % vectorized get of non-PCTGetSet properties.
        % Only get the values for which the corresponding element of
        % handled is false.
            helperFunc = @(a,b,c) iVectorisedGetHelper(obj,a,b,c);
            [values, handled] = cellfun( helperFunc , propsToAccess, handled, existingValues, ...
                                         'UniformOutput', false );            
        end

    end
    
    methods (Hidden, Sealed)
        function hSetPropertyNoCheck(obj, prop, value, level)
        % hSetPropertyNoCheck supports single prop only
            validateattributes(prop, {'char'}, {'row'});
            validateattributes(level, {'parallel.internal.types.SettingsLevel'}, {'scalar'});
            obj.NodeFacade.setKey(prop, value, level);
        end

        function [gettableProps, settableProps, allProps, allConstraints] = hGetPublicSettingsProperties(obj)
        % [gettableProps, settableProps, allProps] = getPublicSettingsProperties(obj)
        % gettableProps and settableProps include public-hidden properties
        % allProps is the union of gettableProps and settableProps
            [gettableProps, settableProps, allProps, allConstraints] = iGetSettingsProps(obj);
        end
        
        function tf = hIsSetAnywhere(obj, prop)
        % tf = isSetAnywhere(obj, prop) - determine if the supplied property is set at any level
        % - obj must be a scalar
        % - prop can be a string

            allLevels = enumeration('parallel.internal.types.SettingsLevel');
            levelsCanIsSet = allLevels([allLevels.CanReallyCallIsSet]);
            tf = any(obj.NodeFacade.areKeysSet(repmat({prop}, numel(levelsCanIsSet), 1), levelsCanIsSet));
        end
    end
    
    methods (Static, Access = public)
        function addMissingKeysWithType(nodeFacade, keyNames, constraints)
        % addMissingKeysWithType(nodeFacade, keyNames, constraints)
        % Add keys that are missing and ensure that their init values
        % are set correctly if required.
            import parallel.internal.settings.SettingsDataMassager
            import parallel.internal.types.SettingsLevel
            
            % validateattributes(nodeFacade, {'parallel.internal.settings.SettingsNodeFacade'}, {'scalar'});
            % validateattributes(keyNames, {'cell'}, {});
            % validateattributes(constraints, {'cell'}, {'numel', numel(keyNames)});
            
            % NB even though SettingsNodeFacade.addKeys can deal with keys that already
            % exist, we need to ensure that we set required special values for those keys
            % that we just added, so just add the keys that don't already exist.
            keysToAddIdx     = ~nodeFacade.existKeys(keyNames);
            keysToAdd        = keyNames(keysToAddIdx);
            constraintsToAdd = constraints(keysToAddIdx);
            [needSpecialValuesIdx, initValues] = ...
                SettingsDataMassager.getInitSettingsValuesFromConstraints(constraintsToAdd);
                
            keysToSet   = keysToAdd(needSpecialValuesIdx);
            valuesToSet = initValues(needSpecialValuesIdx);
            userLevels  = repmat(SettingsLevel.User, size(keysToSet));

            nodeFacade.addKeys(keysToAdd);
            nodeFacade.setKeys(keysToSet, valuesToSet, userLevels);
            nodeFacade.unsetKeys(keysToSet, userLevels);
        end
    end
    
    methods (Access = private, Sealed)
        function value = getSinglePropertyWithLevels(objOrObjs, prop, level)
        % Get a single prop at a particular level.  
            import parallel.internal.types.SettingsLevel
            
            action = 'get';
            iErrorIfPropAndLevelNotString(objOrObjs, prop, level, action);
            level = SettingsLevel.fromNameI(level);
            SettingsLevel.errorIfLevelsNotGettable(level);
            userSpecifiedLevel = true;
            objOrObjs.errorIfPropsNotGettable(prop, action, userSpecifiedLevel);
            
            value = cell(numel(objOrObjs), 1);
            try
                for ii = 1:length(objOrObjs)
                    value{ii} = objOrObjs(ii).hGetProperty(prop, level); 
                end
            catch err
                ex = MException(message('parallel:settings:GetAtLevelFailed', prop, level.Name));
                ex = ex.addCause(err);
                throw(ex);
            end
            % Return a cell only if there were multiple objs
            if numel(objOrObjs) == 1
                value = value{1};
            end
        end
        
        function value = setSinglePropertyWithLevels(objOrObjs, prop, value, level)
            import parallel.internal.customattr.Reflection
            import parallel.internal.types.SettingsLevel
            
            action = 'set';
            iErrorIfPropAndLevelNotString(objOrObjs, prop, level, action);
            level = SettingsLevel.fromNameI(level);
            SettingsLevel.errorIfLevelsNotSettable(level);
            userSpecifiedLevel = true;
            objOrObjs.errorIfPropsNotSettable(prop, action, userSpecifiedLevel);
            try
                for ii = 1:length(objOrObjs)
                    Reflection.deriveAndCheckConstraint(objOrObjs(ii), prop, value);
                    objOrObjs(ii).hSetProperty(prop, value, level);
                end
            catch err
                ex = MException(message('parallel:settings:SetAtLevelFailed', prop, level.Name));
                ex = ex.addCause(err);
                throw(ex);
            end
        end

        function errorIfPropsNotGettable(obj, props, action, userSpecifiedLevel)
            gettableProps = obj.hGetPublicSettingsProperties();
            iErrorIfGetSetNotAllowed(gettableProps, props, class(obj), action, userSpecifiedLevel);
        end

        function errorIfPropsNotSettable(obj, props, action, userSpecifiedLevel)
            [~, settableProps] = obj.hGetPublicSettingsProperties();
            iErrorIfGetSetNotAllowed(settableProps, props, class(obj), action, userSpecifiedLevel);
        end
        
        function verifyAndCorrectPropertyValues(obj)
            [~, ~, props, constraints] = iGetSettingsProps(obj);
            
            % Ensure these cell arrays are all column-oriented
            props = props(:);
            constraints = constraints(:);
            
            values = hGetProperty(obj, props);
            values = values(:);
            
            % only bother with the non-default values
            nonDefault = cellfun(@(x) ~parallel.settings.DefaultValue.isDefault(x), values);
            
            correctedValues = values;
            % Validate all properties against their constraints in a batch
            [~, correctedValues(nonDefault)] = cellfun(@(constraintType, propertyName, value) ...
                parallel.internal.customattr.validateSimpleConstraint(constraintType, propertyName, value, true), ...
                constraints(nonDefault), props(nonDefault), values(nonDefault), ...
                'UniformOutput', false);
            
            % seek out the values that were actually changed
            changed = cellfun(@(x, y) ~isequal(x, y), values, correctedValues);
            % seek out the default values AFTER we changed some of them
            default = cellfun(@(x) parallel.settings.DefaultValue.isDefault(x), correctedValues);
            
            % set the new values that have changed and are non-default
            nonDefaultAndChanged = changed & ~default;
            hSetProperty(obj, props(nonDefaultAndChanged), correctedValues(nonDefaultAndChanged));
            
            % unset the new values that have been reverted to the default
            defaultAndChanged = changed & default;
            levels = repmat(parallel.internal.types.SettingsLevel.User, size(props(defaultAndChanged)));
            obj.NodeFacade.unsetKeys(props(defaultAndChanged), levels);
        end
    end
    
    methods (Access = 'protected')
        function supportVec = checkVectorizedSetSupport( ~ )
            % Flag to indicate whether this class supports vectorized sets over
            % multiple instances. By default this flag is false and sets will
            % be iterated over. Override this method in child classes which
            % support vectorized sets.
	    % Classes which override this to return true must support calls to 
	    % hSetProperty and hSetPropertyNoCheck with multiple instances.
            supportVec = false;
        end
    end
end

%-------------------------------------------------------
function iErrorIfGetSetNotAllowed(allowedProps, props, className, action, userSpecifiedLevel)
badProps = setdiff(props, allowedProps);
if isempty(badProps)
    return;
end

badPropsStr = strtrim(sprintf('%s ', badProps{:}));
if userSpecifiedLevel
    assert(~iscellstr(props)) %#ok<ISCLSTR>
    throwAsCaller(MException(message('parallel:settings:GetSetActionWithLevelNotAllowedForProperty', action, badPropsStr, className)));
else
    throwAsCaller(MException(message('parallel:settings:GetSetActionNotAllowedForProperty', action, className, badPropsStr)));
end
end

%-------------------------------------------------------
function [gettableProps, settableProps, allSettingsProps, allSettingsConstraints] = iGetSettingsProps(obj)
classObj = metaclass(obj);
[gettableProps, settableProps, allSettingsProps, allSettingsConstraints] = ...
    parallel.internal.settings.getPropertiesAndConstraints(classObj);
end

%-------------------------------------------------------
function iErrorIfPropAndLevelNotString(objOrObjs, prop, level, action)
if iscellstr(prop) %#ok<ISCLSTR> only support cellstrs
    throwAsCaller(MException(message('parallel:settings:GetSetVectorizedActionWithLevelNotAllowed', action, class( objOrObjs ))));
end
if ~(ischar(prop) && isrow(prop))
    throwAsCaller(MException(message('parallel:settings:GetSetActionWithLevelPropNotString', action)));
end
if ~(ischar(level) && isrow(level))
    throwAsCaller(MException(message('parallel:settings:GetSetActionLevelNotString', action)));
end
end
%--------------------------------------------------------------------------
% iVectorisedGetHelper: helper function for bulk getting of access
% properties
function [val, handled] = iVectorisedGetHelper(obj, name, alreadyHandled, existingValue)
    if alreadyHandled
        val = existingValue;
        handled = alreadyHandled;
    else
        if numel(obj) > 1
            val = {obj.(name)}';
        else
            val = obj.(name);
        end
        handled = true;
    end
end

