function varargout = pctconfig(varargin)
%PCTCONFIG Configure a Parallel Computing Toolbox session
%
%    CONFIG = PCTCONFIG() returns a struct, CONFIG, of the configuration 
%    property names and values.
%
%    CONFIG = PCTCONFIG('P1', V1, 'P2', V2,...) configures the properties 
%    of the Parallel Computing Toolbox which are passed as 
%    parameter/value pairs, P1, V1, P2, V2.  The parameter/value pairs can 
%    be specified as a cell array or a struct.  The function then returns a 
%    struct, CONFIG, of the configuration property names and values.
%  
%    If the property is 'portrange', the specified value is used to set the
%    range of ports to by used by the client session of the Parallel Computing Toolbox 
%    The value should either be a 2 element vector [minPort, maxPort]
%    or 0 to specify that the client session should use ephemeral ports.
%
%    If the property is 'hostname', the specified value is used to set the
%    hostname for the client session of the Parallel Computing Toolbox.
%
%    Note: The values set by this function do not persist between MATLAB
%    sessions.  To guarantee its effect, call pctconfig before calling any 
%    other Parallel Computing Toolbox functions.
%
%    Examples:
%    % Set client to use ports between 30000 and 30010
%    % and hostname 'host4'
%    pctconfig('portrange', [30000, 30010], 'hostname', 'host4');
%
%    % Set the client to use ephemeral ports 
%    pctconfig('portrange', 0);
 

% Copyright 2007-2019 The MathWorks, Inc.
  
    mlock;
    persistent stored_config;
    persistent stored_undoc;
    % obtain default configuration information
    if isempty(stored_config)
        stored_config = iSetDefaultValues();
        stored_undoc = iSetDefaultUndocValues();
    end
    try
        % if we don't have any input arguments, just return the current config
        if ~isempty(varargin)
            % Convert any string inputs to character vectors
            [varargin{:}] = convertStringsToChars(varargin{:});

            [params, values] = parallel.internal.convertToPVArrays(varargin{:});
            [params, values, pUndoc, vUndoc] = iGetUndocParams(params, values);
            if ~isempty(params)
                % check to make sure all arguments are valid properties
                [params, values] = iArgCheck(params, values, stored_config, stored_undoc);
                stored_config = iSetValues(stored_config, params, values);
            end
            if ~isempty(pUndoc)
                % check to make sure all arguments are valid properties
                [pUndoc, vUndoc] = iArgCheckUndoc(pUndoc, vUndoc);
                stored_undoc = iSetValues(stored_undoc, pUndoc, vUndoc);
            end
        end
    catch err
        rethrow(err);
    end
    varargout{1} = stored_config;
    if nargout > 1
        varargout{2} = stored_undoc;
    end
end


% Throws an error if any of the PV pairs don't match the ones that this function
% is expecting.
function [params, values] = iArgCheck(params, values, stored_config, stored_undoc)
    for i = 1:length(params)
        param = params{i};
        value = values{i};
        switch param
            case 'portrange'
                values{i} = iCheckValidPortRange(value);
            case 'hostname'
                values{i} = iCheckClientSessionHostname(value, stored_config, stored_undoc);
            case {'port', 'pmodeport'}
                    % These properties have been replaced by portrange.
                if iIsEphemeralPort(value)
                    portrange = value;
                else
                    portrange = [value, value+1000];
                end
                warning(message('parallel:cluster:PctConfigDeprecatedProperty', param, num2str( portrange )));
                params{i} = 'portrange';
                values{i} = iCheckValidPortRange(portrange);
                
            otherwise
                error(message('parallel:cluster:PctConfigInvalidPVPair', param));
        end
    end
end

% creates a default configuration
function config = iSetDefaultValues()
    % To preserve the behaviour prior to fix for g1532246, we set the portrange to 0
    % on workers (as that is what they previously always used)
    if feature('isdmlworker')
        portrange = 0;
    else
        portrange = [27370, 27470];
    end
    
    % See if someone has given set the hostname (in initworker for example), or from
    % an environment variable.
    hostname = iGetHostnameFromSystemPropertyOrEnvironment();
    if isempty(hostname)
        hostname = parallel.internal.general.HostNameUtils.getShortLocalHostName();
    end

    config = struct('portrange', portrange, ...
                    'hostname', hostname);
