% MJSClientPropertiesAdaptor - Helper class for
% parallel.internal.cluster.MJSSupportv2 for invoking property gets and sets.
%
% Each instance of MJSClientPropertiesAdaptor encodes:
%   - the names of all scheduler properties to be gotten which compose this
%   client property
%   - function which accepts these scheduler property values and returns
%   the client property value
%   - (if settable) the names of all scheduler properties to be set when
%   setting this client property
%   - (if settable) function which accepts the client property value and
%   returns the scheduler property values
%
% Static maps (TaskMap, JobMap, ClusterMap) map property names to the
% corresponding MJSClientPropertiesAdaptor to use.

% Copyright 2020-2021 The MathWorks, Inc.

classdef (Sealed, Hidden) MJSClientPropertiesAdaptor < handle
    
    properties (SetAccess = immutable)
        % Scheduler property names to get
        % If a client property is composed from multiple scheduler property
        % values, this is a cellstr of the scheduler properties to get.
        SchedulerGetPropertyNames
        
        % Scheduler property names to set
        % If a client property sets multiple scheduler properties, this is 
        % a cellstr of the scheduler properties to set.
        SchedulerSetPropertyNames
    end
    
    properties (GetAccess = private, SetAccess = immutable)
        % Method to convert scheduler property values to client value
        % This function returns a single client value, and accepts
        % varargin, where nargin is the number of scheduler properties in
        % SchedulerGetPropertyNames.
        AdaptGetPropertyValueFcn
        
        % Method to convert client value to scheduler property values.
        % This method accepts a single client value, and returns varargout,
        % where nargout is the number of scheduler properties in
        % SchedulerSetPropertyNames.
        AdaptSetPropertyValueFcn
        
        % Default value to provide on error. This is a function handle
        % which accepts the original error. The default is just to throw
        % the original error.
        DefaultValueOnErrorFcn
    end
    
    properties (Constant, GetAccess = private)
        % Constant maps of property name to MJSClientPropertiesAdaptor instance.
        TaskMap    = iTaskMap();
        JobMap     = iJobMap();
        ClusterMap = iClusterMap();
    end
    
    methods
        function getProps = get.SchedulerSetPropertyNames(obj)
            assert(~isempty(obj.SchedulerSetPropertyNames), ...
                'Property cannot be set');
            getProps = obj.SchedulerSetPropertyNames;
        end
    end
    
    methods (Access = private)
        function obj = MJSClientPropertiesAdaptor(schedulerGetProperties, ...
                getFcn, schedulerSetProperties, setFcn, errorHandlerFcn)
            if nargin < 2
                % Property that requires no conversion on get
                getFcn = @iNoop;
            end
            if nargin < 3
                % Property that cannot be set
                schedulerSetProperties = {};
            end
            if nargin < 4
                % Property that requires no conversion on set
                setFcn = @iNoop;
            end
            if nargin < 5
                % No way to provide default property value on error;
                errorHandlerFcn = @iThrow;
            end
            
            if ~iscell(schedulerGetProperties)
                schedulerGetProperties = {schedulerGetProperties};
            end
            if ~iscell(schedulerSetProperties)
                schedulerSetProperties = {schedulerSetProperties};
            end
            
            validateattributes(schedulerGetProperties, {'cell'}, {});
            validateattributes(getFcn, {'function_handle'}, {'scalar'});
            validateattributes(schedulerSetProperties, {'cell'}, {});
            validateattributes(setFcn, {'function_handle'}, {'scalar'});
            validateattributes(errorHandlerFcn, {'function_handle'}, {'scalar'});
            
            obj.SchedulerGetPropertyNames = schedulerGetProperties;
            obj.AdaptGetPropertyValueFcn = getFcn;
            obj.SchedulerSetPropertyNames = schedulerSetProperties;
            obj.AdaptSetPropertyValueFcn = setFcn;
            obj.DefaultValueOnErrorFcn = errorHandlerFcn;
        end
    end
    
    methods
        function clientValues = adaptGetPropertyValues(obj, schedulerValues)
            % Convert scheduler property get values into the client property
            % get value
            if isempty(schedulerValues)
                % If there are no scheduler values, the adapt function must
                % still be called to supply the client values
                numObjects = size(schedulerValues, 1);
                clientValues = repmat({obj.AdaptGetPropertyValueFcn()}, 1, numObjects);
            else
                clientValues = cellfun(@(varargin) obj.AdaptGetPropertyValueFcn(varargin{:}), ...
                    schedulerValues{:}, 'UniformOutput', false);
            end
        end
        
        function varargout = adaptSetPropertyValues(obj, clientValues)
            % Convert client property set value into the scheduler
            % property set values
            numProps = nargout(obj.AdaptSetPropertyValueFcn);
            [varargout{1:numProps}] = cellfun(@(v) obj.AdaptSetPropertyValueFcn(v), ...
                clientValues, 'UniformOutput', false);
        end
        
        function defaultValues = tryHandleGetError(obj, error, numTargets)
            defaultValueOnError = obj.DefaultValueOnErrorFcn(error);
            defaultValues = repmat({defaultValueOnError}, numTargets, 1);
        end
    end
    
    methods (Static)
        function adaptors = getClusterAdaptors(clientPropertyNames)
            % Get adaptor matching cluster property names
            adaptors = iGetPropertyAdaptors(...
                parallel.internal.cluster.MJSClientPropertiesAdaptor.ClusterMap, clientPropertyNames);
        end
        
        function adaptors = getTaskAdaptors(clientPropertyNames)
             % Get adaptor matching task property names
            adaptors = iGetPropertyAdaptors(...
                parallel.internal.cluster.MJSClientPropertiesAdaptor.TaskMap, clientPropertyNames);
        end
        
        function adaptors = getJobAdaptors(clientPropertyNames)
             % Get adaptor matching job property names
            adaptors = iGetPropertyAdaptors(...
                parallel.internal.cluster.MJSClientPropertiesAdaptor.JobMap, clientPropertyNames);
        end
    end
    
    % Stand alone conversion helper methods
    methods (Static)
        function adaptedValues = adaptJobStatusData(values)
            adaptedValues = arrayfun(@iUnpackJobStatusData, values, 'UniformOutput', false);
        end
        
        function adaptedValues = adaptJobIDNumAndType(values)
            adaptedValues = iJobIDNumAndTypes(values);
        end
        
        function adaptedValues = adaptTaskIDAndNum(values)
            adaptedValues = iTaskIDAndNums(values);
        end
    end
