function pmode(action, varargin)
%PMODE Interactive parallel command window
%   PMODE allows interactive parallel execution of MATLAB commands.  PMODE
%   achieves this by defining and submitting a parallel job, and it opens a
%   Parallel Command Window connected to the labs running the job.  The labs
%   receive commands entered in the Parallel Command Window, process them,
%   and send the command output back to the Parallel Command Window.
%
%   SYNTAX
%
%     PMODE START
%     PMODE START PROF <numlabs>
%     PMODE QUIT or PMODE EXIT or PMODE CLOSE
%     PMODE CLIENT2LAB clientVar <labs> labVar
%     PMODE LAB2CLIENT labVar <lab> clientVar
%     PMODE CLEANUP PROF
%
%   PMODE START  starts pmode using the default cluster profile. You can also
%   specify the number of labs using PMODE START <numlabs>, but note that
%   most schedulers have a maximum number of processes that they can start.
%
%   PMODE START PROF <numlabs> starts pmode using the cluster profile
%   PROF to locate the scheduler.  If the number of labs is specified, it
%   overrides the minimum and maximum number of workers specified in the
%   cluster profile.
%
%   PMODE QUIT or PMODE EXIT or PMODE CLOSE stops the parallel job, destroys
%   it, and closes the Parallel Command Window.
%
%   PMODE CLIENT2LAB clientVar <labs> labVar  copies the variable clientVar from
%   the MATLAB client to the variable labVar on labs <labs>.  If labVar is
%   omitted, the copy is named clientVar.  The destination labs can be
%   either a single lab index or a vector of lab indices.
%
%   PMODE LAB2CLIENT labVar <lab> clientVar  copies the variable labVar from
%   lab <lab> to the variable clientVar on the MATLAB client.  If clientVar is
%   omitted, the copy is named labVar.
%
%   PMODE CLEANUP PROF  destroys all parallel jobs created by pmode for the
%   current user using the scheduler specified by the cluster profile PROF.  This
%   includes jobs that are currently running.  The cluster profile is optional,
%   and the default cluster profile is used if none is provided.
%
%   PMODE can be invoked as either a command or a function.  For example, the
%   following are equivalent:
%       PMODE START PROF 4
%       PMODE('START', 'PROF', 4)
%
%   EXAMPLES
%   1. Start pmode using the default cluster profile:
%       >> pmode start
%   2. Execute the command    X = 2*labindex;    on all labs:
%       P>> X = 2*labindex;
%   3. Copy the variable X from lab 3 to the MATLAB client:
%       >> pmode lab2client X 3
%   4. Copy the variable y from the MATLAB client to labs 1-4:
%       >> pmode client2lab y 1:4
%
%   See also parcluster, parallel.Cluster/createCommunicatingJob, parallel.defaultClusterProfile.

%   Copyright 2006-2017 The MathWorks, Inc.


iVerifyJava();
if matlab.ui.internal.desktop.isMOTW()
    error(message('parallel:convenience:PmodeNotSupportedInMATLABOnline'))
end

% Convert any string inputs to character vectors
[action, varargin{:}] = convertStringsToChars(action, varargin{:});

validActions = 'start open exit quit close client2lab lab2client cleanup';
msgValidActions = getString(message('parallel:convenience:PmodeValidActions', validActions));
% The action is a required input.
if nargin == 0
    error(message('parallel:convenience:PmodeNoAction', msgValidActions));
end
% Check that action is a 1xN char array.
if ~(ischar(action) && (size(action, 2) == numel(action)))
    error(message('parallel:convenience:PmodeActionNotString', msgValidActions));
end

import parallel.internal.apishared.ProfileConfigHelper
profHelper = ProfileConfigHelper.buildDefault();

