% MJSSchedulerPropertiesAdaptor - Helper class for
% parallel.internal.cluster.MJSAccess for invoking property gets and sets.
%
% The results of a single property get and the arguments for a single
% property set are encoded in a MapStringAny schema which has the fields:
%
%   key: propertyName
%   value: propertyValues
%
% where key is a string and value is some other schema message holding the
% property values.
% Each instance of MJSSchedulerPropertiesAdaptor encodes:
%   - the name of the schema to populate "value" with
%   - function to convert MATLAB property values into the schema values
%   - function to convert schema values into the MATLAB property values
%
% Static maps (TaskMap, JobMap, ClusterMap) map property names on the
% scheduler to the corresponding MJSSchedulerPropertiesAdaptor to use.
%
% Static helper methods adapt<X>SetArguments use these maps to adapt property
% names and values into MapStringAny collections.
% Static helper methods adapt<X>GetResults use these maps to adapt 
% MapStringAny collections into property names and values.

% Copyright 2020-2021 The MathWorks, Inc.

classdef (Sealed, Hidden) MJSSchedulerPropertiesAdaptor < handle
    
    properties (GetAccess = private, SetAccess = immutable)       
        % Name of schema that will hold adapted set/get values
        PropertiesSchemaName
        
        % Method to convert struct value to MATLAB get value
        AdaptGetPropertyValueFcn
        
        % Method to convert MATLAB set value to required struct
        % value
        AdaptSetPropertyValueFcn
        
        % Authentication level required to get a property
        GetAuthenticationLevel
        
        % Authentication level required to set a property
        SetAuthenticationLevel
        
        % Flag derived from PropertiesSchemaName indicating whether schema
        % values should be collected as uniform output or not.
        % See iCheckUniformOutput.
        UniformOutput
    end
    
    properties (Constant, GetAccess = private)
        % Constant maps of property name to MJSSchedulerPropertiesAdaptor instance.
        TaskMap    = iTaskMap();
        JobMap     = iJobMap();
        ClusterMap = iClusterMap();
    end
    
    methods (Access = private)
        function obj = MJSSchedulerPropertiesAdaptor(schemaName, getFcn, setFcn, ...
                getAuthenticationLevel, setAuthenticationLevel)
            import parallel.internal.cluster.MJSSchemaHelper;

            % Define the default arguments for most common use case:
            % Gettable-only property that requires no conversion.
            if nargin < 2
                % Default is no adaption for get value
                getFcn = @iNoop;
            end
            
            if nargin < 3
                % Default is property is not settable
                setFcn = @iInvalidSet;
            end
            
            if nargin < 4
                % Default get authentication level is Any
                getAuthenticationLevel = parallel.internal.cluster.types.AccessLevel.Any;
            end
            
            if nargin < 5
                % Default set authentication level is Owner
                setAuthenticationLevel = parallel.internal.cluster.types.AccessLevel.Owner;
            end
            
            validateattributes(schemaName, {'char', 'string'}, {'scalartext'});
            validateattributes(getFcn, {'function_handle'}, {'scalar'});
            validateattributes(setFcn, {'function_handle'}, {'scalar'});
            validateattributes(getAuthenticationLevel, {'parallel.internal.cluster.types.AccessLevel'}, {'scalar'});
            validateattributes(setAuthenticationLevel, {'parallel.internal.cluster.types.AccessLevel'}, {'scalar'});
            
            obj.PropertiesSchemaName = MJSSchemaHelper.getFullName(schemaName);
            obj.AdaptGetPropertyValueFcn = getFcn;
            obj.AdaptSetPropertyValueFcn = setFcn;
            obj.GetAuthenticationLevel = getAuthenticationLevel;
            obj.SetAuthenticationLevel = setAuthenticationLevel;
            
            obj.UniformOutput = iCheckUniformOutput(schemaName);
        end
        
        function mapStringAnySchema = buildMapStringAnySchema(obj, propertyName, adaptedValues)
            % Given a property name and values, populate a MapStringAny
            % schema where 'key' is the propertyName and 'value' is a schema
            % identified by obj.PropertiesSchemaName with 'values' set to
            % adaptedValues.
            import parallel.internal.cluster.MJSSchemaHelper;
            
            if obj.UniformOutput
                adaptedValues = cell2mat(adaptedValues);
            else
                % Avoid struct creation with cell array of values creating
                % an array of structs
                adaptedValues = {adaptedValues};
            end
            % Build obj.PropertiesSchemaName schema to hold property values
            valuesSchemaStruct = struct('values', adaptedValues, MJSSchemaHelper.TypeUrlField, ...
                obj.PropertiesSchemaName);
            % Build key-value MapStringAny schema from property name and
            % values
            mapStringAnySchema = struct('key', propertyName, ...
                'value', valuesSchemaStruct);
        end
        
        function adaptedValues = decomposeMapStringAnySchema(obj, mapStringAnySchema)
            % Given a MapStringAny schema where 'key' is the propertyName 
            % and 'value' is a schema identified by obj.PropertiesSchemaName
            % with 'values' set to property values, validate the expected
            % schema and extract the property values.
            import parallel.internal.cluster.MJSSchemaHelper;
            
            validateattributes(mapStringAnySchema, {'struct'}, {'scalar'});
            valuesSchemaStruct = mapStringAnySchema.value;
            MJSSchemaHelper.validateFullMessageName(valuesSchemaStruct, obj.PropertiesSchemaName);
            
            adaptedValues = valuesSchemaStruct.values;
            if obj.UniformOutput
                adaptedValues = num2cell(adaptedValues);
            end
        end
    end
    
    methods (Static)
        function [propertyNames, adaptedValues] = adaptClusterGetResults(propertyArgsStructs, deserializableData)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            [propertyNames, adaptedValues] = MJSSchedulerPropertiesAdaptor.adaptGetResults(...
                MJSSchedulerPropertiesAdaptor.ClusterMap, propertyArgsStructs, deserializableData);
        end
        
        function [propertyNames, adaptedValues] = adaptTaskGetResults(propertyArgsStructs, deserializableData)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            [propertyNames, adaptedValues] = MJSSchedulerPropertiesAdaptor.adaptGetResults(...
                MJSSchedulerPropertiesAdaptor.TaskMap, propertyArgsStructs, deserializableData);
        end
        
        function [propertyNames, adaptedValues] = adaptJobGetResults(propertyArgsStructs, deserializableData)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            [propertyNames, adaptedValues] = MJSSchedulerPropertiesAdaptor.adaptGetResults(...
                MJSSchedulerPropertiesAdaptor.JobMap, propertyArgsStructs, deserializableData);
        end
        
        function propertyArgsStructs = adaptClusterSetArguments(propertyNames, values, serializableDataArray)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            propertyArgsStructs = MJSSchedulerPropertiesAdaptor.adaptSetArguments(...
                MJSSchedulerPropertiesAdaptor.ClusterMap, propertyNames, values, serializableDataArray);
        end
        
        function propertyArgsStructs = adaptTaskSetArguments(propertyNames, values, serializableDataArray)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            propertyArgsStructs = MJSSchedulerPropertiesAdaptor.adaptSetArguments(...
                MJSSchedulerPropertiesAdaptor.TaskMap, propertyNames, values, serializableDataArray);
        end
        
        function propertyArgsStructs = adaptJobSetArguments(propertyNames, values, serializableDataArray)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            propertyArgsStructs = MJSSchedulerPropertiesAdaptor.adaptSetArguments(...
                MJSSchedulerPropertiesAdaptor.JobMap, propertyNames, values, serializableDataArray);
        end
        
        function levels = getClusterPropertyGetAccessLevels(propertyNames)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            levels = MJSSchedulerPropertiesAdaptor.getPropertyGetAccessLevels(...
                MJSSchedulerPropertiesAdaptor.ClusterMap, propertyNames);
        end
        
        function levels = getTaskPropertyGetAccessLevels(propertyNames)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            levels = MJSSchedulerPropertiesAdaptor.getPropertyGetAccessLevels(...
                MJSSchedulerPropertiesAdaptor.TaskMap, propertyNames);
        end
        
        function levels = getJobPropertyGetAccessLevels(propertyNames)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            levels = MJSSchedulerPropertiesAdaptor.getPropertyGetAccessLevels(...
                MJSSchedulerPropertiesAdaptor.JobMap, propertyNames);
        end
        
        function levels = getClusterPropertySetAccessLevels(propertyNames)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            levels = MJSSchedulerPropertiesAdaptor.getPropertySetAccessLevels(...
                MJSSchedulerPropertiesAdaptor.ClusterMap, propertyNames);
        end
        
        function levels = getTaskPropertySetAccessLevels(propertyNames)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            levels = MJSSchedulerPropertiesAdaptor.getPropertySetAccessLevels(...
                MJSSchedulerPropertiesAdaptor.TaskMap, propertyNames);
        end
        
        function levels = getJobPropertySetAccessLevels(propertyNames)
            import parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
            levels = MJSSchedulerPropertiesAdaptor.getPropertySetAccessLevels(...
                MJSSchedulerPropertiesAdaptor.JobMap, propertyNames);
        end
    end
    
    methods (Static, Access = private)        
        % Common code for adapting get method results
        function [propertyNames, adaptedValues] = adaptGetResults(adaptorMap, propertyArgsStructs, deserializableDataArray)            
            % Extract property names
            propertyNames = arrayfun(@(s) s.key, propertyArgsStructs, 'UniformOutput', false);
            % Get property adaptors by name
            adaptors = iGetPropertyAdaptors(adaptorMap, propertyNames);
            
            % Extract values from MapStringAny schema struct array
            values = cell(size(adaptors));
            for idx = 1:numel(values)
                values{idx} = adaptors{idx}.decomposeMapStringAnySchema(propertyArgsStructs(idx));
            end
            numTargets = iAssertCommonNumValues(values);
            
            % Process values in the order required by the scheduler:
            % foreach target
            %   foreach property
            % This is required to process deserializableDataArray entries
            % in the same order they are written by the remote end
            numProps = numel(propertyNames);
            adaptedValues = cell(1, numProps);
            for ip = 1:numProps
                adaptedValues{ip} = cell(1, numTargets);
            end
            for it = 1:numTargets
                for ip = 1:numProps
                    adaptedValues{ip}{it} = adaptors{ip}.AdaptGetPropertyValueFcn(values{ip}{it}, deserializableDataArray);
                end
            end
        end
                
        % Common code for adapting set method arguments
        function propertyArgsStructs = adaptSetArguments(adaptorMap, propertyNames, values, serializableDataArray)
            % Check that the number of properties matches the size of
            % values
            numProps = numel(propertyNames);
            assert(numProps == numel(values), 'Number of properties to set must match number of values provided');
            numTargets = iAssertCommonNumValues(values);
            
            % Get property adaptors by name
            adaptors = iGetPropertyAdaptors(adaptorMap, propertyNames);
            
            % Process values in the order required by the scheduler:
            % foreach target
            %   foreach property
            % This is required for entries to be appended to
            % serializableDataArray in the same order they are read by the
            % remote end
            adaptedValues = cell(1, numProps);
            for ip = 1:numProps
                adaptedValues{ip} = cell(1, numTargets);
            end
            for it = 1:numTargets
                for ip = 1:numProps
                    adaptedValues{ip}{it} = adaptors{ip}.AdaptSetPropertyValueFcn(values{ip}{it}, serializableDataArray);
                end
            end
            
            % Collect adapted values in MapStringAny schema struct array
            propertyArgsStructs = arrayfun(@(ip) ...
                adaptors{ip}.buildMapStringAnySchema(propertyNames{ip}, adaptedValues{ip}), 1:numProps);
        end
        
        function levels = getPropertyGetAccessLevels(adaptorMap, propertyNames)
            adaptors = iGetPropertyAdaptors(adaptorMap, propertyNames);
            levels = cellfun(@(adaptor) adaptor.GetAuthenticationLevel, adaptors);
        end
        
        function levels = getPropertySetAccessLevels(adaptorMap, propertyNames)
            adaptors = iGetPropertyAdaptors(adaptorMap, propertyNames);
            levels = cellfun(@(adaptor) adaptor.SetAuthenticationLevel, adaptors);
        end
    end
    
    % Stand alone conversion helper methods
    methods (Static)
        function adaptedValues = adaptJobStatusData(result)
            adaptedValues = arrayfun(@iAdaptJobStatusData, result.values);
        end
        
        function adaptedValues = adaptJobIDNumAndType(values)
            adaptedValues = arrayfun(@iAdaptJobIDNumAndType, values);
        end
        
        function adaptedValues = adaptTaskIDAndNum(values)
            adaptedValues = values;
        end
    end
