% JobDisplayer - Displayer to handle subclasses of parallel.Job

% Copyright 2011-2020 The MathWorks, Inc.

classdef (Hidden) JobDisplayer < parallel.internal.display.Displayer

    properties (Constant, GetAccess = private)
        BaseDisplayClass = 'parallel.Job';
        % 'Tasks' and 'StateEnum' do not appear here as we need to use an efficient method to get them.
        VectorDisplayProps = {'ID', 'Type', 'FinishDateTime', 'Username'};
        VectorTableColumns = { ...
            % 'Title',        'Resizeable', 'MinimumWidth'
            'ID',             true,         length('ID'); ...
            'Type',           false,        length('Communicating'); ...
            'State',          false,        length('unavailable'); ...
            % Users can change the default display of datetimes at any time.
            % 26 characters is the longest possible display in the MATLAB preferences
            % (corresponding to millisecond display in Japan), but if users
            % override this to a longer format then use that length.
            'FinishDateTime', true,         max(length(char(datetime('now', 'TimeZone', 'local'))), 26); ...
            'Username',       true,         length('Username'); ...
            % assuming users won't have more than 100000 tasks in their jobs....!
            'Tasks',          false,        length('Tasks'); ...
        }
    end
    
    properties (SetAccess = immutable, GetAccess = protected) % implementation for abstract Displayer properties
        DisplayHelper 
    end

    methods
        function obj = JobDisplayer(someObj)
            obj@parallel.internal.display.Displayer(someObj, parallel.internal.cluster.JobDisplayer.BaseDisplayClass); 
            obj.DisplayHelper = parallel.internal.display.DisplayHelper( length('Task ID of Warnings') );
        end
    end

    methods (Access = private)
        function doInvalidSingleDisplay( obj, invalidToDisp )
            obj.DisplayHelper.displayMainHeading(getString( message('parallel:cluster:InvalidJobID', invalidToDisp.ID)));
            
            obj.DisplayHelper.displayProperty('Type', invalidToDisp.Type);
            obj.DisplayHelper.displayProperty('State', invalidToDisp.State);
        end
    end
    
    methods (Access = protected)
        
        function doSingleDisplay(obj, toDisp)
            import parallel.internal.display.Displayer
            import parallel.internal.cluster.JobDisplayer
            import parallel.internal.types.States
            % TODO:later consider adding whether or not job is a batch job or not

            if ~States.isValidState( toDisp.State )
                % Do not attempt to access any further info.
                obj.doInvalidSingleDisplay( toDisp );
                return
            end
            % For the moment, for jobs, we only want to display help for the common base class 
            obj.DisplayHelper.displayMainHeading( obj.formatDocLink(parallel.internal.cluster.JobDisplayer.BaseDisplayClass) );
            obj.DisplayHelper.displaySubHeading(getString(message('parallel:cluster:Properties')));
            obj.displaySpecificItems(toDisp);
            
            % Retrieve the tasks
            jobStatusData = toDisp.hGetJobStatusData();
            jobStatusData = jobStatusData{1};
            if isempty(jobStatusData)
                % hGetJobStatusData is robust to jobs being removed and 
                % returns empty rather than errors. 
                dctSchedulerMessage( 1, 'Failed to retrieve task properties because job has been deleted.' );
                return
            end
            
            obj.DisplayHelper.displaySubHeading('%s', char(obj.hGetTasksLink(toDisp, getString(message('parallel:cluster:AssociatedTasks')))));
            obj.DisplayHelper.displayProperty('Number Pending',    jobStatusData.getTotalPendingTasks());
            obj.DisplayHelper.displayProperty('Number Running',    jobStatusData.getTotalRunningTasks());
            obj.DisplayHelper.displayProperty('Number Finished',   jobStatusData.getTotalFinishedTasks());
            jobMemento = parallel.internal.display.JobMemento(toDisp);
            obj.DisplayHelper.displayHTMLArray('Task ID of Errors', obj.hGetLinkArrayFromTasks(double(jobStatusData.getTaskIdsWithErrors()), jobMemento));
            obj.DisplayHelper.displayHTMLArray('Task ID of Warnings', obj.hGetLinkArrayFromTasks(double(jobStatusData.getTaskIdsWithWarnings()), jobMemento));
            % SchedulerID only relevant for CJS jobs that aren't on local
            if isa(toDisp, 'parallel.internal.cluster.CJSJobMixin') && ~isa(toDisp.Parent, 'parallel.cluster.Local')
                diFactory = parallel.internal.display.DisplayableItemFactory(obj.DisplayHelper);
                TaskSchedulerIDs = toDisp.getTaskSchedulerIDs();
                displayValue = diFactory.createSchedulerIDsItem(TaskSchedulerIDs, toDisp);
                displayValue.displayInMATLAB('Task Scheduler IDs');
            end
        end
        
        function doVectorDisplay(obj, toDisp)
            import parallel.internal.cluster.JobDisplayer
            tableData = cell(numel(toDisp), size(JobDisplayer.VectorTableColumns, 1));

            % We need to filter invalid objects from the vector of objects
            % to display, because getDisplayPropsAsStruct errors when an
            % object is invalid.
            validToDisp = toDisp(isvalid(toDisp));
            
            try
                % We may have a heterogeneous array of jobs, so call a helper to split the array into
                % homogeneous chunks and get the properties for each chunk separately.
                dispPropsFcn = @(x) obj.getDisplayPropsAsStruct( x, JobDisplayer.VectorDisplayProps );
                propsToDisp = parallel.internal.cluster.hetfun( dispPropsFcn, validToDisp, 'stable' );
                propsToDisp = [propsToDisp{:}];

                statesFcn = @(x) hGetStateEnumForDisplay(x);
                states = parallel.internal.cluster.hetfun( statesFcn, validToDisp, 'stable' );
                states = [states{:}];

                try
                    taskCounts = parallel.internal.cluster.hetfun( @hGetTaskCount, validToDisp, 'stable' );
                catch E
                    % Rethrow the error if it is anything other than a job
                    % being deleted
                    if ~isequal( E.identifier, 'parallel:cluster:storage:WorkUnitNotFound' )
                        rethrow( E ); % Logged by outer try/catch
                    end
                    
                    % Otherwise iterate over the jobs to determine which
                    % one(s) could not be found
                    dctSchedulerMessage( 1, 'Failed to retrieve task counts. Original error:\n%s', E.getReport() );
                    taskCounts = cell( numel( validToDisp ), 1 );
                    for ii = 1:numel( validToDisp )
                        taskCounts{ii} = iGetTaskCountNoError( validToDisp(ii) );
                        if taskCounts{ii} < 0
                            states(ii) = {parallel.internal.types.States.Destroyed};
                            taskCounts{ii} = 0;
                        end
                    end
                end
            catch E
                dctSchedulerMessage( 1, 'Failed to retrieve job properties using hetfun. Original error:\n%s', ...
                    E.getReport() );
                [propsToDisp, states, taskCounts] = iReturnUnavailable( validToDisp );
            end
            
            % We also need a separate counter for valid objects, as we only
            % have information for those.
            validIdx = 1;
            prevParent = parallel.Cluster.empty;
            clusterMemento = parallel.internal.display.MinimalClusterMemento.empty;
            for ii = 1:numel( toDisp )
                thisJob = toDisp( ii );
                if isvalid( thisJob )
                    % Subsequent jobs will have the same parent more often
                    % than not. For performance we will create the cluster
                    % memento here and reuse it if possible.
                    if isempty( prevParent ) || thisJob.Parent ~= prevParent
                        clusterMemento = parallel.internal.display.MinimalClusterMemento( thisJob.Parent );
                        prevParent = thisJob.Parent;
                    end
                    tableData(ii, :) = obj.createVectorTableRow( ...
                        thisJob, taskCounts{validIdx}, states{validIdx}.Name, propsToDisp( validIdx ), clusterMemento );
                    validIdx = validIdx + 1;
                else
                    tableData(ii, :) = {'', '', parallel.internal.display.AbstractDisplayer.DeletedString, '', '', ''};
                end
            end

            % In the vector display we only want to display help for the common base class
            obj.DisplayHelper.displayDimensionHeading(size(toDisp), obj.formatDocLink(JobDisplayer.BaseDisplayClass));
            obj.DisplayHelper.displayTable(JobDisplayer.VectorTableColumns, tableData, obj.DisplayHelper.DisplayIndexColumn);
        end
        
    end

    methods ( Access = private )
        function dataRow = createVectorTableRow( obj, toDisp, taskCount, state, props, clusterMemento )
            import parallel.internal.types.States
            if States.isValidState( state )
                if taskCount > 0
                    tasksLink = obj.hGetTasksLink( toDisp, taskCount, clusterMemento );
                else
                    tasksLink = 0;
                end
                dataRow = { ...
                    obj.hGetLink( toDisp, props.ID, clusterMemento ), ...
                    props.Type, ...
                    state, ...
                    obj.DisplayHelper.formatDateTime( props.FinishDateTime ), ...
                    props.Username, ...
                    tasksLink ...
                    };
            else
                dataRow = { toDisp.ID, toDisp.Type, state, ...
                    '', '', '' };
            end
        end
    end
    
    methods ( Hidden, Sealed )
        
        function tasksLink = hGetTasksLink( obj, toDisp, displayValue, clusterMemento )
            if nargin < 4
                serializedMemento = serialize( parallel.internal.display.JobMemento( toDisp ) );
            else
                serializedMemento = serialize( parallel.internal.display.JobMemento( toDisp, clusterMemento ) );
            end
            
            if obj.ShowLinks
                matlabFunction = sprintf('parallel.internal.cluster.JobDisplayer.displayTasks( ''%s'')', ...
                    serializedMemento );
                tasksLink = parallel.internal.display.HTMLDisplayType( displayValue, matlabFunction );
            else
                 % Getting a link will always return an HTMLDisplayType,
                 % but if we are not building a hyperlink, we do not
                 % include the matlab command. 
                tasksLink = parallel.internal.display.HTMLDisplayType( displayValue );
            end
        end
        
        function taskLinks = hGetLinkArrayFromTasks(obj, taskIDs, jobMemento)
            if ~isempty(taskIDs)
                taskMementos = arrayfun( @(id) parallel.internal.display.TaskMemento(id, jobMemento), taskIDs );
                if obj.ShowLinks
                    taskLinksCell = arrayfun( @(id, mem) parallel.Task.hCreateLink(id, mem), taskIDs, taskMementos, 'UniformOutput', false );
                else
                    % HTMLDisplayType with only one argument displays the object without a hyperlink, which is what
                    % we want when obj.ShowLinks is false.
                    taskLinksCell = arrayfun( @(id) parallel.internal.display.HTMLDisplayType(id), taskIDs, 'UniformOutput', false );
                end
                % arrayfun cannot output an array of HTMLDisplayType, so we need to convert to a cell array and back
                taskLinks = [taskLinksCell{:}];
            else
                % Getting a link will always return an HTMLDisplayType
                taskLinks = parallel.internal.display.HTMLDisplayType.empty;
            end
        end

    end
    
    methods ( Hidden, Static )
        % Display tasks when the hyperlink is clicked in the job display
        function displayTasks(serializedMemento)
            try
                job = createObjectFromMemento(parallel.internal.display.Memento.deserialize(serializedMemento));
                disp(job.Tasks)
            catch err
                dctSchedulerMessage( 1, ...
                    'Failed to create Job from memento, original error: %s', err.getReport() );
                if feature('hotlinks')
                    helpString = '<a href="matlab: helpPopup parallel.Job">help parallel.Job</a>';
                else
                    helpString =  'help parallel.Job';
                end
                error(message('parallel:cluster:LinkDispFailure', getString(message('parallel:cluster:AssociatedTasks')), getString(message('parallel:cluster:Job')), helpString));
            end
        end
    end
end

function [propsToDisp, states, taskCounts] = iReturnUnavailable( jobs )
    propsToDisp = cell( 1, numel( jobs ) );
    taskCounts = cell( 1, numel( jobs ) );
    states = repmat( {parallel.internal.types.States.Unavailable}, 1, numel( jobs ) );
end

% Returns -1 if the job has been destroyed
function taskCount = iGetTaskCountNoError( job )
    try
        taskCount = job.hGetTaskCount();
    catch E
        if isequal( E.identifier, 'parallel:cluster:storage:WorkUnitNotFound' )
            taskCount = -1;
        else
            rethrow( E );
        end
    end
end