end

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

function m = iPopulateMap(m, propName, getFcn, setFcn)
    % Helper method for populating maps when property name is common to
    % client and scheduler for both gets and sets
    ctor = @parallel.internal.cluster.MJSClientPropertiesAdaptor;
    if nargin < 3
        % Assume get value requires no conversion
        getFcn = @iNoop;
    end
    if nargin < 4
        % Assume property is not settable
        m(propName) = ctor(propName, getFcn);
    else
        m(propName) = ctor(propName, getFcn, propName, setFcn);
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Populate map for common workunit properties
function m = iWorkUnitMap(m)
    ctor = @parallel.internal.cluster.MJSClientPropertiesAdaptor;
    m = iPopulateMap(m, 'CreateDateTime', @iCreateDateTime);
    m = iPopulateMap(m, 'CreateTime');
    m = iPopulateMap(m, 'FinishDateTime', @iCreateDateTime);
    m = iPopulateMap(m, 'FinishTime');
    m = iPopulateMap(m, 'ID', @double);
    m = iPopulateMap(m, 'Name', @iNoop, @iNoop);
    m = iPopulateMap(m, 'StartDateTime', @iCreateDateTime);
    m = iPopulateMap(m, 'StartTime');
    m('StateEnum') = ctor('StateEnum', @iMapWorkUnitStateEnum, {}, @iNoop, @iGetStateEnumErrorHandler);
    m = iPopulateMap(m, 'Timeout', @iUnpackTimeout, @iPackTimeout);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Build the map for Tasks