end

function adaptors = iGetPropertyAdaptors(map, propNames)
    if any(~isKey(map, propNames))
        assert(false, 'Unknown scheduler properties: %s', strjoin(propNames(~isKey(map, propNames)), ', '));
    end
    adaptors = map.values(propNames);
end

function numTargets = iAssertCommonNumValues(values)
    % Validate that all property values contain the same number of
    % entries, matching the number of targets
    entrySizes = unique(cellfun(@numel, values));
    assert(numel(entrySizes) == 1, 'Number of values for each property must be equal');
    numTargets = entrySizes(1);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Populate map for common workunit properties
function m = iWorkUnitMap(m)
    ctor = @parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
    m('AuthorizedUsers') = ctor('StringArrayGetSet', @iAdaptArrayValue, @iPackCellArrayValues);
    m('CreateDateTime') = ctor('LongGetSet');
    m('CreateTime') = ctor('StringGetSet');
    m('FinishDateTime') = ctor('LongGetSet');
    m('FinishTime') = ctor('StringGetSet');
    m('ID') = ctor('LongGetSet');
    m('Name') = ctor('StringGetSet', @iNoop, @iNoop);
    m('StartDateTime') = ctor('LongGetSet');
    m('StartTime') = ctor('StringGetSet');
    m('StateEnum') = ctor('IntGetSet');
    m('Timeout') = ctor('LongGetSet', @iNoop, @iNoop);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Build the map for Tasks