pool = gcp('nocreate');
action = lower(action);
switch action
    case {'start' 'open'}
        iVerifyOnClient(action);
        parallel.internal.pool.errorIfPoolAlreadyExists();
        [nlabs, config] = iParseStartArgs(varargin{:});
        if isempty(config)
            % Use the default config if one was not specified
            config = profHelper.getDefaultName();
        end
        
        % We have a message for each of 'profile' and 'configuration', so use the property
        % name to access the message (which requires capitalizing the property name.)
        profilePropertyName = profHelper.PropertyName;
        if strcmp( profilePropertyName, 'profile' )
            startingWithMsgId = 'parallel:convenience:StartingWithProfile';
        elseif strcmp( profilePropertyName, 'configuration' )
            startingWithMsgId = 'parallel:convenience:StartingWithConfiguration';
        else
            assert( false, ['Unknown profile property name: ' profilePropertyName] );
        end
        
        fprintf( '%s ', getString( message( startingWithMsgId, 'pmode', config ) ) );

        % get the cluster type for the current profile name
        typeObj = parallel.internal.settings.ProfileExpander.getClusterType(config);
        clusterType = typeObj.Name;
        % broadcast 'session about to start' notifications
        sessionInfo = com.mathworks.toolbox.distcomp.pmode.SessionInfo.NULL_SESSION_INFO;
        sessionInfo = com.mathworks.toolbox.distcomp.pmode.SessionFactory.notifySessionAboutToStart(sessionInfo, clusterType, config);
        
        try
            sched = profHelper.buildScheduler(config);
        catch E
            % Without this newline the error would begin on the same line as the
            % StartingWith message.
            fprintf( '\n' );
            rethrow( E );
        end
        parallel.Pool.hBuildPmode('Cluster', sched, 'NumWorkers', nlabs, 'IdleTimeout', inf, ...
            'SessionInfo', sessionInfo);
    case {'exit', 'quit', 'close'}
        % Exit and quit take no additional arguments.
        iVerifyNoArgs(action, numel(varargin));
        if iIsOnClient()
            iErrorIfPoolEmpty(pool);
            pool.delete();            
        else
            % We allow all the labs to send this command back to the client, to
            % ensure that the command is run.
            cmd = 'pmode exitnoerrors';
            iRunCmdOnClient(action, cmd);
        end
    case 'exitnoerrors'
        % Put into a try-catch block so that the numlabs - 1 times it is run
        % unnecessarily, we do not throw a visible error in the MATLAB
        % command window.
        try
            iErrorIfPoolEmpty(pool);
            pool.delete();
        catch e %#ok<NASGU>
        end
        
    case 'exitleaveguiopen' % Undocumented.
        % exitleaveguiopen takes no additional arguments.
        iVerifyOnClient(action);
        iErrorIfPoolEmpty(pool);
        pool.hExitLeaveGuiOpen(varargin{1});
        
    case 'lab2client'
        [labvarname, lab, clientvarname] = iParseTransferArgs(...
            action, pool, varargin{:});
        if ~iIsIntegerScalar(lab, 1, iGetNumLabs(action, pool))
            error(message('parallel:convenience:PmodeInvalidSourceLab', iGetNumLabs(action, pool)));
        end
        if iIsOnClient()
            iErrorIfPoolEmpty(pool);           
            pool.hGetClient().lab2client(labvarname, lab, clientvarname);
        else
            if labindex == lab
                cmd = sprintf('pmode(''%s'', ''%s'', %s, ''%s'');', ...
                    action, labvarname, num2str(lab), clientvarname);
                iRunCmdOnClient(action, cmd);
            end
        end
    case 'client2lab'
        [clientvarname, labs, labvarname] = iParseTransferArgs( ...
            action, pool, varargin{:});
        if ~iIsIntegerVector(labs, 1, iGetNumLabs(action, pool)) || isempty(labs)
            error(message('parallel:convenience:PmodeInvalidDestinationLab', iGetNumLabs(action, pool)));
        end
        if iIsOnClient()
            iErrorIfPoolEmpty(pool);              
            pool.hGetClient().client2lab(clientvarname, labs, labvarname);
        else
            if min(labs) == labindex
                cmd = sprintf('pmode(''%s'', ''%s'', [%s], ''%s'');', ...
                    action, clientvarname, num2str(labs), labvarname);
                iRunCmdOnClient(action, cmd);
            end
        end
    case 'cleanup'
        if iIsOnClient()
            iErrorIfPoolEmpty(pool);
            iCleanupAndCloseGui(pool);
        else
            % We always want this action to be performed, even if it is
            % executed on only one lab.  We must therefore allow all labs to
            % send it back to the client.
            cmd = sprintf('pmode(''%s'');', action);
            iRunCmdOnClient(action, cmd);
        end
    otherwise
        error(message('parallel:convenience:PmodeInvalidAction', action, msgValidActions));
end
end

function [nlabs, config] = iParseStartArgs(varargin)
%iParseStartArgs Parse the input arguments to pmode start.
%   Varargin should be one of:
%   {}, {config, numlabs}, {config}, {numlabs}
%   Return the number of labs and the configuration.  These are empty if
%   not specified in the input.
%   We perform no input checking on the configuration, but rely on the error
%   messages from findResource.
if numel(varargin) > 2
    error(message('parallel:convenience:PmodeInvalidStartArguments'));
end
config = '';
nlabs = [];
if isempty(varargin)
    return;
end
if numel(varargin) == 1
    % Only one input argument.  It can be either the number of labs or
    % the configuration.  If we can convert the input to an integer, it
    % must be the number of labs, otherwise it must be the
    % configuration.
    if isnumeric(varargin{1})
        nlabs = varargin{1};
    else
        % Try to convert varargin{1} to double.
        nlabs = str2double(varargin{1});
        if ~isfinite(nlabs)
            % Conversion to double failed, so varargin{1} must be a
            % configuration.
            config = varargin{1};
            nlabs = [];
        end
    end
else
    config = varargin{1};
    nlabs = varargin{2};
    if ~isnumeric(nlabs)
        nlabs = str2double(nlabs);
        % Now nlabs is either a double or a NaN.
    end