end

% Order of precedence:
% 1. If MDCE_OVERRIDE_EXTERNAL_HOSTNAME is set, return that
% 2. If HOST_NAME system property is set, return that
% 3. Return empty
% Note that initworker always tries to set HOST_NAME system property from
% HOSTNAME environment variable.
function hostname = iGetHostnameFromSystemPropertyOrEnvironment() 
hostname = getenv('MDCE_OVERRIDE_EXTERNAL_HOSTNAME');
if isempty(hostname)
    propertyName = com.mathworks.toolbox.distcomp.util.SystemPropertyNames.HOST_NAME;       
    hostname = char(java.lang.System.getProperty(propertyName));
    dctSchedulerMessage(2, 'pctconfig using SystemPropertyNames.HOST_NAME: %s', ...
                        hostname);
else
    dctSchedulerMessage(2, 'pctconfig using MDCE_OVERRIDE_EXTERNAL_HOSTNAME: %s', ...
                        hostname);
end
end

% adds values to a configuration
function config = iSetValues(config, params, values)
    for i = 1:length(params)
        param = params{i};
        value = values{i};
        config.(param) = value;
    end
end

% Creates the default undocumented values.
function undoc = iSetDefaultUndocValues()
    % used by pool creation to allow workers to connect to the proxying worker
    % more efficiently by using a purely-internal hostname.
    internalhostname = getenv('MDCE_OVERRIDE_INTERNAL_HOSTNAME');
    if ~isempty(internalhostname)
        dctSchedulerMessage(2, 'pctconfig using MDCE_OVERRIDE_INTERNAL_HOSTNAME: %s', ...
                            internalhostname);
    end

    undoc = struct('preservejobs', false, ...
                   'initclienthasrun', false, ...
                   'hostinetaddress', [], ...
                   'internalhostname', internalhostname, ...
                   'convenienceapi', 2, ...
                   'useprofiles', true, ...
                   'mjspollinterval', 60, ... % units of seconds
                   'cjspollinterval', 1, ... % units of seconds
                   'optimizedpoolbroadcast', logical.empty(), ... % empty means "use cluster default"
                   'reliableconnections', true, ...
                   'parfevalMaxWorkerQueueLength', 1, ...
                   'filestoragenumattempts', 5); % With a 1 second pause in between
end

% Separates the undocumented PV-pairs from the list of PV-pairs
function [params, values, pUndoc, vUndoc] = iGetUndocParams(params, values)
    allUndoc = {'preservejobs', ...
                'initclienthasrun', ...
                'hostinetaddress', ...
                'internalhostname', ...
                'convenienceapi', ...
                'useprofiles', ...
                'mjspollinterval', ...
                'cjspollinterval', ...
                'optimizedpoolbroadcast', ...
                'reliableconnections', ...
                'parfevalMaxWorkerQueueLength', ...
                'filestoragenumattempts'};
    ind = ismember(params, allUndoc);
    pUndoc = params(ind);
    vUndoc = values(ind);
    params(ind) = [];
    values(ind) = [];
end