function m = iTaskMap()
    import parallel.internal.cluster.types.AccessLevel;
    
    m = containers.Map;
    ctor = @parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
    m = iWorkUnitMap(m);
    m('CaptureDiary') = ctor('BoolGetSet', @iNoop, @iNoop);
    m('DependentFiles') = ctor('StringArrayGetSet', @iAdaptArrayValue,@iPackCellArrayValues);
    m('Diary') = ctor('StreamingDataGetSet', @iDeserializePlain, @iInvalidSet, AccessLevel.Owner);
    m('ErrorIdentifier') = ctor('StringGetSet');
    m('ErrorMessage') = ctor('StringGetSet');
    m('ErrorStruct') = ctor('BytesGetSet', @iDeserMxArrayDefaultParallelException);
    m('FailureInfo') = ctor('FailedTaskAttemptInfoArrayGetSet', @iAdaptFailedTaskAttemptInfoArray, @iInvalidSet, AccessLevel.Owner);
    m('Function') = ctor('StreamingDataGetSet', @iDeserMxArrayDefaultFunction, @iSerializeMxArray, AccessLevel.Owner);
    m('FunctionName') = ctor('StringGetSet', @iDefaultFunctionName, @iNoop);
    m('InputArguments') = ctor('StreamingDataGetSet', @iDeserMxArrayDefautInputArgs, @iSerializeMxArray, AccessLevel.Owner);
    m('JobID') = ctor('UuidGetSet');
    m('MaximumRetries') = ctor('IntGetSet', @iNoop, @iNoop);
    m('NumFailures') = ctor('IntGetSet');
    m('NumOutputArguments') = ctor('IntGetSet', @iNoop, @iNoop);
    m('OutputArguments') = ctor('StreamingDataGetSet', @iDeserMxArrayDefaultCell, @iSerializeMxArray, AccessLevel.Owner);
    m('ParallelPortInfo') = ctor('ParallelPortInfoGetSet', @iNoop, @iNoop);
    m('PoolConnectionInfo') = ctor('PoolConnectionInfoGetSet', @iNoop, @iNoop);
    m('Warnings') = ctor('StreamingDataGetSet', @iDeserializeMxArrayDefaultWarning, @iInvalidSet, AccessLevel.Owner);
    m('WorkerProperties') = ctor('WorkerPropertiesGetSet');
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Build the map for Job information
function m = iJobMap()
    import parallel.internal.cluster.types.AccessLevel;

    m = containers.Map();
    ctor = @parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
    m = iWorkUnitMap(m);
    m('AdditionalPaths') = ctor('StringArrayGetSet', @iAdaptArrayValue, @iPackCellArrayValues);
    m('ApiTag') = ctor('StringGetSet', @iNoop, @iNoop);
    m('AutoAttachFiles') = ctor('BoolGetSet', @iNoop, @iNoop);
    m('AttachedFileData') = ctor('StreamingDataGetSet', @iDeserializePlain, @iSerializePlain, AccessLevel.Owner);
    m('AttachedFilePaths') = ctor('StreamingDataGetSet', @iDeserMxArrayDefaultDouble, @iSerializeMxArray, AccessLevel.Owner);
    m('AttachedFiles') = ctor('StringArrayGetSet', @iAdaptArrayValue, @iPackCellArrayValues);
    m('AutoAddClientPath') = ctor('BoolGetSet', @iNoop, @iNoop);
    m('DependentFiles') = ctor('StringArrayGetSet', @iAdaptArrayValue, @iPackCellArrayValues);
    m('ExecutionMode') = ctor('IntGetSet', @iNoop, @iNoop);
    m('EnvironmentVariables') = ctor('StringArrayGetSet', @iAdaptArrayValue, @iPackCellArrayValues);
    m('JobData') = ctor('StreamingDataGetSet', @iDeserMxArrayDefaultDouble, @iSerializeMxArray, AccessLevel.Owner);
    m('JobStatusData') = ctor('JobStatusDataGetSet', @iAdaptJobStatusData);
    m('LeadingTaskNum') = ctor('LongGetSet');
    m('MATLABDrivePathMap') = ctor('StreamingDataGetSet', @iDeserMxArrayDefaultDouble, @iSerializeMxArray, AccessLevel.Owner);
    m('MATLABRelease') = ctor('StringGetSet');
    m('MaxNumWorkers') = ctor('IntGetSet', @iNoop, @iNoop);
    m('MinNumWorkers') = ctor('IntGetSet', @iNoop, @iNoop);
    m('NumLabs') = ctor('IntGetSet');
    m('NumPathsToAppend') = ctor('IntGetSet', @iNoop, @iNoop);
    m('NumThreads') = ctor('IntGetSet', @iNoop, @iNoop);
    m('ParallelTag') = ctor('StringGetSet', @iNoop, @iNoop);
    m('ProductList') = ctor('BytesGetSet', @iNoop, @iNoop);
    m('RestartWorker') = ctor('BoolGetSet', @iNoop, @iNoop);
    m('SubmitDateTime') = ctor('LongGetSet');
    m('SubmitTime') = ctor('StringGetSet');
    m('Tag') = ctor('StringGetSet', @iNoop, @iNoop);
    m('Tasks') = ctor('TaskIDAndNumArrayGetSet', @iAdaptArrayValue);
    m('Username') = ctor('StringGetSet', @iNoop, @iNoop);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Build the map for Cluster (MJS) information