function m = iTaskMap()
    m = containers.Map;
    ctor = @parallel.internal.cluster.MJSClientPropertiesAdaptor;
    m = iWorkUnitMap(m);
    m = iPopulateMap(m, 'CaptureDiary', @iNoop, @iNoop);
    m = iPopulateMap(m, 'DependentFiles', @iCellColumn, @iRemoveEmptyCellEntries);
    m('DiagnosticWarnings') = ctor('Warnings');
    m = iPopulateMap(m, 'Diary', @iUnpackDiary);
    m('Error') = ctor({'ErrorStruct', 'ErrorIdentifier', 'ErrorMessage'}, @iConstructError);
    m = iPopulateMap(m, 'ErrorIdentifier');
    m = iPopulateMap(m, 'ErrorMessage');
    m = iPopulateMap(m, 'ErrorStruct');
    m = iPopulateMap(m, 'FailureInfo', @iUnpackFailedTaskAttemptInfos);
    m('Function') = ctor('Function', @iNoop, ...
                         {'Function', 'FunctionName'}, @iPackFunctionAndFuncName);
    m = iPopulateMap(m, 'FunctionName', @iNoop, @iNoop);
    m = iPopulateMap(m, 'InputArguments', @iNoop, @iNoop);
    m = iPopulateMap(m, 'JobID');
    m = iPopulateMap(m, 'MaximumRetries', @double, @iNoop);
    m = iPopulateMap(m, 'NumFailures', @double);
    m = iPopulateMap(m, 'NumOutputArguments', @double, @iNoop);
    m = iPopulateMap(m, 'OutputArguments', @iNoop);
    m = iPopulateMap(m, 'ParallelPortInfo', @iMapParallelPortInfoToMATLAB, @iMapParallelPortInfoFromMATLAB);
    m = iPopulateMap(m, 'PoolConnectionInfo', @iMapPoolConnectionInfoToMATLAB, @iMapPoolConnectionInfoFromMATLAB);
    m = iPopulateMap(m, 'Warnings', @iConstructWarnings);
    m = iPopulateMap(m, 'WorkerProperties', @iWorkerProperties);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Build the map for Jobs
function m = iJobMap()
    m = containers.Map;
    ctor = @parallel.internal.cluster.MJSClientPropertiesAdaptor;
    m = iWorkUnitMap(m);
    m = iPopulateMap(m, 'AdditionalPaths', @iCellColumn, @iPackAdditionalPaths);
    m = iPopulateMap(m, 'ApiTag', @iNoop, @char);
    m = iPopulateMap(m, 'AuthorizedUsers', @iCellRow, @iNoop);
    m = iPopulateMap(m, 'AutoAttachFiles', @iNoop, @iNoop);
    m = iPopulateMap(m, 'AttachedFileData', @iFormatAttachedFileData, @iTypecastToUint8);
    m = iPopulateMap(m, 'AttachedFilePaths', @iGetAttachedFilePaths, @iSetAttachedFilePaths);
    m = iPopulateMap(m, 'AttachedFiles', @iCellColumn, @iResolveAttachedFileList);
    m = iPopulateMap(m, 'AutoAddClientPath', @iNoop, @iNoop);
    m = iPopulateMap(m, 'DependentFiles', @iCellColumn, @iRemoveEmptyCellEntries);
    m = iPopulateMap(m, 'ExecutionMode', @iNoop, @iNoop);
    m = iPopulateMap(m, 'EnvironmentVariables', @iCellRow, @iMakeUniqueStable);
    m = iPopulateMap(m, 'JobData', @iNoop, @iNoop);
    m = iPopulateMap(m, 'JobStatusData', @iUnpackJobStatusData);
    m = iPopulateMap(m, 'LeadingTaskNum', @double);
    m = iPopulateMap(m, 'MATLABDrivePathMap', @iGetMATLABDrivePathMap, @iSetMATLABDrivePathMap);
    m = iPopulateMap(m, 'MATLABRelease');
    m = iPopulateMap(m, 'MaxNumWorkers', @iMapWorkersToMATLAB, @iMapWorkersFromMATLAB);
    m = iPopulateMap(m, 'MinNumWorkers', @iMapWorkersToMATLAB, @iMapWorkersFromMATLAB);
    m = iPopulateMap(m, 'NumLabs', @double);
    m = iPopulateMap(m, 'NumPathsToAppend', @iNoop, @iNoop);
    m = iPopulateMap(m, 'NumThreads', @double, @iNoop);
    m = iPopulateMap(m, 'ParallelTag', @iNoop, @iNoop);
    m('ProductKeys') = ctor('ProductList', @iFormatProductKeys, ...
                            'ProductList', @iNoop);
    m = iPopulateMap(m, 'RestartWorker', @iNoop, @iNoop);
    m = iPopulateMap(m, 'SubmitDateTime', @iCreateDateTime);
    m = iPopulateMap(m, 'SubmitTime', @iNoop);
    m = iPopulateMap(m, 'Tag', @iNoop, @iNoop);
    m = iPopulateMap(m, 'Tasks', @iTaskIDAndNums);
    m = iPopulateMap(m, 'Username', @iNoop, @iNoop);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Build the map for Cluster