% Throws an error if any of the PV pairs don't match the undocumented PV pairs
% that this function is expecting.
function [pUndoc, vUndoc] = iArgCheckUndoc(pUndoc, vUndoc)
    for i = 1:length(pUndoc)
        param = pUndoc{i};
        value = vUndoc{i};
        switch param
            case 'preservejobs'
                if ~(isscalar(value) && islogical(value))
                    error(message('parallel:cluster:LogicalScalarConstraint', 'preservejobs'));
                end
            case 'initclienthasrun'
                if ~(isscalar(value) && islogical(value))
                    error(message('parallel:cluster:LogicalScalarConstraint', 'initclienthasrun'));
                end
            case 'hostinetaddress'
                if ~( isa(value, 'java.net.InetAddress') || isempty(value) )
                    error(message('parallel:cluster:PctConfigBadHostInetAddress'));
                end
            case 'internalhostname'
              if ~(ischar(value) && (isempty(value) || isrow(value)))
                  error(message('parallel:cluster:StringConstraint', param));
              end
            case 'convenienceapi'
                if ~ ( isequal( value, 1 ) || isequal( value, 2 ) )     
                    error(message('parallel:cluster:PctConfigBadApi'));
                end
            case 'useprofiles'
                if ~(isscalar(value) && islogical(value))
                    error(message('parallel:cluster:LogicalScalarConstraint', 'useprofiles'));
                end
            case {'mjspollinterval', 'cjspollinterval'}
                iErrorIfPollIntervalInvalid(value, param);
            case 'optimizedpoolbroadcast'
                if ~(islogical(value) && numel(value) <= 1)
                    error(message('parallel:cluster:LogicalScalarOrEmptyConstraint', param));
                end
            case 'reliableconnections'
                if ~(islogical(value) && isscalar(value))
                    error(message('parallel:cluster:LogicalScalarConstraint', 'reliableconnections'))
                end
            case 'parfevalMaxWorkerQueueLength'
                if ~(isnumeric(value) && isscalar(value) && round(value) == value && value > 0)
                    error(message('parallel:cluster:PctConfigBadWorkerQueueLength'))
                end
            case 'filestoragenumattempts'
                error = parallel.internal.customattr.validateSimpleConstraint('positivenonzerofiniteintscalar', param, value);
                if ~isempty(error)
                    throw(error)
                end
            otherwise
                error(message('parallel:cluster:PctConfigInvalidPVPair', param));
        end
    end
end

function iErrorIfPollIntervalInvalid(value, param)
% Minimum period in seconds, imposed by timer object
minimumPeriod = 0.001;
% Maximum period in seconds, imposed by timer object
maximumPeriod = 212400;
if ~isscalar(value) || ~isnumeric(value) || value < minimumPeriod || value > maximumPeriod
    error(message('parallel:cluster:PctConfigBadPollInterval', param, num2str(minimumPeriod), num2str(maximumPeriod)));
end
end

function value = iCheckClientSessionHostname(value, stored_config, stored_undoc)
% Check whether the input is a valid client session hostname.  Return the value
% that we are willing to assign it to.
    % Check that hostname is a 1xN char array
    if ~(ischar(value) && (size(value, 2) == numel(value)))
        error(message('parallel:cluster:StringConstraint', 'hostname'));
    end
    if stored_undoc.initclienthasrun 
        currentHostname = stored_config.hostname;
        if ~strcmp(value, currentHostname) 
            % It's too late to change the hostname, so override what the user
            % requested.
            warning(message('parallel:cluster:PctConfigHostnameAlreadySet', value, currentHostname));
            value = currentHostname;
        end
    end
end

function value = iCheckValidPortRange(value)
    validPortRange = [1, intmax('uint16')];    
    
    if iIsValidPortRange(value, validPortRange)
        if isunix && value(1) < 1024
            warning(message('parallel:cluster:PctConfigUsingWellKnownPortNumber'));
            
        end
    elseif iIsEphemeralPort(value)
        % ok
    else
        e = MException(message('parallel:cluster:PctConfigBadPortRange', validPortRange( 1 ), validPortRange( 2 )));
        throwAsCaller(e);
    end
end

function valid = iIsValidPortRange(value, validRange)
    valid = isnumeric(value) && numel(value)==2 ...
            && iIsValidPort(value(1), validRange) ...
            && iIsValidPort(value(2), validRange) ...
            && value(2) > value(1);
end

function valid = iIsEphemeralPort(value)
    valid = isnumeric(value) && isscalar(value) && value == 0;
end

function valid = iIsValidPort(value, validRange)
    % Check that port is a 1x1 integer in the correct range.
    valid = isnumeric(value) && isscalar(value) ...
            && round(value) == value ...
            && value>=validRange(1) && value<=validRange(2);
end