function m = iClusterMap()
    import parallel.internal.cluster.types.AccessLevel;

    m = containers.Map();
    ctor = @parallel.internal.cluster.MJSSchedulerPropertiesAdaptor;
    m('AdminUsername') = ctor('StringGetSet');
    m('AllHostAddresses') = ctor('StringArrayGetSet', @iAdaptArrayValue);
    m('AllowClientPasswordCache') = ctor('BoolGetSet');
    m('AuthorisationLevel') = ctor('AuthorizationLevelGetSet', @iAdaptAuthorizationLevel);
    m('BusyWorkers') = ctor('WorkerPropertiesArrayGetSet', @iAdaptArrayValue);
    m('ClusterLogLevel') = ctor('IntGetSet', @iNoop, @iNoop, AccessLevel.Any, AccessLevel.Admin);
    m('ClusterLogSpec') = ctor('StringGetSet', @iNoop, @iNoop, AccessLevel.Any, AccessLevel.Admin);
    m('ClusterMatlabRoot') = ctor('StringGetSet');
    m('ClusterOperatingSystem') = ctor('StringGetSet');
    m('HasSecureCommunication') = ctor('BoolGetSet');
    m('HostName') = ctor('StringGetSet');
    m('IdleWorkers') = ctor('WorkerPropertiesArrayGetSet', @iAdaptArrayValue);
    m('IsRunAsUser') = ctor('BoolGetSet');
    m('Jobs') = ctor('JobIDNumAndTypeArrayGetSet', @iAdaptJobIDNumAndTypeArray);
    m('LookupURL') = ctor('StringGetSet');
    m('MaxCapacity') = ctor('IntArrayGetSet', @iAdaptArrayValue, @iPackArrayValues, AccessLevel.Any, AccessLevel.Admin);
    m('MpiLibraryName') = ctor('StringGetSet');
    m('Name') = ctor('StringGetSet');
    m('NumBusyWorkers') = ctor('IntGetSet');
    m('NumIdleWorkers') = ctor('IntGetSet');
    m('OperatingSystem') = ctor('StringGetSet');
    m('RequiresOnlineLicensing') = ctor('BoolGetSet');
    m('SecurityLevel') = ctor('IntGetSet');
    m('StateEnum') = ctor('IntGetSet');
    m('SupportedReleases') = ctor('StringArrayGetSet', @iAdaptArrayValue);
    m('WorkerPool') = ctor('WorkerPoolPropertiesGetSet');
    m('WorkerProxiesPoolConnections') = ctor('BoolGetSet');
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Return flag indicating if schema values are stored as array or cell array
function uniformOutput = iCheckUniformOutput(schemaName)
    nonUniformOutputSchames = {'BytesGetSet', 'StringGetSet'};
    uniformOutput = ~ismember(schemaName, nonUniformOutputSchames);
