% SettingsDataMassager - Class to provide massage data as required for storage in settings.

%   Copyright 2011-2021 The MathWorks, Inc.

classdef SettingsDataMassager

    properties (Constant, GetAccess = private)
        % Constraints that must be converted before they can stored in Settings
        NeedConversionConstraints = { ...
            'datalocation' ...      % datalocation could be a struct or string
            'callback' ...          % callbacks can be strings, function handles or cell arrays
            'workerlimits' ...      % need to deal with dynamic sizing of worker limits
            'numericscalar' ...     % numeric types must be stored as doubles
            'positivescalar'...     % numeric types must be stored as doubles
            'positiveintscalar' ... % numeric types must be stored as doubles
            'portrange' ...         % numeric types must be stored as doubles
            'additionalproperties'  % additionalproperties is a dynamic class with numeric, logical, and string properties
            }

        % Constraints that require a special initial value when the key is added
        NeedSpecialInitValueConstraints = { ...
            'cellstr' ...          % cellstrs must be initialized to {} to allow dynamic sizing
            'datalocation' ...     % datalocation will be stored as a cellstr with 1 or 2 entries
            }
        SpecialInitValues = { ...
            {} ...                 % cellstrs must be initialized to {} to allow dynamic sizing
            {} ...                 % data locations must be initialized to {} to allow dynamic sizing
            }
    
    end

    methods (Static)
        function values = convertToSettings(props, values, constraints)
        % Convert values into a Settings-friendly format
            import parallel.internal.settings.SettingsDataMassager
            if iscell(props)
                returnCell = true;
                validateattributes(props,       {'cell'}, {});
                validateattributes(values,      {'cell'}, {'numel', numel(props)});
                validateattributes(constraints, {'cell'}, {'numel', numel(props)});
            else
                validateattributes(props,       {'char'}, {'row'});
                validateattributes(constraints, {'char'}, {'row'});
                props = {props};
                values = {values};
                constraints = {constraints};
                returnCell = false;
            end
            
            valuesToConvertIdx = cellfun(@(x) any(strcmp(x, SettingsDataMassager.NeedConversionConstraints)), ...
                constraints);
            if any(valuesToConvertIdx)
                valuesToConvert      = values(valuesToConvertIdx);
                convertedValues      = cell(size(valuesToConvert));
                propNamesToConvert   = props(valuesToConvertIdx);
                constraintsToConvert = constraints(valuesToConvertIdx);
                for ii = 1:numel(valuesToConvert)
                    currValueToConvert = valuesToConvert{ii};
                    currPropName       = propNamesToConvert{ii};
                    currConstraint     = constraintsToConvert{ii};
                    try
                        convertedValues{ii} = iConvertValueForStorage(currValueToConvert, currConstraint);
                    catch err
                        ex = MException(message('parallel:settings:GetSetFailedToConvertValueForStorage', class( currValueToConvert ), currPropName));
                        ex = ex.addCause(err);
                        throw(ex);
                    end
                end
                values(valuesToConvertIdx) = convertedValues;
            end
            if ~returnCell
                values = values{1};
            end
        end

        function values = convertFromSettings(props, values, constraints)
        % Convert values from their Settings-friendly format into something
        % that we actually want.
            import parallel.internal.settings.SettingsDataMassager
            if iscell(props)
                returnCell = true;
                validateattributes(props,       {'cell'}, {});
                validateattributes(values,      {'cell'}, {'numel', numel(props)});
                validateattributes(constraints, {'cell'}, {'numel', numel(props)});
            else
                validateattributes(props,       {'char'}, {'row'});
                validateattributes(constraints, {'char'}, {'row'});
                props = {props};
                values = {values};
                constraints = {constraints};
                returnCell = false;
            end
            
            nonDefaultValues = cellfun(@(x) ~parallel.settings.DefaultValue.isDefault(x), values);
            needConversion = cellfun(@(x) any(strcmp(x, SettingsDataMassager.NeedConversionConstraints)), ...
                constraints);
            valuesToConvertIdx = nonDefaultValues & needConversion;
            if any(valuesToConvertIdx)
                valuesToConvert      = values(valuesToConvertIdx);
                convertedValues      = cell(size(valuesToConvert));
                propNamesToConvert   = props(valuesToConvertIdx);
                constraintsToConvert = constraints(valuesToConvertIdx);
                for ii = 1:numel(valuesToConvert)
                    currValueToConvert = valuesToConvert{ii};
                    currPropName       = propNamesToConvert{ii};
                    currConstraint     = constraintsToConvert{ii};
                    try
                        currValue = iConvertStoredSettingToValue(currValueToConvert, currConstraint);
                        % Check the converted value actually satisfies the constraint
                        parallel.internal.customattr.checkSimpleConstraint(currConstraint, currPropName, currValue);
                        convertedValues{ii} = currValue;
                    catch err
                        ex = MException(message('parallel:settings:GetSetFailedToConvertStoredSettingToValue', currPropName, currConstraint));
                        ex = ex.addCause(err);
                        throw(ex);
                    end
                end
                values(valuesToConvertIdx) = convertedValues;
            end
            
            if ~returnCell
                values = values{1};
            end
        end
        
        function [needSpecialValues, initValues] = getInitSettingsValuesFromConstraints(constraints)
            if iscell(constraints)
                validateattributes(constraints, {'cell'}, {});
                returnCell = true;
            else
                validateattributes(constraints, {'char'}, {'row'});
                constraints = {constraints};
                returnCell = false;
            end
            
            [needSpecialValues, initValues] = cellfun(@(x) iGetSpecialInitValue(x), constraints, ...
                'UniformOutput', false);
            needSpecialValues = [needSpecialValues{:}];
            if ~returnCell
                initValues = initValues{1};
            end
        end
    end
