function outputArgs = argumentsParser(varargin)
%   For internal use only
%
%   Parse the input arguments to parpool.
%   Varargin should be one of:
%   {}, {Profile, NumWorkers}, {Profile}, {NumWorkers}
%   Potentially followed by PV pairs - 'AttachedFiles', {attachedFiles}
%   'SpmdEnabled', boolean
%   'IdleTimeout', positiveInteger
%   'RestartOnClusterChange', boolean
%   'RestartOnPreferredNumWorkersChange', boolean
%   'EnvironmentVariables', {environmentVariables}
%   Return the action args that can be passed to AbstractClusterPool.hBuildPool()

%   Copyright 2013-2020 The MathWorks, Inc.

% Allowed to specify param/value pair:
% - Full string matching
% - Case sensitive
% - Must be as param/value pair - CAN NOT be a struct or cell
allowedProps = parallel.internal.pool.clusterParpoolProperties();

% Split inputs into "initial" and "P-V" args at first allowedProp
initialArgs = {};
pvArgs = {};
% Since varargin is not a cellstr have to do this the long way.
if nargin == 0
    initialArgs = varargin;
elseif ischar(varargin{1}) && any(strcmp(varargin{1}, allowedProps))
    pvArgs = varargin;
elseif length(varargin) > 1 && ischar(varargin{2}) && any(strcmp(varargin{2}, allowedProps))
    initialArgs = varargin(1);
    pvArgs = varargin(2:end);
elseif length(varargin) > 2
    initialArgs = varargin(1:2);
    pvArgs = varargin(3:end);
else
    initialArgs = varargin;
end
% Need to ensure pvArgs has an even number of elements
if mod(numel(pvArgs),2) ~= 0
    error(message('parallel:cluster:PropertyValueInPairs'));
end

[cluster, numWorkers, didSpecifyCluster, didSpecifyNumWorkers, sessionInfo] = iGetClusterAndNumWorkers(initialArgs);

% Need to make sure "P-V" args contains allowed inputs
for n = 1:2:length(pvArgs)
    parameter = pvArgs{n};
    value = pvArgs{n+1};
    switch parameter
        case 'AttachedFiles'
            if ischar(value)
                value = {value};
                pvArgs{n+1} = value; %#ok<AGROW> This allocation will happen at most once.
            end
            parallel.internal.customattr.checkSimpleConstraint('cellstr', 'AttachedFiles', value);
        case 'AutoAddClientPath'
            if ~islogical(value)
                error(message('parallel:lang:pool:InvalidAutoAddClientPath'));
            end
        case 'SpmdEnabled'
            if ~islogical(value)
                error(message('parallel:lang:pool:InvalidSpmdEnabled'));
            end
            % Some clusters do not support disabling SPMD, so if the value
            % is false, check with the cluster whether a warning needs to
            % issued.
            if ~value
                cluster.hWarnIfCannotDisableSPMD();
            end
        case 'RestartOnClusterChange'
            if ~islogical(value)
                error(message('parallel:lang:pool:InvalidRestartOnClusterChange'));
            end
        case 'IdleTimeout'
            if ~iIsValidIdleTimeout(value)
                error(message('parallel:lang:pool:InvalidIdleTimeout'));
            end
        case 'RestartOnPreferredNumWorkersChange'
            if ~islogical(value)
                error(message('parallel:lang:pool:InvalidRestartOnPreferredNumWorkersChange'));
            end
        case 'EnvironmentVariables'
            if ischar(value)
                value = {value};
                pvArgs{n+1} = value; %#ok<AGROW> This allocation will happen at most once.
            end
            parallel.internal.customattr.checkSimpleConstraint('cellstr', 'EnvironmentVariables', value);
        otherwise
            error(message('parallel:lang:pool:InvalidInputParameter', parameter));
    end
end

outputArgs = [{'Cluster', cluster, 'NumWorkers', numWorkers, 'SessionInfo', sessionInfo}, pvArgs];
% RestartOnClusterChange and RestartOnPreferredNumWorkersChange are always
% false if the user specified either a profile or a cluster object,
% unless the user manually specified a RestartOnClusterChange or RestartOnPreferredNumWorkersChange.
% Since right-most value wins, put the value of  false on the front of the outputArgs.
if didSpecifyCluster
    outputArgs = ['RestartOnClusterChange', false,...
        'RestartOnPreferredNumWorkersChange', false, outputArgs];
end

% RestartOnPreferredNumWorkersChange is always false if the user specified
% NumWorkers, unless the user manually specified a
% RestartOnPreferredNumWorkersChange. Since right-most value wins, it is OK
% to add RestartOnPreferredNumWorkersChange to the left of the outputArgs again,
% even if it was already added in the didSpecifyCluster block earlier.
if didSpecifyNumWorkers
    outputArgs = ['RestartOnPreferredNumWorkersChange', false, outputArgs];