end

% Noop function if no conversion is required
function adaptedValue = iNoop(value, ~)
    adaptedValue = value;
end

% Return the default task function name if value is empty
function adaptedValue = iDefaultFunctionName(value, ~)
    if isempty(value)
        adaptedValue = char(@parallel.internal.pool.poolWorkerFcn);
    else
        adaptedValue = value;
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% AdaptSetPropertyValueFcn methods
% All functions have the signature:
% adaptedValue = AdaptSetPropertyValueFcn(value, serializableDataArray)

% Function which throws if a property cannot be set
function adaptedValue = iInvalidSet(~, ~) %#ok<STOUT>
    error(message('parallel:cluster:MJSUnableToSetProperty'));
end

function adaptedValue = iPackArrayValues(values, ~)
    adaptedValue = struct('values', values);
end

function adaptedValue = iPackCellArrayValues(cellValues, ~)
    adaptedValue = iPackArrayValues({cellValues});
end

function value = iPackStreamingData(index)
    % MATLAB 1-based indexing to 0-based
    value = struct('index', index - 1);
end

function adaptedValue = iSerializeMxArray(value, serializableDataArray)
    import parallel.internal.cluster.SerializableData
    index = serializableDataArray.put(SerializableData.createMxArray(value));
    adaptedValue = iPackStreamingData(index);