end

%-------------------------------------------------------
function [needsSpecialValue, initValue] = iGetSpecialInitValue(constraint)
import parallel.internal.settings.SettingsDataMassager
validateattributes(constraint, {'char'}, {'row'});

specialValueIdx = find(strcmp(constraint, SettingsDataMassager.NeedSpecialInitValueConstraints), 1, 'first');

if isempty(specialValueIdx)
    needsSpecialValue = false;
    initValue = [];
else
    needsSpecialValue = true;
    initValue = SettingsDataMassager.SpecialInitValues{specialValueIdx};
end
end



%-------------------------------------------------------
function storedValue = iConvertValueForStorage(value, constraint)
storedValue = value;
switch constraint
    case 'datalocation'
        % Store datalocation as a cellstr with 1 or two entries.  
        % If 2 entries, then 1st entry is unix, 2nd is windows.
        if isstruct(value)
            storedValue = {value.unix, value.windows};
        else
            storedValue = {value};
        end
    case {'callback', 'additionalproperties'}
        % TODO:later store in a saner way to support UI?
        serializedVal = distcompserialize(value);
        storedValue = matlab.net.base64encode(serializedVal);
    case 'workerlimits'
        % WorkerLimits can be a 1x1 or 1x2 vector of doubles.  However, 
        % keys that are first set to a double array of a certain size cannot 
        % later be set to a double array of a larger size so we will  store 
        % WorkerLimits as a 1x3 vector of doubles.  The first element of
        % the value indicates how many elements the user actually specified
        % NB numeric values should be stored as doubles
        storedValue = [numel(value), double(value)];
        if numel(value) == 1
            % add a dummy value onto the end to pad the storedValue out to 3
            dummyVal = double(value);
            storedValue = [storedValue, dummyVal];
        end
    case {'numericscalar', 'positivescalar', 'positiveintscalar', 'portrange'}
        % NB numeric values should be stored as doubles
        storedValue = double(value);
    otherwise
        % do nothing
end
end

%-------------------------------------------------------
function value = iConvertStoredSettingToValue(storedValue, constraint)
% NB numeric values will always be returned as doubles
value = storedValue;
switch constraint
    case 'datalocation'
        % Store datalocation as a cellstr with 1 or two entries.  
        % If 2 entries, then 1st entry is unix, 2nd is windows.
        if numel(storedValue) == 2
            value = struct('unix', storedValue{1}, 'windows', storedValue{2});
        else
            value = storedValue{1};
        end
    case {'callback', 'additionalproperties'}
        % TODO:later store in a saner way to support UI?
        serializedVal = matlab.net.base64decode(storedValue);
        value = distcompdeserialize(serializedVal);
    case 'workerlimits'
        if storedValue(1) == 1
            value = storedValue(2);
        else
            value = storedValue(2:end);
        end
    otherwise
        % do nothing
end
end