end

end

function [cluster, numWorkers, didSpecifyCluster, didSpecifyNumWorkers, sessionInfo] = iGetClusterAndNumWorkers(initialArgs)
% didSpecifyCluster indicates whether or not a cluster object or profile was supplied by the user.
% didSpecifyNumWorkers indicates whether or not NumWorkers was supplied by the user.

% Getting the PreferredNumWorkers from Parallel prefs
settings = parallel.Settings;
preferredNumWorkers = settings.Pool.PreferredNumWorkers;

% initialArgs has length 0, 1 or 2
didSpecifyCluster = false;
didSpecifyNumWorkers = false;
if isempty(initialArgs)
    % This is parpool()
    sessionInfo = iPoolStartingMessage(parallel.defaultClusterProfile());
    cluster = parcluster();
    numWorkers = cluster.hGetDefaultPoolNumWorkersRange(preferredNumWorkers);
elseif length(initialArgs) == 1
    if isnumeric(initialArgs{1})
        % This is parpool(4)
        requestedWorkers = initialArgs{1};
        parallel.internal.customattr.checkSimpleConstraint('workerlimits', 'NumWorkers', requestedWorkers);
        sessionInfo = iPoolStartingMessage(parallel.defaultClusterProfile());
        cluster = parcluster();
        numWorkers = iNormalizeRequestedWorkers(requestedWorkers);
        didSpecifyNumWorkers = true;
    elseif ischar(initialArgs{1})
        % This is parpool('profileName')
        sessionInfo = iPoolStartingMessage(initialArgs{1});
        cluster = parcluster(initialArgs{1});
        numWorkers = cluster.hGetDefaultPoolNumWorkersRange(preferredNumWorkers);
        didSpecifyCluster = true;
    elseif isa(initialArgs{1}, 'parallel.Cluster')
        % This is parpool(clusterObject)
        cluster = initialArgs{1};
        sessionInfo = iPoolStartingMessage(cluster.Profile, cluster.Type);
        numWorkers = cluster.hGetDefaultPoolNumWorkersRange(preferredNumWorkers);
        didSpecifyCluster = true;
    else
        error(message('parallel:lang:pool:InvalidSingleInput'));
    end
else
    if ischar(initialArgs{1})
        % This is (hopefully) parpool('profileName', 4)
        sessionInfo = iPoolStartingMessage(initialArgs{1});
        cluster = parcluster(initialArgs{1});
        didSpecifyCluster = true;
    elseif isa(initialArgs{1}, 'parallel.Cluster')
        % This is (hopefully) parpool(clusterObject, 4)
        cluster = initialArgs{1};
        sessionInfo = iPoolStartingMessage(cluster.Profile, cluster.Type);
        didSpecifyCluster = true;
    else
        error(message('parallel:lang:pool:InvalidTwoInputFirstArgument'));
    end
    
    if isnumeric(initialArgs{2})
        parallel.internal.customattr.checkSimpleConstraint('workerlimits', 'NumWorkers', initialArgs{2});
        numWorkers = iNormalizeRequestedWorkers(initialArgs{2});
        didSpecifyNumWorkers = true;
    else
        error(message('parallel:lang:pool:InvalidTwoInputSecondArgument'));
    end
end

cluster.hErrorIfNumPoolWorkersInvalid(numWorkers);
end

function sessionInfo = iPoolStartingMessage(profileName, varargin)
if nargin > 1
    clusterType = varargin{1};
else
    clusterType = '';
end

if isempty(profileName)
    parallel.internal.display.parallelPrint(1, '%s\n', ...
        getString(message('parallel:convenience:StartingParallelPoolNoProfile')));
else
    if isempty(clusterType)
        typeObj = parallel.internal.settings.ProfileExpander.getClusterType(profileName);
        clusterType = typeObj.Name;
    end
    parallel.internal.display.parallelPrint(1, '%s\n', ...
        getString(message('parallel:convenience:StartingParallelPoolWithProfile',...
        profileName)));
end

sessionInfo = parallel.internal.pool.ISessionNotifier.buildAboutToStartInfo(...
    clusterType, profileName);
end

function numWorkers = iNormalizeRequestedWorkers(requestedWorkers)
    if numel(requestedWorkers) == 1
        numWorkers = [requestedWorkers, requestedWorkers];
    else
        numWorkers = requestedWorkers;
    end
end

function tf = iIsValidIdleTimeout(x)
    % Valid values are positive scalar integers or Inf
    tf = (isnumeric(x) && isreal(x) && isscalar(x) && mod(x, 1) == 0 && x > 0) || isequal(x, Inf);
end
