classdef ProfilerReportBuilder < handle
    %PROFILERREPORTBUILDER creates a profiling report

%   Copyright 2021 The MathWorks, Inc.
    
    properties(Access=private)
        Title;
        WindowTitle;
        Definitions;
        IntroMsgID;
        DocText = [];
        DocTag = [];
        PostIntroMsgID;
        BuildDir;
        ReportFcn;
        SectionItems;
        CustomDocLinks;
        Icons;
    end
    
    methods
        function this = ProfilerReportBuilder(buildDir, reportFcn)
            this.Definitions = [];
            this.BuildDir = buildDir;
            this.ReportFcn = reportFcn;
            this.SectionItems = coder.profile.ProfileReportItem.empty;
            this.Icons = cell(1,0);
        end
        
        function addDefinitionMap(this, definitionList)
            this.Definitions = definitionList;
        end
        
        function setWindowTitle(this, title)
            this.WindowTitle = title;
        end
        
        function setTitle(this, title)
            this.Title = title;
        end
        
        function setIntroduction(this, introMsgID, postIntroMsgID)
            this.IntroMsgID = introMsgID;
            this.PostIntroMsgID = postIntroMsgID;
        end

        function setDocLink(this, docText, docTag)
            this.DocText = docText;
            this.DocTag = docTag;
        end
        
        function setDocumentationLinks(this, docLink)
            this.CustomDocLinks = docLink;
        end
        
        function addIcon(this, iconPath)
            this.Icons{end+1} = iconPath;
        end
        
        function addSection(this, name, html)
            this.SectionItems{end+1} = coder.profile.ProfileReportSection(name, html, false, true, '');
        end
        
        function addCustomSection(this, name, html, ShrinkButton, SecNumber, WidthDec)
            this.SectionItems{end+1} = coder.profile.ProfileReportSection(name, html, ShrinkButton, SecNumber, WidthDec);
        end
        
        function addTableSection(this, name, headings, colWidthsInPercent, colAlignment, rows, widthDec)
            html = coder.profile.ProfilerReportBuilder.getTableHtml(...
                headings, colWidthsInPercent, colAlignment, rows);
            this.addCustomSection(name, html, false, true, widthDec);
        end

        function addCustomTableSection(this, name, tableInfo, footNotes, linksStyle, ShrinkButton, SecNumber, WidthDec)
            this.SectionItems{end+1} = coder.profile.ProfileReportTable(name, tableInfo, footNotes, linksStyle, ShrinkButton, SecNumber, WidthDec);
        end
        
        function addCustomObj(this, section)
            this.SectionItems{end+1} = section;
        end
        
        function [lExecTimeReport, fileName] = build(this, lGlobalRegistry, varargin)
            fileName = this.getReportFileName(lGlobalRegistry, varargin);
            
            % First create object that provides e.g. MATLAB Coder or Simulink
            % Coder context
            context = feval(this.ReportFcn);
            
            % Now invoke the factory method to get the appropriate report
            % object
            lExecTimeReport = context.getReportObject(fileName, this.BuildDir);
            
            % title
            lExecTimeReport.setTitle(this.Title);
            
            % custom doc links
            if ~isempty(this.CustomDocLinks)
                lExecTimeReport.setHelpButtonHelpArgs(this.CustomDocLinks);
                lExecTimeReport.setIntroTextHelpArgs(this.CustomDocLinks);
            end
            
            % introduction
            if ~isempty(this.IntroMsgID)
                if isempty(this.DocText)
                    docLink = lExecTimeReport.getIntroHelpHyperlink();
                else
                    docLink = this.generateDocLink();
                end
                intro = sprintf('<p>%s</p>', DAStudio.message(this.IntroMsgID, docLink));
                if ~isempty(this.PostIntroMsgID)
                    intro = sprintf('%s<p>%s</p>\n', intro, DAStudio.message(this.PostIntroMsgID));
                end
                lExecTimeReport.addIntro(intro);
            end
            
            % sections
            for i=1:length(this.SectionItems)
                section = this.SectionItems{i};
                section.build(lExecTimeReport, fileName);
            end
            
            % definitions
            if ~isempty(this.Definitions)
                definitionHtml = '';
                keys = this.Definitions.keys;
                for i=1:length(keys)
                    k = keys{i};
                    definitionHtml = sprintf('%s<p><b><a>%s</a></b>: %s</p>',...
                        definitionHtml, k, this.Definitions(k));
                end
                lExecTimeReport.addSection('Definitions', definitionHtml);
            end
            
            fid = fopen(fileName, 'w', 'n', getEncoding(lExecTimeReport));
            fwrite(fid, lExecTimeReport.emitHTML, 'char');
            fclose(fid);
            
            dstFolder = fileparts(fileName);
            for i=1:length(this.Icons)
               icon = this.Icons{i};
               coder.profile.ProfilerReportBuilder.copySingleIcon(dstFolder, icon);
            end
            
            lExecTimeReport.launchReport(fileName, this.WindowTitle);
        end
    end
    
    methods(Access=private)
        
        function fileName = getReportFileName(this, lGlobalRegistry, varargin)
            if isempty(dir(this.BuildDir))
                reportFolder = pwd;
            else
                reportFolder = this.BuildDir;
            end
            
            reportFolder = fullfile(reportFolder, 'html');
            if isempty(dir(reportFolder))
                mkdir(reportFolder);
            end
                        
            if ~lGlobalRegistry.isTraceabilityValid
                reportFolder = fullfile(reportFolder, 'orphaned');
                if exist(reportFolder,'dir') ~= 7
                    mkdir(reportFolder)
                end
            end
            
            fileName = fullfile(reportFolder, ...
                coder.profile.ProfilerReportBuilder.getReportName(varargin{:}));
        end

        function docHyperlink = generateDocLink(this)
            docHyperlink = sprintf...
                ('<a href="matlab:helpview(''ecoder'', ''%s'')">%s</a>',...
                 this.DocTag, this.DocText);
        end
    end
    
    methods(Access=private, Static=true)
        function tableHtml = getTableHtml(headings, colWidthsInPercent, colAlignment, rows)
            idBackground = 1;
            backgroundColors = {'#eeeeff', '#ffffff'};
        
            % headers
            headingsHtml = '';
            for i=1:length(headings)
                headingHtml = sprintf(...
                    '<td width="%d%%" align="%s" style="border-style: none"><b>%s</b>\n</td>\n', ...
                    colWidthsInPercent(i), colAlignment{i}, headings{i});
                headingsHtml = sprintf('%s%s', headingsHtml, headingHtml);
            end
            if ~isempty(headingsHtml)
                headingsHtml = sprintf(...
                    '<tr style="background-color: %s">%s</tr>', ...
                    backgroundColors{idBackground}, headingsHtml);
                idBackground = 2;
            end
            
            % data
            rowsHtml = '';
            numCols = length(rows(1,:));
            for i=1:length(rows(:,1))
                cellsHtml = '';
                for ii1 =  1:numCols
                    cellContent = rows{i, ii1}; 
                    cellHtml = sprintf...
                              ('<td width="%d%%" align="%s" style="border-style: none">%s</td>\n', ...
                               colWidthsInPercent(ii1), colAlignment{ii1}, cellContent);
                    cellsHtml = sprintf('%s%s', cellsHtml, cellHtml);
                end

                rowHtml = sprintf('<tr style="border-style: none; background-color: %s">%s</tr>\n', ...
                                  backgroundColors{idBackground}, cellsHtml);
                idBackground = idBackground + 1;
                if idBackground > 2
                    idBackground = 1;
                end
                rowsHtml = sprintf('%s%s', rowsHtml, rowHtml);
            end

            % final table
            tableHtml = sprintf(...
                '<table width="100%%" cellpadding="2" border="0px">\n%s%s</table>\n', ...
                headingsHtml, rowsHtml);
        end
        
        function name = getReportName(varargin)
            % Return a name for the report that uniquely encodes the time 
            % when the execution profile object was created and the 
            % settings for the report
            asInts = coder.profile.md5(varargin{:});
            assert(length(asInts) == 4, 'Unexpected length for MD5 checksum');
            hash = '';
            for i = 1:length(asInts)
                currInt = asInts(i);
                currHex = lower(dec2hex(currInt, 8));
                
                % Need to reverse the hex pairwise
                hash = [hash, currHex(7), currHex(8), ...
                        currHex(5), currHex(6), ...
                        currHex(3), currHex(4), ...
                        currHex(1), currHex(2)]; %#ok<AGROW>
            end
            name = sprintf('ExecutionProfiling_%s.html', hash(1:2:end));
        end
        
        function copySingleIcon(dstFolder, iconName)
            dst = fullfile(dstFolder, iconName);
            if ~isfile(dst)
                src = fullfile(coder.profile.ExecHelper.ResourcesFolder, iconName);
                copyfile(src, dst);
                fileattrib(dst,'+w');
            end
        end
    end
end
