classdef ModelAnnotationData < handle & matlab.mixin.Heterogeneous
% Holds data required for annotating a model
    
%   Copyright 2010-2019 The MathWorks, Inc.
    
    
    properties(Constant=true, Access=private)
        WrapperBlockHtml = sprintf...
            ('<table>\n<tr><td align=center>\n%s</td>\n</tr>\n</table>', ...
             DAStudio.message...
             ('CoderProfile:ExecutionTimeInformer:etiHierarchyText'));
    end
    
    properties (Access=private)
        Model;
        SIDsWithOwnProfilingData;
        SectionNumbersForSID;
        ExecutionTime;
        % unit of measurement (e.g.: ticks, seconds)
        Units;
        % System International unit: this field is not empty
        % when Units is in second and the Scale Factor parameter
        % is 1e-3, 1e-6 or 1e-9.
        % Allowed values: ms, us, ns
        SIUnits; 
        DispInSeconds;
        TimeFormat;
        TimeScaleFactor;
        TaskSIDsInThisModel;
    end        
    
    methods (Access=public, Static=true)
        
        function models = getModelsToAnnotate(executionProfile)
            [~, allProfiledBlocks] = coder.profile.ModelAnnotationData...
                     .getModelDataForProfiledSections(executionProfile);
            
            % Blocks containing a profiled block and not themselves profiled
            allIntermediateBlocks = executionProfile.IntermediateSIDs;

            allBlocks = [allIntermediateBlocks(:); allProfiledBlocks(:)];                         
            
            % Extract model names from block specifications
            models = coder.profile.private.getTopModelFromBlock(allBlocks);
            models = unique(models(~strcmp(models, '')));
        end
    end
    
    methods (Access=private, Static=true)
        
        function [models, blocks, sectionNumbers, taskSIDs] = getModelDataForProfiledSections...
                (executionProfile)
            % Return model name and SIDs for each profiled section
            
            sections = executionProfile.Sections;
            blocks = {};
            sectionNumbers = [];
            lSimulationComponentSID = executionProfile.SimulationComponentSID;
            taskSIDs = {};
            
            for i=1:length(sections)
                traceInfo = sections(i).getTraceInfo;
                if(traceInfo.isTask)
                    % Always the task level section with the simulation component
                    newBlocks = {lSimulationComponentSID};
                    
                    % Identify the SID from the original model associated with this task
                    % section
                    lCallSiteSID = getCallSiteSids(traceInfo);
                    if ~isempty(lCallSiteSID)
                        assert(length(lCallSiteSID)==1, 'Expected a single call site');
                        lCallSiteSID = lCallSiteSID{1};
                        
                        if ~strcmp(lSimulationComponentSID, lCallSiteSID)
                            if ~strcmp(lCallSiteSID, strtok(lCallSiteSID, ':'))
                                % SIL/PIL block case: highlight subsystem in
                                % the original model
                                newBlocks{end+1} = lCallSiteSID; %#ok<AGROW>
                            end
                        end
                    end
                    N = length(newBlocks);
                    taskSIDs(end+1:end+N) = newBlocks;
                else
                    newBlocks = getCallSiteSids(traceInfo);
                end
                blocks = [blocks; newBlocks(:)]; %#ok
                sectionNumbers = [sectionNumbers; repmat(i, size(newBlocks(:)))]; %#ok
            end
            % Extract model names from block specifications
            models = coder.profile.private.getTopModelFromBlock(blocks);
            taskSIDs = unique(taskSIDs);
        end
    end
    
    methods (Access=private)
            
        function tableHeadingsHtml = getTableHeadingsHtml(this, columns, col_width_vec)
            
            % Data for report column headings
            headings = coder.profile.profReportColumnHeadings...
                (columns, this.Units, this.SIUnits, []);
            numColumns = length(headings);
            headingContent = cell(1, numColumns);
            for i=1:numColumns
                headingContent{i} = headings(i);
            end
            headingContent = headingContent(2:end); % Omit the name of the profiled section
                                                    % because we display it as a
                                                    % sub-heading
            
            % Generate html for the table headings
            option.HasBorder = true;
            option.HasHeaderRow = true;
            if this.ExecutionTime.IsOnTargetData
                align_vec = {'right', 'right', 'right', 'center'};
            else
                align_vec = {'right', 'right', 'right', 'right', 'right', 'center'};
            end
            table1 = rtw.report.Report.create_html_table...
                     (headingContent, option, col_width_vec, align_vec);
            tableHeadingsHtml = table1.emitHTML;
            
        end
        
        function fullSectionNameHtml = getFullSectionNameHtml(~, fullSectionName)
                
                tooltip = DAStudio.message...
                    ('CoderProfile:ExecutionTimeInformer:etiFullSectionNameTooltip');
                
                fullSectionNameHtml = sprintf...
                    (['<table title="%s" cellpadding="0" border="0">\n'...
                      '<tr><td style="padding-top:5px;"><b>%s</b></td></tr>\n'...
                      '</table>'], ...
                      tooltip, fullSectionName);
            
        end
        
        function html = getTableContentHtml(this, columns, sid, col_width_vec)

            % Identify the profiled sections associated with this SID
            sidIdx = strcmp(sid, this.SIDsWithOwnProfilingData);
            assert(sum(sidIdx)==1, 'Must be exactly one match');
            sectionsForSID = this.ExecutionTime.Sections...
                (this.SectionNumbersForSID{find(sidIdx, 1)});
            numSectionsForSID = length(sectionsForSID);
            
            html = '';
            
            % Options for generating constituent tables
            align_vec = {'right', 'right', 'right', 'right', 'right', 'center'};
            option.HasBorder = true;
            option.HasHeaderRow = false;
            
            % Required arguments for retrieving report content; footNotes is required
            % although not used for this call site
            footNotes = containers.Map('KeyType', 'char', 'ValueType', 'any');
            lReportFcn = this.ExecutionTime.ReportFcn;
            fullData = this.ExecutionTime.isTimeSeriesDataAvailable();
            actionLinks.sdi = fullData;
            actionLinks.histogram = fullData;
            actionLinks.pieChart = this.ExecutionTime.isInternalDistributionAvailable();

            % Full hierarchical name of section within the profiled call tree
            fullSectionNames = getFullSectionNames(this, sectionsForSID);
            
            for i = 1:numSectionsForSID
                section = sectionsForSID(i);
                lUseAbsolutePathForIcons = true;
                sectionCompareTo = [];
                comparisonEnabled = false;
                [columnContent, footNotes] = ...
                    coder.profile.getReportContentForSection...
                    (section, this.DispInSeconds, this.TimeFormat, this.TimeScaleFactor,...
                              lReportFcn, footNotes, this.Model, actionLinks, columns, ...
                              lUseAbsolutePathForIcons, ...
                              sectionCompareTo, comparisonEnabled);
                
                fullSectionName = fullSectionNames{i};
                fullSectionNameHtml = getFullSectionNameHtml(this, fullSectionName);
                
                for ii2=1:length(columnContent)
                    columnContent{ii2} = columnContent(ii2);
                end
                table = rtw.report.Report.create_html_table...
                    (columnContent, option, col_width_vec, align_vec);
                tableHtml = table.emitHTML;
                
                html = sprintf('%s\n%s%s', html, fullSectionNameHtml, tableHtml);
            end     
        end        
        
        function html = getSIDSectionDataHtml(this, sid)
    
            % Columns appearing in the report depend on whether the data was collected in
            % real-time or single threaded execution
            columns = getResultColumns(this.ExecutionTime, []);
            
            % Generate html for the column headings
            if this.ExecutionTime.IsOnTargetData
                col_width_vec = [100 100 100 100]/4;
            else 
                col_width_vec = [100 100 100 100 100 100]/6; 
            end
            columnHeadingsHtml = getTableHeadingsHtml(this, columns, col_width_vec);

            % Generate html for the table content
            tableContentHtml = getTableContentHtml(this, columns, sid, col_width_vec);
            
            html = sprintf('%s\n%s', columnHeadingsHtml, tableContentHtml);
            
        end
        
        function html = getBlockDescriptionHtml(~, itemHandle)
            
            sid = Simulink.ID.getSID(itemHandle);
            
            entityName = get_param(sid, 'Name');
            
            if strcmp(sid, bdroot(sid))
                entityDescription = DAStudio.message...
                    ('CoderProfile:ExecutionTimeInformer:etiModel', ...
                     coder.profile.htmlEscape(entityName));
            else
                entityDescription = DAStudio.message...
                    ('CoderProfile:ExecutionTimeInformer:etiBlock', ...
                     coder.profile.htmlEscape(entityName));
            end
            
            html = sprintf('<p><b>%s</b></p>', entityDescription);
        end
        
        function sectionName = getFullSectionName(~, section)
            sectionName = section.Name;
            parent = section.getParent;
            parentName = parent.Name;
            while(parent.Number ~= parent.getParent.Number) % Check if root node
                sectionName = [parentName ':' sectionName]; %#ok
                parent = parent.getParent;
                parentName = parent.Name;
            end
        end

        function fullSectionNames = getFullSectionNames(this, sections)
            numSections = length(sections);
            fullSectionNames = cell(numSections, 1);
            for i = 1:numSections
                section = sections(i);
                fullSectionNames{i} = coder.profile.htmlEscape(...
                    getFullSectionName(this, section));
            end
        end

        
        function intermediateBlockHandles = getIntermediateBlockHandles(this)
            
            intermediateSIDs = this.ExecutionTime.IntermediateSIDs;
            
            % The above returns intermediate SIDs for all models in the hierarchy: select
            % only the SIDs applying to the current model
            thisModelIdx = strcmp(strtok(intermediateSIDs, ':'), this.Model);
            intermediateSIDs = intermediateSIDs(thisModelIdx);
            
            intermediateBlockHandles = zeros(size(intermediateSIDs));
            keepIdx = false(size(intermediateSIDs));
            for i=1:length(intermediateSIDs)                
                sidTemp = intermediateSIDs{i};
                try
                    intermediateBlockHandles(i) = get_param(sidTemp, 'Handle');
                    keepIdx(i) = true;
                catch exc
                    if strcmpi(exc.identifier, 'Simulink:Commands:InvSimulinkObjectName')
                        % The original block no longer exists
                    else
                        rethrow(exc);
                    end
                end
            end
            intermediateBlockHandles = intermediateBlockHandles(keepIdx);
        end
        
        
        function [handles, modelHasDeletedBlocks] = getValidItemsWithProfilingData(this)
            
            modelHasDeletedBlocks = false;

            sids = this.SIDsWithOwnProfilingData;
            handles = zeros(size(sids));
            keepIdx = false(size(sids));
            for i=1:length(sids)                
                sid = sids{i};
                try
                    [h, ~, sidSpace] = Simulink.ID.getHandle(sid);
                    if isa(h,'Stateflow.Object')
                        handles(i) = sidSpace;
                    else
                        handles(i) = h;
                    end
                    keepIdx(i) = true;
                catch exc
                    if strcmpi(exc.identifier, 'Simulink:utility:objectDestroyed')
                        % The original block no longer exists
                        modelHasDeletedBlocks = true;
                    else
                        rethrow(exc);
                    end
                end
            end
            
            % Filter out deleted blocks
            handles = handles(keepIdx);
        end
    end
    

    methods (Access=public)
        
        
        function html = getHTMLForItem(this, itemHandle, reportLink)
            
            sid = Simulink.ID.getSID(itemHandle);
            
            if any(strcmp(sid,this.SIDsWithOwnProfilingData))
                % There is profiling information to display for this item
                blockDescriptionHtml = getBlockDescriptionHtml(this, itemHandle);
            
                sectionsForSIDHtml = getSIDSectionDataHtml(this, sid);
                
                html = sprintf('<p>%s</p>\n<p>%s</p>\n<p>%s</p>', ...
                               blockDescriptionHtml, sectionsForSIDHtml, reportLink);
            else
                % This is a subsystem that contains profiled blocks but it does not have its own
                % profiling information
                html = coder.profile.ModelAnnotationData.WrapperBlockHtml;
            end     

        end
        
        function hTaskBlockAncestors = getTaskBlockAncestors(this)
           
            hTaskBlockAncestors = [];
            
            lTaskSIDs = this.TaskSIDsInThisModel;
            
            if isempty(lTaskSIDs)
                return;
            end
            
            for i=1:length(lTaskSIDs)
                lTaskSID = lTaskSIDs{i};
                
                try
                    hBlock = get_param(lTaskSID, 'handle');
                catch e
                    if strcmp(e.identifier, 'Simulink:Commands:InvSimulinkObjectName')
                        % block may have been deleted (e.g. replace with
                        % SIL/PIL block workflow)
                        continue
                    else
                        rethrow(e);
                    end
                end
                
                assert(strcmp(bdroot(lTaskSID), this.Model), 'SID must be within this model');
                
                if ~strcmp(lTaskSID, this.Model)
                    parent = get_param(hBlock, 'parent');
                    while ~strcmp(parent, this.Model)
                        hParent = get_param(parent, 'handle');
                        hTaskBlockAncestors(end+1) = hParent; %#ok<AGROW>
                        parent = get_param(hParent, 'parent');
                    end
                end
            end
            
        end
        
        function [itemHandles, modelHasDeletedBlocks] = getItemsToHighlight(this)
            
            % Get valid handles for this model
            [hEntitiesWithProfiling, modelHasDeletedBlocks] = ...
                getValidItemsWithProfilingData(this);
            
            % Any model block or sub-system block containing a profiled block,
            % (but not itself profiled) must be highlighted
            intermediateBlockHandles = getIntermediateBlockHandles(this);
            
            % The task level block may be several levels down in the model hierarchy: we
            % highlight its ancestor blocks (up to but not including bdroot)
            hTaskBlockAncestors = getTaskBlockAncestors(this);            
                        
            itemHandles = [hEntitiesWithProfiling(:); 
                           intermediateBlockHandles(:);
                           hTaskBlockAncestors(:)];
            
        end
    
        function this = ModelAnnotationData(model, executionProfile, varargin)
            % Extract the data related to profiling for this model only
                
            [this.Units, this.SIUnits, this.DispInSeconds, this.TimeFormat, this.TimeScaleFactor] = ...
                coder.profile.processReportArgs...
                (executionProfile.TimerTicksPerSecond, executionProfile.TimerHWCounterUnit, varargin{:});                
                
            this.Model = model;
            this.ExecutionTime = executionProfile;
            [models, blocks, sectionNumbers, taskSIDs] = ...
                coder.profile.ModelAnnotationData...
                .getModelDataForProfiledSections(executionProfile);
            thisModelIdx = strcmp(models, model);
            
            % Find SIDs for all entities, within this model, having their own profiling data
            sids = blocks(thisModelIdx);
            sectionNumbersThisModel = sectionNumbers(thisModelIdx);
            
            [uniqueSIDs, ~, ic] = unique(sids);
            
            % Each SID may have multiple profiled sections associated with it
            lSectionNumbersForSID = cell(size(uniqueSIDs));
            
            for i=1:length(uniqueSIDs)
                lSectionNumbersForSID{i} = sectionNumbersThisModel((ic==i));
            end            
            
            this.SIDsWithOwnProfilingData = uniqueSIDs;
            this.SectionNumbersForSID = lSectionNumbersForSID;
            
            % Identify any task SIDs in this model
            tempModels = coder.profile.private.getTopModelFromBlock(taskSIDs);
            this.TaskSIDsInThisModel = taskSIDs(strcmp(tempModels, model));
        end
    end
end