end
if ~isempty(nlabs) && ~iIsIntegerScalar(nlabs, 1, realmax)
    error(message('parallel:convenience:PmodeInvalidNumLabs'));
end
end

function [srcvarname, labs, destvarname] = iParseTransferArgs(action, pool, varargin)
%iParseTransferArgs Parse the input arguments to pmode client2lab and pmode
%lab2client.
%   srcvarname is a string describing the source variable.  E.g. clientVar.
%   destvarname is a string describing the destination variable  E.g. labVar.
%   Parses the arguments, verifies that pmode is running.  Returns the
%   names of the source and the destination variables.
%   The labs argument is returned with almost no argument checking.  We try
%   to convert it to a vector of doubles.  Caller must verify that it is a
%   vector and that it only contains ints in the range [1, numlabs].
if ~(2 <= numel(varargin) && numel(varargin) <= 3)
    error(message('parallel:convenience:PmodeInvalidNumberOfArguments', action));
end
srcvarname = varargin{1};
labs = varargin{2};
if numel(varargin) > 2
    destvarname = varargin{3};
else
    destvarname = srcvarname;
end
if ~isvarname(srcvarname)
    error(message('parallel:convenience:PmodeInvalidSourceVariable'));
end
if ~isvarname(destvarname)
    error(message('parallel:convenience:PmodeInvalidTargetVariable'));
end
if iIsOnClient()
    if isempty(pool)
        pmodeRunning = false;
    else
        client = pool.hGetClient();
        pmodeRunning = client.isPossiblyRunning();
    end
else
    [~, sessionObject] = parallel.internal.pool.PoolArrayManager.getCurrent();
    session = sessionObject.Session;
    pmodeRunning = ~isempty(sessionObject) && ~isempty(session);
end
if ~pmodeRunning
    error(message('parallel:convenience:CannotExecutePmodeNotRunning', action));
end
if ischar(labs)
    labs = str2num(labs); %#ok Allow arrays of integers.
end
end

function iVerifyNoArgs(action, nargs)
if nargs ~= 0
    error(message('parallel:convenience:PmodeNoArguments', action));
end
end

function valid = iIsIntegerScalar(value, lowerBound, upperBound)
%iIsIntegerScalar Check if input is a scalar integer within the specified
%bounds.
valid = iIsIntegerVector(value, lowerBound, upperBound) ...
    && isscalar(value);
end

function valid = iIsIntegerVector(value, lowerBound, upperBound)
%iIsIntegerScalarVector Check if input is a vector of integers within the
%specified bounds.
valid = isnumeric(value) && isreal(value) && isvector(value) ...
    && all((value >= lowerBound)) ...
    && all(value <= upperBound) && ~any(isnan(value)) ...
    && all(fix(value) == value);
end

function iVerifyOnClient(action)
%iVerifyOnClient A no-op on the MATLAB client.
%   Error if trying to execute pmode with this action on the labs.
if ~iIsOnClient()
    error(message('parallel:convenience:PmodeRunOnLabs', action));
end
end

function onclient = iIsOnClient()
onclient = ~system_dependent('isdmlworker');
end

function iVerifyJava()
%iVerifyJava Error if swing is not present.
if iIsOnClient()
    error(javachk('swing', 'pmode'));
end
end

function iRunCmdOnClient(action, cmd)
%iRunCmdOnClient Send a command back to the client for asynchronous evaluation.
[~, sessionObject] = parallel.internal.pool.PoolArrayManager.getCurrent();
if isempty(sessionObject) || isempty(sessionObject.Session)
    error(message('parallel:convenience:CannotExecutePmodeNotRunning', action));
end
% Error messages will only be displayed in the main MATLAB command window, and
% the command will only be executed in the MATLAB client when it is idle.
fprintf('Sending  pmode %s  to the MATLAB client for asynchronous evaluation.', action);
c = sessionObject.Session.getClient;
c.evalConsoleOutput(cmd);
end

function nlabs = iGetNumLabs(action, pool)
%iGetNumLabs Can be called both on the client and the labs to get numlabs.
if ~iIsOnClient()
    nlabs = numlabs;
    return;
end
try
    if isempty(pool)
        nlabs = 0;
    else
        % Cannot use the NumWorkers property of the pool here as it returns zero for a
        % pmode type pool
        nlabs = pool.hGetSession().getPoolSize();
    end
catch err
    newEx = MException(message('parallel:convenience:CannotExecutePmodeNotRunning', action));
    newEx = newEx.addCause(err);
    throw(newEx);
end
end

function iErrorIfPoolEmpty(pool)
if isempty(pool)
    error(message('parallel:convenience:InteractiveSessionNotRunning', 'pmode', 'pmode'))
end
end

function iCleanupAndCloseGui(pool)
dctPathAndClearNotificationGateway('off'); % ToDo:manyPools modify when multiple pools can exist
pool.hGetClient().cleanup('pmode', pool.Cluster);
parallel.internal.pool.PoolArrayManager.remove(pool);
end
