classdef TraceabilityHelper < handle
%TRACEABILITYHELPER assists with analyzing profiling traceability data
%

%   Copyright 2015-2023 The MathWorks, Inc.
    
    properties(Access=private)
        OriginalComponentName;
        AllModels;
        AllCodeDescriptors;
        ModelBlockSIDs = {};
        ModelBlockReferencedModelNames = {};
        AllProfiledSIDs;
        AllIntermediateSIDs;
        TaskLevelSID;
        SIDMapTemporaryToOriginal;
        SIDMapOriginalToTemporary;
        IsSubsystemBuild;
        SubsystemBuildTemporaryModelName;
    end
    
    methods (Access=private)
        
        function populateSubSystemBuildMappings(this, lCodeFolder)
            profInfoFile = fullfile(lCodeFolder, 'profilingTraceability.mat');
            if exist(profInfoFile, 'file')
                profInfo = load(profInfoFile);
            else
                profInfo = [];
            end
            if ~isempty(profInfo) && isfield(profInfo.profTraceInfo, 'RightClickSIDMap')
                lSIDMapTemporaryToOriginal = profInfo.profTraceInfo.RightClickSIDMap;
            else
                % Empty map
                lSIDMapTemporaryToOriginal = containers.Map;
            end
            
            % Additionally need the reverse mapping
            lSIDMapOriginalToTemporary = containers.Map;
            
            keys = lSIDMapTemporaryToOriginal.keys;
            for i=1:length(keys)
                key = keys{i};
                lSIDMapOriginalToTemporary...
                    (lSIDMapTemporaryToOriginal(key)) = key;
            end
            
            this.SIDMapTemporaryToOriginal = lSIDMapTemporaryToOriginal;
            this.SIDMapOriginalToTemporary = lSIDMapOriginalToTemporary;
        end

    
        function [sidsOut, lModelBlocksOut] = ...
                rightClickTransformSIDs(this, sidsIn, lModelBlocksIn)
            sidsOut = sidsIn;
            keepIdx = true(size(sidsOut));
            for i=1:length(sidsIn)
                sidIn = sidsIn{i};
                if strcmp(strtok(sidIn, ':'), ...
                        this.SubsystemBuildTemporaryModelName)
                    if isKey(this.SIDMapTemporaryToOriginal, sidIn)
                        sidsOut{i} = this.SIDMapTemporaryToOriginal(sidIn);
                    else
                        % If the SID was not in the map and still
                        % corresponds to the temporary model
                        % name then we replace it with the SID for the
                        % task level block
                        keepIdx(i) = false;
                    end
                end
            end
            sidsOut = sidsOut(keepIdx);
            lModelBlocksOut = lModelBlocksIn(keepIdx);
        end    
        
        function parentBlockSID = rightClickGetParentBlock(this, bhm, sid, currentModel)
        % For right-click subsystem builds we need a a transformation between SIDs
        % recorded in the ExecutionTime object and SIDs recorded in the code
        % descriptor @todo (g1337883)
            
            sidTemporaryModel = this.SIDMapOriginalToTemporary(sid);
            blocks = bhm.getBlocksBySID(sidTemporaryModel);
            block = coder.descriptor.GraphicalBlock.empty;
            if ~isempty(blocks)
                block = blocks(1);
            end
            if isempty(block)
                % Default to current model if block not found in code
                % descriptor
                parentBlockSID = currentModel;
            else
                parentBlockSID = block.ParentSystemSID;
                if this.SIDMapTemporaryToOriginal.isKey(parentBlockSID)
                    parentBlockSID = this.SIDMapTemporaryToOriginal(parentBlockSID);
                else
                    % Reached the right click build subsystem itself
                    parentBlockSID = this.TaskLevelSID;
                end
            end
        end
        
        function findChildCodeDescriptors(this, lCodeDescriptor)
                        
            blockHierachyMap = getBlockHierarchyMap(lCodeDescriptor);
          
            lModelBlocks = getBlocksByType(blockHierachyMap, 'ModelReference');
            N = length(lModelBlocks);
            [lModelBlockSIDs{1:N}] = lModelBlocks(:).SID; 
            
            if this.IsSubsystemBuild
                % SID transformation for right-click subsystem build
                [lModelBlockSIDs, lModelBlocks] = ...
                    rightClickTransformSIDs(this, lModelBlockSIDs, lModelBlocks);                
            end
            
            N = length(lModelBlockSIDs);
            this.ModelBlockSIDs(end+1:end+N) = lModelBlockSIDs(:);
            if (~isempty(lModelBlocks))
                [lRefModelNames{1:N}] = lModelBlocks(:).ReferencedModelName;
            else
                [lRefModelNames{1:N}] = [];
            end

            % Filter out any protected models
            if (~isempty(lModelBlocks))
                [lIsProtectedModel{1:N}] = lModelBlocks(:).IsProtectedModelBlock;
            else
                [lIsProtectedModel{1:N}] = [];
            end
            lIsProtectedModel = cell2mat(lIsProtectedModel);
            lRefModelNames = lRefModelNames(~lIsProtectedModel);
            
            N = length(lRefModelNames);
            this.ModelBlockReferencedModelNames(end+1:end+N) = lRefModelNames;            
            
            childModels = unique(lRefModelNames); 
            childModels = setdiff(childModels, this.AllModels);
            childDescriptors = lCodeDescriptor([]);
            for i=1:length(childModels)
                childDescriptors(end+1) = ...
                    getReferencedModelCodeDescriptor(lCodeDescriptor, ...
                    childModels{i}); %#ok<AGROW>
            end
            
            this.AllModels = [this.AllModels; childModels(:)];
            this.AllCodeDescriptors = [this.AllCodeDescriptors; childDescriptors(:)];
            
            for i=1:length(childModels)
                findChildCodeDescriptors(this, childDescriptors(i));
            end
            
        end
        
        
        function graphicalParentSIDs = getGraphicalParentSIDs(this, sid)
            
            currentModel = strtok(sid, ':');
            
            % Query code descriptor block hierarchy map to find graphical parent within
            % current model
            modelIdx = strcmp(this.AllModels, currentModel);
            
            lCodeDescriptor = this.AllCodeDescriptors(modelIdx);

            bhm = getBlockHierarchyMap(lCodeDescriptor);
           
            if this.IsSubsystemBuild && ...
                    strcmp(currentModel, strtok(this.TaskLevelSID, ':'))
                % Right click subsystem build case
                parentBlockSID = rightClickGetParentBlock(this, bhm, sid, currentModel);
            else
                % All other cases
                blocks = bhm.getBlocksBySID(sid);
                block = coder.descriptor.GraphicalBlock.empty;
                if ~isempty(blocks)
                    block = blocks(1);
                end
                if isempty(block) || ~isprop(block, 'ParentSystemSID') || isempty(block.ParentSystemSID)
                    % Default to current model if block not found in code
                    % descriptor
                    parentBlockSID = currentModel;
                else
                    parentBlockSID = block.ParentSystemSID;
                end
            end
           
            if strcmp(parentBlockSID, this.TaskLevelSID)
                % Reached the top
                graphicalParentSIDs = {parentBlockSID};
            elseif strcmp(parentBlockSID, currentModel)
                % Look for blocks referencing this model
                matchIdx = ...
                    strcmp(this.ModelBlockReferencedModelNames, currentModel);
                graphicalParentSIDs = this.ModelBlockSIDs(matchIdx(:));
            else
                graphicalParentSIDs = {parentBlockSID};
            end
            
        end        
        
        function [profiledParentSIDs, intermediateSIDs] = ...
                getProfiledGraphicalParentSIDs(this, sidIn)
            
        % First find the graphical parent systems, irrespective of whether the parent
        % itself is profiled
            
            profiledParentSIDs = {};
            intermediateSIDs = {};
            graphicalParentSIDs = getGraphicalParentSIDs(this, sidIn);
            for i=1:length(graphicalParentSIDs)
                sid = graphicalParentSIDs{i};
                if any(strcmp(sid, this.AllProfiledSIDs))
                    profiledParentSIDs{end+1} = sid; %#ok
                    intermediateSIDs{end+1} = {}; %#ok<AGROW>
                else
                    [tmpProfiledParentSIDs, tmpIntermediateSIDs] = ... 
                        getProfiledGraphicalParentSIDs(this, sid);
                    N = length(tmpProfiledParentSIDs);
                    for ii2=1:N
                        tmpSIDs = tmpIntermediateSIDs{ii2};
                        tmpSIDs{end+1} = sid; %#ok<AGROW>
                        intermediateSIDs{end+1} = tmpSIDs; %#ok<AGROW>
                    end
                    profiledParentSIDs = [profiledParentSIDs tmpProfiledParentSIDs]; %#ok<AGROW>
                end
            end
        end
        
        
        function val = isCallSiteFeasible(this, section, sidIn)
            
            if isTask(getTraceInfo(section))
                val = true;
            else
                lProfiledGraphicalParentSIDs = ...
                    getProfiledGraphicalParentSIDs(this, sidIn);
                
                % In the execution profile hierarchy, identify the parent section and its
                % associated call site SIDs
                parentSection = section.getParent;
                parentTraceInfo = getTraceInfo(parentSection);     
                if isempty(parentTraceInfo)
                    % with capture mode, functions with reusable code do not
                    % have a valid parent to investigate. So, return the current
                    % entry as feasible
                    val = true;
                else
                    parentSectionCallSites = getCallSiteSids(parentTraceInfo);
                    
                    % Filter out graphical parents that don't match the profiling hierarchy
                    lFeasibleGraphicalParentSIDs = intersect...
                        (lProfiledGraphicalParentSIDs, parentSectionCallSites);
                    
                    feasibleIdx = false(size(lFeasibleGraphicalParentSIDs));
                    for i=1:length(lFeasibleGraphicalParentSIDs)
                        sid = lFeasibleGraphicalParentSIDs{i};
                        
                        feasibleIdx(i) = isCallSiteFeasible(this, parentSection, sid);
                    end
                    val = any(feasibleIdx);
                end
            end            
        end
        
        function updateIntermediateSIDs(this)
            
            for i=1:length(this.AllProfiledSIDs)
                sid = this.AllProfiledSIDs{i};
                
                if strcmp(sid, this.TaskLevelSID)
                    % At the top of profiling hierarchy
                    continue
                end

                [~, lIntermediateSIDs] = ...
                    getProfiledGraphicalParentSIDs(this, sid);   
                
                % Save the intermediate SIDs
                for jj3=1:length(lIntermediateSIDs)
                    N = length(lIntermediateSIDs{jj3});
                    this.AllIntermediateSIDs(end+1:end+N) = ...
                        lIntermediateSIDs{jj3};
                end            
            end                
            this.AllIntermediateSIDs = unique(this.AllIntermediateSIDs);
        end
        
    end
    
    methods (Access=public)
        
        function sids = getAllIntermediateSIDs(this)
            % Intermediate SIDs are for model components that contain a
            % profiled block but are not themselves profiled; this
            % information is used by the profiling Informer to highlight
            % these model components.
            this.AllIntermediateSIDs = {};
            updateIntermediateSIDs(this);
            sids = this.AllIntermediateSIDs;
        end
        
        
        function sids = getFeasibleCallSiteSIDs(this, section, lCallSiteSIDs)
            
            % The current section may have multiple SIDs associated with it
            % based on information that was statically determined. By
            % comparing the ancestry within the execution profiling dynamic
            % call tree with the graphical model hierarchy, we eliminate
            % call site SIDs that do correspond to the current execution
            % profile section.
            isFeasible = false(size(lCallSiteSIDs));
            for i=1:length(lCallSiteSIDs)
                isFeasible(i) = isCallSiteFeasible(this, section, lCallSiteSIDs{i});
            end
            sids = lCallSiteSIDs(isFeasible);
            
        end
        

        function [sids, lTaskLevelSID] = getAllProfiledSids(~, lExecutionTime, lAllModels)
            
            sids = {};
            lTaskLevelSID = '';
            for i=1:length(lExecutionTime.Sections)
                section = lExecutionTime.Sections(i);
                lTraceInfo = section.getTraceInfo;
                if ~isempty(lTraceInfo)
                    sids = union(sids, getUnresolvedCallSiteSids(lTraceInfo));
                    if isTask(lTraceInfo)
                        lTaskLevelSID_tmp = getUnresolvedCallSiteSids(lTraceInfo);
                        assert(iscell(lTaskLevelSID_tmp) && length(lTaskLevelSID_tmp)==1, ...
                               'Task level SID must be unique');
                        lTaskLevelSID_tmp = lTaskLevelSID_tmp{1};
                        if ~isempty(lTaskLevelSID)
                            assert(isequal(lTaskLevelSID, lTaskLevelSID_tmp), ...
                                   'The task level SID must be unique');
                        else
                            lTaskLevelSID = lTaskLevelSID_tmp;
                        end
                    end                                
                end
            end
            
            % Filter out any SIDs that don't correspond to a model in the hierarchy
            modelPart = coder.profile.private.getTopModelFromBlock(sids);
            keepIdx = false(size(sids));
            for i=1:length(lAllModels)
                keepIdx = keepIdx | strcmp(modelPart, lAllModels{i});
            end
            sids = sids(keepIdx);
        end
        
        function this = TraceabilityHelper(lCodeFolder, lOriginalComponentName, ...
                lExecutionTime)
            
            this.OriginalComponentName = lOriginalComponentName;
            
            % For real-time profiling results, it is possible that the
            % build folder no longer exists or is stale.
            try
                if isvarname(this.OriginalComponentName) && bdIsLoaded(this.OriginalComponentName)
                    % For some workflows, e.g., SIL block is reused in the
                    % location different from the original one where it is
                    % created, the profiling should work in the new
                    % location where the SIL block is not even loaded. In
                    % this case, skip this. Or in the workflow where
                    % PIL block is created from a subsystem, and it is
                    % put in the test harness, the passed model name
                    % refers to the original name for the PIL block that
                    % has the '/' in its name. This invalidates variable
                    % name rules, so skip this.
                    sdpTypes = coder.internal.rte.SDPTypes(this.OriginalComponentName);
                    buildDir = RTW.getBuildDir(this.OriginalComponentName).BuildDirectory;
                    if sdpTypes.CodeInterface == coder.internal.rte.CodeInterface.Service
                        exeFolder = sdpTypes.getServiceFolders(buildDir).exeFolder;
                        if ~isempty(exeFolder)
                            lCodeFolder = buildDir;
                        end
                    end
                end
                lCodeDescriptor = coder.profile.private.loadCodeDescriptor(lCodeFolder);
            catch e
                msg = e.message;
                MSLDiagnostic...
                    ('CoderProfile:ExecutionTime:CodeDescriptorNotLoadedWarn', msg).reportAsWarning;
                return;
            end
            
            % For right-click subs-system build case, need mapping for code descriptor SIDs
            this.IsSubsystemBuild = ...
                ~strcmp(lOriginalComponentName, strtok(lOriginalComponentName, '/'));
                
            if this.IsSubsystemBuild 
                this.SubsystemBuildTemporaryModelName = lCodeDescriptor.ModelName;
                populateSubSystemBuildMappings(this, lCodeFolder);
            end
            
            this.AllModels = {strtok(lOriginalComponentName, '/')};
            this.AllCodeDescriptors = lCodeDescriptor;
            
            findChildCodeDescriptors(this, lCodeDescriptor);
            
            % Identify all SIDs corresponding to profiled blocks.
            [lAllProfiledSIDs, lTaskLevelSID] = getAllProfiledSids...
                (this, lExecutionTime, this.AllModels);
            this.TaskLevelSID = lTaskLevelSID;
            this.AllProfiledSIDs = lAllProfiledSIDs;
            
        end
    end
    
    methods(Access = public, Static = true)
        function extAncestors = extendAncestors(lAncestors)
            extAncestors = {};
            for i = 1:length(lAncestors)
                extAncestors = [extAncestors, lAncestors{i}]; %#ok<AGROW>
                parent = get_param(lAncestors{i}, 'Parent');
                while isequal(get_param(parent, 'type'), 'block') 
                    extAncestors = [extAncestors, Simulink.ID.getSID(parent)]; %#ok<AGROW>
                    parent = get_param(parent, 'Parent');
                end
            end
        end
    end
end