function m = iClusterMap()
    m = containers.Map;
    ctor = @parallel.internal.cluster.MJSClientPropertiesAdaptor;
    m = iPopulateMap(m, 'AllHostAddresses', @iAllHostAddresses);
    m = iPopulateMap(m, 'BusyWorkers', @iWorkerProperties);
    m = iPopulateMap(m, 'ClusterLogLevel', @double, @iNoop);
    m = iPopulateMap(m, 'ClusterLogSpec', @iNoop, @iNoop);
    m = iPopulateMap(m, 'ClusterMatlabRoot');
    m = iPopulateMap(m, 'ClusterOperatingSystem', @iClusterOperatingSystem);
    m = iPopulateMap(m, 'HasSecureCommunication');
    m('Host') = ctor({'HostName', 'LookupURL'}, @iGetLookupURLOrHost);
    m = iPopulateMap(m, 'HostName', @iNoop);
    m = iPopulateMap(m, 'IdleWorkers', @iWorkerProperties);
    m = iPopulateMap(m, 'IsRunAsUser');
    m = iPopulateMap(m, 'Jobs', @iJobIDNumAndTypes);
    m = iPopulateMap(m, 'LookupURL');
    m = iPopulateMap(m, 'MaxCapacity', @iMakeColumn, @iNoop);
    m = iPopulateMap(m, 'MpiLibraryName', @iUnpackMpiLibraryName);
    m = iPopulateMap(m, 'Name');
    m('NumBusyMachines') = ctor('BusyWorkers', @iNumBusyMachines);
    m = iPopulateMap(m, 'NumBusyWorkers', @double);
    m('NumIdleMachines') = ctor('IdleWorkers', @iNumBusyMachines);
    m = iPopulateMap(m, 'NumIdleWorkers', @double);
    m('NumMachines') = ctor({'HostName', 'WorkerPool'}, @iNumMachines);
    m('NumWorkers') = ctor({'NumBusyWorkers', 'NumIdleWorkers'}, @iNumWorkers);
    m = iPopulateMap(m, 'OperatingSystem');
    m = iPopulateMap(m, 'RequiresOnlineLicensing');
    m = iPopulateMap(m, 'SecurityLevel', @double);
    m('StateEnum') = ctor('StateEnum', @iMapClusterStateEnum, {}, @iNoop, @iGetStateEnumErrorHandler);
    m = iPopulateMap(m, 'SupportedReleases', @iCellColumn);
    m = iPopulateMap(m, 'WorkerProxiesPoolConnections');
    m('Workers') = ctor('WorkerPool', @iWorkers);
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Noop function if no conversion is required
function adaptedValue = iNoop(value)
    adaptedValue = value;
end

function adaptedValue = iMakeUniqueStable(values)
    adaptedValue = unique(values, 'stable');
end

function adaptedValue = iPackAdditionalPaths(cellValues)
    cellValues = unique(cellValues, 'stable');
    if ispc
        adaptedValue = cellfun(@dctReplaceDriveWithUNCPath, cellValues, ...
                     'UniformOutput', false);
    else
        adaptedValue = cellValues;
    end
    adaptedValue = iRemoveEmptyCellEntries(adaptedValue);
end

function adaptedValue = iTypecastToUint8(value)
    adaptedValue = typecast(value, 'uint8');
end

function adaptedValue = iTypecastToInt8(value)
    adaptedValue = typecast(value, 'int8');
end

function adaptedValue = iFormatAttachedFileData(value)
    adaptedValue = iTypecastToInt8(value);
    if isempty(adaptedValue)
        adaptedValue = int8([]);
    end
end

function adaptedValue = iFormatProductKeys(value)
    adaptedValue = iTypecastToUint8(value);
    adaptedValue = iMakeRow(adaptedValue); 
end