end

function adaptedValue = iSerializePlain(value, serializableDataArray)
    import parallel.internal.cluster.SerializableData
    index = serializableDataArray.put(SerializableData.createPlain(value));
    adaptedValue = iPackStreamingData(index);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% AdaptGetPropertyValueFcn methods
% All functions have the signature:
% adaptedValue = AdaptGetPropertyValueFcn(value, deserializableDataArray)
function adaptedValue = iAdaptArrayValue(arrayValue, ~)
    adaptedValue = arrayValue.values;
end

function index = iAdaptStreamingData(value)
    % 0-based indexing to MATLAB 1-based
    index = value.index + 1;
end

function adaptedValue = iDeserMxArrayWithDefaultEmpty(value, deserializableDataArray, defaultEmpty)
    index = iAdaptStreamingData(value);
    data = deserializableDataArray(index);
    adaptedValue = data.deserializeMxArray(defaultEmpty);
end

function adaptedValue = iDeserMxArrayDefaultDouble(value, deserializableDataArray)
    adaptedValue = iDeserMxArrayWithDefaultEmpty(value, deserializableDataArray, []);
end

function adaptedValue = iDeserMxArrayDefaultCell(value, deserializableDataArray)
    adaptedValue = iDeserMxArrayWithDefaultEmpty(value, deserializableDataArray, cell(1,0));