function adaptedValue = iResolveAttachedFileList(value)
    % Resolve the file paths and then check that they all exist.
    value = unique(value, 'stable');
    adaptedValue = cellfun(...
        @parallel.internal.apishared.AttachedFiles.resolvePath, value, ...
            'UniformOutput', false);
    adaptedValue = iRemoveEmptyCellEntries(adaptedValue);
end

function adaptedValue = iNumBusyMachines(busyWorkerProperties)
    busyWorkerHostnames = arrayfun(@(wp) wp.hostName, busyWorkerProperties, ...
        'UniformOutput', false);
    adaptedValue = numel(unique(busyWorkerHostnames));
end

function adaptedValue = iNumMachines(headNodeHostName, workerPool)
    busyWorkerHostnames = arrayfun(@(wp) wp.hostName, workerPool.busyWorkers, ...
        'UniformOutput', false);
    idleWorkerHostnames = arrayfun(@(wp) wp.hostName, workerPool.idleWorkers, ...
        'UniformOutput', false);
    % Collection of unique hostnames from union of busy worker, idle
    % worker, and headnode hostnames.
    adaptedValue = numel(unique([{headNodeHostName} busyWorkerHostnames idleWorkerHostnames]));
end

function adaptedValue = iNumWorkers(numBusyWorkers, numIdleWorkers)
    adaptedValue = double(numBusyWorkers + numIdleWorkers);
end

function adaptedValue = iMakeColumn(value)
    adaptedValue = reshape(value, numel(value), 1);
end

function adaptedValue = iCellColumn(value)
    if isempty(value)
        adaptedValue = cell(0, 1);
    else
        adaptedValue = cell(value);
        adaptedValue = iMakeColumn(adaptedValue);
    end
end

function adaptedValue = iMakeRow(value)
    adaptedValue = reshape(value , 1, numel(value));
end

function adaptedValue = iCellRow(value)
    if isempty(value)
        adaptedValue = cell(1, 0);
    else
        adaptedValue = cell(value);
        adaptedValue = iMakeRow(adaptedValue);
    end
end

function adaptedValue = iCreateDateTime(long)
    if long < 0
        adaptedValue = datetime.empty();
    else
        adaptedValue = datetime(double(long) / 1000, 'ConvertFrom', 'posixtime', ...
            'TimeZone', 'local');
    end
end

function adaptedValue = iUnpackDiary(diaryBytes)
    if ~isempty(diaryBytes)
        adaptedValue = parallel.internal.cluster.getDiaryFromBytes(diaryBytes);
    else
        adaptedValue = '';
    end
end

function adaptedValue = iConstructError(errorStruct, errID, errMessage)
% iConstructError - return the Error as a ParallelException. Need to check ErrorMessage and
% ErrorIdentifier too as per @distcomp/@task/pGetError   
    % If the error itself is empty, but either of the ErrorMessage
    % or ErrorIdentifier are non-empty, then build the appropriate
    % ParallelException from these
    if isempty(errorStruct) && ~(isempty(errID) && isempty(errMessage))
        adaptedValue = ParallelException.hBuildLikeMException(errID, '%s', errMessage);
    else
        adaptedValue = errorStruct;
    end
end

function adaptedValue = iRemoveEmptyCellEntries(cellValue)
    adaptedValue = cellValue(~cellfun(@isempty, cellValue));
end

function adaptedValue = iUnpackFailedTaskAttemptInfo(value)
    infoStruct = struct('StartDateTime', iCreateDateTime(value.startTime), ...
                        'StartTime', value.startTimeString, ...
                        'ErrorIdentifier', value.errorIdentifier, ...
                        'ErrorMessage', value.errorMessage, ...
                        'Diary', iUnpackDiary(value.commandWindowOutput), ...
                        'Worker', value.workerProperties.name, ...
                        'WorkerHost', value.workerProperties.hostName);
    adaptedValue = parallel.task.MJSFailureInfo.hBuild(infoStruct);
end

function value = iUnpackFailedTaskAttemptInfos(value)
    if ~isempty(value)
        value = arrayfun(@iUnpackFailedTaskAttemptInfo, value);
    else
        value = [];
    end
end

function [func, funcName] = iPackFunctionAndFuncName(func)
    funcName = char(func);
end

function adaptedValues = iGetLookupURLOrHost(hostname , lookupURL)
    % Return the lookup url with base port, unless the base port is the
    % default value.
    adaptedValues = lookupURL;
    % Include the separator so we don't match a host that happens to have the default
    % base port in its name.
    defaultBasePort = [':' num2str(parallel.internal.cluster.PortConfig.getDefaultBasePort())];
    len = length(defaultBasePort);
    if isempty(lookupURL) || length(lookupURL) > len && strcmp(lookupURL(end-(len-1):end), defaultBasePort)
        adaptedValues = hostname;
    end
end

function adaptedValue = iAllHostAddresses(addresses)
    import matlab.net.internal.InetAddress

    validAddresses = cell(1, numel(addresses));
    for ii = 1:numel(addresses)
        try
            rAddress = InetAddress.getByName(addresses{ii});
            if ~rAddress.IsLoopback
                validAddresses{ii} = char(rAddress.getHostName());
            end
        catch err
            isUnknownHostException = strcmp(err.identifier, 'MATLAB:network:inetaddress:CannotResolveHost');
            if isUnknownHostException
                % We have failed to resolve the host; ignore this address
                dctSchedulerMessage(1, 'Ignoring unknown host address ''%s''. Original error:\n%s', ...
                    addresses{ii}, err.getReport());
            else
                rethrow(err);
            end
        end
    end
    validAddresses(cellfun(@isempty, validAddresses)) = [];
    adaptedValue = validAddresses;
end

function adaptedValue = iClusterOperatingSystem(operatingSystem)
    import parallel.internal.apishared.OsType
    
    if strcmp(operatingSystem, 'MIXED')
        adaptedValue = OsType.MIXED.Api2Name;
    elseif strncmp(operatingSystem, 'PC', 2) % This is same test 'ispc' uses
        adaptedValue = OsType.PC.Api2Name;
    else
        adaptedValue = OsType.UNIX.Api2Name;
    end
end

function value = iWorkerProperty(value)
    value.allHostAddresses = iCellColumn(value.allHostAddresses);
end

function value = iWorkerProperties(value)
    if ~isempty(value)
        % Since the scheduler can't return a null value, treat a single
        % worker with an empty name as an empty worker
        if isscalar(value) && isempty(value.name)
            value = struct.empty(1, 0);
        else
            value = arrayfun(@iWorkerProperty, value);
        end
    end
end

function value = iWorkers(value)
    busyWorkers = arrayfun(@iWorkerProperties, value.busyWorkers);
    idleWorkers = arrayfun(@iWorkerProperties, value.idleWorkers);
    value = [busyWorkers, idleWorkers];
end

function value = iJobIDNumAndType(value)
    value.num = double(value.num);
end

function value = iJobIDNumAndTypes(value)
    if ~isempty(value)
        value = arrayfun(@iJobIDNumAndType, value);
    end
end

function value = iUnpackJobStatusData(value)
    % Convert to MATLAB class
    value = parallel.internal.cluster.JobStatusData(...
        double(value.id), ...
        value.jobType, ...
        value.username, ...
        iCreateDateTime(value.createTime), ...
        iCreateDateTime(value.submitTime), ...
        iCreateDateTime(value.finishTime), ...
        value.state, ...
        double(value.totalPendingTasks), ...
        double(value.totalRunningTasks), ...
        double(value.totalFinishedTasks), ...
        double(value.totalTasks), ...
        value.tag, ...
        value.apiTag, ...
        double(value.totalSuccessfulFinishedTasks), ...
        iMakeColumn(double(value.taskIDsWithErrors)), ...
        iMakeColumn(double(value.taskIDsWithWarnings)), ...
        value.firstTaskMLFunctionName, ...
        arrayfun(@iUnpackJobCorruptionInfo, value.corruptionInfo));
end

function value = iUnpackJobCorruptionInfo(value)
    value = parallel.internal.cluster.JobCorruptionInfo(value.level, value.message);
end

function value = iGetAttachedFilePaths(value)
    if isempty(value)
        value = parallel.internal.apishared.AttachedFileMap();
    end
end

function value = iSetAttachedFilePaths(value)
    % To save space, represent an empty AttachedFileMap with []
    if value.Length == 0
        value = [];
    end
end

function value = iGetMATLABDrivePathMap(value)
    if isempty(value)
        value = parallel.internal.apishared.MATLABDrivePathMap({});
    end