end

function adaptedValue = iDeserMxArrayDefaultParallelException(errorBytes, ~)
    if ~isempty(errorBytes)
        adaptedValue = distcompdeserialize(errorBytes);
    else
        adaptedValue = ParallelException.empty();
    end
end

function adaptedValue = iDeserializeMxArrayDefaultWarning(value, deserializableDataArray)
    adaptedValue = iDeserMxArrayWithDefaultEmpty(value, deserializableDataArray, ...
        matlab.internal.diagnostic.Warning.empty(0, 1));
end

function adaptedValue = iDeserMxArrayDefaultFunction(value, deserializableDataArray)
% Note: we get here for a batch-pool job, so we must specify the pool worker function.
    adaptedValue = iDeserMxArrayWithDefaultEmpty(value, deserializableDataArray, ...
                                                 @parallel.internal.pool.poolWorkerFcn);
end

function adaptedValue = iDeserMxArrayDefautInputArgs(value, deserializableDataArray)
    adaptedValue = iDeserMxArrayWithDefaultEmpty(value, deserializableDataArray, {});
end

function adaptedValue = iDeserializePlain(value, deserializableDataArray)
    index = iAdaptStreamingData(value);
    adaptedValue = deserializableDataArray(index).deserializePlain();
end

function value = iAdaptFailedTaskAttemptInfo(value, deserializableDataArray)
    value.commandWindowOutput = iDeserializePlain(value.commandWindowOutput, deserializableDataArray);
end

function adaptedValue = iAdaptFailedTaskAttemptInfoArray(value, deserializableDataArray)
    adaptedValue = arrayfun(@(r) iAdaptFailedTaskAttemptInfo(r, deserializableDataArray), value.values);
end

function value = iAdaptJobStatusData(value, ~)
    value.jobType = parallel.internal.cluster.types.JobType.fromOrdinal(value.jobType);
    value.corruptionInfo = arrayfun(@iAdaptCorruptionInfo, value.corruptionInfo);
end

function corruptionInfo = iAdaptCorruptionInfo(corruptionInfo)
    corruptionInfo.level = parallel.internal.cluster.types.JobCorruptionLevel.fromOrdinal(corruptionInfo.level);
end

function jobIDNumAndType = iAdaptJobIDNumAndType(jobIDNumAndType)
    jobIDNumAndType.mlType = parallel.internal.cluster.types.JobMLType.fromOrdinal(jobIDNumAndType.mlType);
end

function jobIDNumAndTypeArray = iAdaptJobIDNumAndTypeArray(jobIDNumAndTypeArray, ~)
    jobIDNumAndTypeArray = arrayfun(@iAdaptJobIDNumAndType, jobIDNumAndTypeArray.jobs);
end

function value = iAdaptAuthorizationLevel(value, ~)
    value = parallel.internal.cluster.types.AuthorizationLevel.fromOrdinal(value);
end