end

function value = iSetMATLABDrivePathMap(value)
    % To save space, represent an empty MATLABDrivePathMap with []
    if isempty(value.PathMap)
        value = [];
    end
end

function adaptedValue = iMapWorkersToMATLAB(value)
    if isequal(value, intmax('int32'))
        adaptedValue = Inf;
    else
        adaptedValue = double(value);
    end
end

function adaptedValue = iMapWorkersFromMATLAB(value)
    % NB int32 cast correctly handles Inf, so all we need is simply:
    adaptedValue = int32(value);
end

function adaptedValue = iUnpackMpiLibraryName(value)
    % If the MpiOption is MSMPI, that is an indication that _if_ we are on
    % windows we should use MSMPI, otherwise we need to use the default for MJS 
    % which is defined by MpiOption to be MPICH2_SOCK. See g1375404.
    adaptedValue = value;
    if ~ispc()
        if strcmp(value, 'msmpi')
            adaptedValue = 'default';
        end
    end
end

function adaptedValue = iMapWorkUnitStateEnum(stateInt)
    import parallel.internal.types.States;
    switch double(stateInt)
      case 0
        adaptedValue = States.Pending;
      case 1
        adaptedValue = States.Queued;
      case 2
        adaptedValue = States.Running;
      case 3
        adaptedValue = States.Finished;
      otherwise
        assert(false);
    end
end

function adaptedValue = iMapClusterStateEnum(stateInt)
    import parallel.internal.types.States;
    switch double(stateInt)
      case 0
        adaptedValue = States.Running;
      case 1
        adaptedValue = States.Paused;
      case 2
        adaptedValue = States.Unavailable;
      otherwise
        adaptedValue = States.Unavailable;
    end
end

function adaptedValue = iUnpackTimeout(milliseconds)
    if isequal(milliseconds, intmax('int64'))
        adaptedValue = Inf;
    else
        adaptedValue = double(milliseconds) / 1000; % convert to seconds
    end
end

function adaptedValue = iPackTimeout(seconds)
    val = seconds * 1000; % convert to milliseconds

    % Get INTMAX for int64 to check for Inf
    INTMAX_I64 = intmax('int64');

    if ~isfinite(val) || val > INTMAX_I64
        adaptedValue = INTMAX_I64;
    else
        adaptedValue = int64(val);
    end
end

function adaptedValue = iMapParallelPortInfoToMATLAB(value)
    import parallel.internal.cluster.ParallelPortInfo
    
    % Since the scheduler can't return a null value, treat a single
    % parallel port info with an empty value as null
    if isscalar(value) && isempty(value.value)
        adaptedValue = [];
    else
        adaptedValue = ParallelPortInfo(value.attempt, value.value);
    end
end

function adaptedValue = iMapParallelPortInfoFromMATLAB(info)
    adaptedValue = struct('attempt', info.Attempt, 'value', info.Value);
end

function adaptedValue = iMapPoolConnectionInfoToMATLAB(value)
    import parallel.internal.cluster.PoolConnectionInfo
    
    % Since the scheduler can't return a null value, treat a single pool
    % connection info with an empty host as null
    if isscalar(value) && isempty(value.host)
        adaptedValue = [];
    else
        adaptedValue = PoolConnectionInfo(value.attempt, value.host, value.port);
    end
end

function adaptedValue = iMapPoolConnectionInfoFromMATLAB(info)
    adaptedValue = struct('attempt', info.Attempt, 'host', info.Host, 'port', info.Port);
end

function adaptedValue = iConstructWarnings(diagnosticWarnings)    
    adaptedValue = parallel.internal.general.WarningUtils.processDiagnosticWarnings(diagnosticWarnings);
end

function value = iTaskIDAndNum(value)
    value.num = double(value.num);
end

function value = iTaskIDAndNums(value)
    if ~isempty(value)
        value = arrayfun(@iTaskIDAndNum, value);
    end
    value = iMakeColumn(value);
end

function value = iThrow(error) %#ok<STOUT>
    rethrow(error);
end

function value = iGetStateEnumErrorHandler(error)
    if strcmp(error.identifier, 'parallel:cluster:MJSUnreachable')
        value = parallel.internal.types.States.Unavailable;
    else
        rethrow(error);
    end
end