classdef DataStoreTimingUtils < handle
    % DATASTORETIMINGUTILS
    %
    %   Utility class to help with propagation of timing interfaces on
    %   DataReads and DataWrites from child models to parent models when
    %   using the model ref code interface at the top of a model hierarchy
    %   for SIL/PIL simulation.

    %   Copyright 2018-2020 The MathWorks, Inc.
    properties (GetAccess = private, SetAccess = immutable)
        RateSpecOfGlobalDSMAccessedByDescExpFcnMdlMap
        GlobalDSMPortGroups
        DSMPortGroups
        FcnCallPortGroups
        ParentTimingProperties
        ParentTimingModes
        ParentTimingPeriods
        ParentTimingOffsets
        ParentTidMap
        UseMF0CodeDescriptor
    end
    
    methods (Access = public)
        function this = DataStoreTimingUtils(lMf0, lParentCodeInfo, lUseMF0CodeDescriptor)
            % DATASTORETIMINGUTILS
            %   Constructor extracts required information from MF0 and
            %   parent CodeInfo.
            this.RateSpecOfGlobalDSMAccessedByDescExpFcnMdlMap = lMf0.rateSpecOfGlobalDSMAccessedByDescExpFcnMdlMap;
            this.GlobalDSMPortGroups = lMf0.portGroupsRequireSameRate.GlobalDSMPortGroups;
            this.DSMPortGroups = lMf0.portGroupsRequireSameRate.DSMPortGroups;
            this.FcnCallPortGroups = lMf0.fcnCallPortGroups.toArray;
            this.ParentTimingProperties = lParentCodeInfo.TimingProperties;
            this.ParentTimingModes = rtw.connectivity.CodeInfoUtils.getCharTimingModeNames({this.ParentTimingProperties.TimingMode});
            this.ParentTimingPeriods = [this.ParentTimingProperties.SamplePeriod];
            this.ParentTimingOffsets = [this.ParentTimingProperties.SampleOffset];
            this.ParentTidMap = ...
                        containers.Map( ...
                        num2cell(lParentCodeInfo.TimingInternalIds), ...
                        num2cell(lParentCodeInfo.TimingProperties));
            this.UseMF0CodeDescriptor = lUseMF0CodeDescriptor;
        end
        
        function updateSubModelGlobalDataStore(this, newSubModelDS, ds, mappedInheritedRates)
            
            % Cache the graphical name of the Data Store
            lGraphicalName = newSubModelDS.GraphicalName;
            
            % Update the SID of the promoted Data Store
            newSubModelDS.SID = ds.SID;
            
            % Reads/writes in context of original child model
            subModelDSDataReads = rtw.connectivity.CodeInfoUtils.getArray(newSubModelDS.DataReads);
            subModelDSDataWrites = rtw.connectivity.CodeInfoUtils.getArray(newSubModelDS.DataWrites);
            lHasReads = ~isempty(subModelDSDataReads);
            lHasWrites = ~isempty(subModelDSDataWrites);
            
            % INHERITED timing interfaces in child
            %
            % Push up any INHERITED timing interfaces from the child.  If
            % we have mapped inherited rates from Code Descriptor, update
            % these rates with correct values. Otherwise bubble them up to
            % the top.            
            lInheritedDirectReadsFromChild = subModelDSDataReads(strcmp({subModelDSDataReads.TimingMode}, 'INHERITED'));
            if ~isempty(lInheritedDirectReadsFromChild) && ~isempty(mappedInheritedRates)
                lInheritedDirectReadsFromChild = mappedInheritedRates;
            end
            lInheritedDirectWritesFromChild = subModelDSDataWrites(strcmp({subModelDSDataWrites.TimingMode}, 'INHERITED'));
            if ~isempty(lInheritedDirectWritesFromChild) && ~isempty(mappedInheritedRates)
                lInheritedDirectWritesFromChild = mappedInheritedRates;
            end
            
            % PERIODIC, IMPLICIT_TASKING in child
            %
            % Push up any implicit periodic timing interfaces from the child.
            % They'll be compared by value when we associate the DSM with
            % tasks, but the NonFcnCallPartitionName might be different.
            % So we need to cross-reference with the parent's timing
            % interfaces.  If a timing period is present in the child, it
            % will be present in the parent.
            %
            % Explicit timing interfaces will be handled later.
            lPeriodicDirectReadsFromChild = this.findEquvalentPeriodicTimingInParent(subModelDSDataReads);
            lPeriodicDirectWritesFromChild = this.findEquvalentPeriodicTimingInParent(subModelDSDataWrites);
            
            % INITIALIZE, TERMINATE in child - maps to equivalent in parent
            %
            % Find the equivalent initialize/terminate interface in the
            % parent.  If these are present in the child they will be
            % present in the parent.
            %
            % RESET in child - maps to equivalent in parent, but we need to
            % get the event that corresponds to the same event name.
            % There's no guarantee that the tid/offset will
            % be the same in the parent.  Associate any reset events from
            % the child with ALL reset events in the parent.  Means we'll
            % potentially transfer more than we need, but this is better
            % than not transferring.  To match the child event to a name
            % we'd need to load the child's binfo to get the name from the
            % timing interface's TID.
            lModelWideEventTimingModes = {'ONESHOT', 'INITIALIZE', 'TERMINATE', 'RESET'};
            lModelWideEventReadsFromChild = ...
                this.findEquivalentModelWideEventInParent(lModelWideEventTimingModes, ...
                subModelDSDataReads);
            lModelWideEventWritesFromChild = ...
                this.findEquivalentModelWideEventInParent(lModelWideEventTimingModes, ...
                subModelDSDataWrites);
            
            % ASYNCHRONOUS, EXPLICIT_TASKING in child
            % PERIODIC, IMPLICIT_TASKING in parent
            %
            % Check to see if there are any global DSMs that should have a
            % PERIODIC timing interface in the current model, but are
            % accessed through a descendant export functions model.
            [lPeriodicDirectReadsFromMF0, lPeriodicDirectWritesFromMF0] = ...
                this.findPeriodicRatesForGlobalDSMAccessedByDescExpFcnMdl(lGraphicalName, ...
                lHasReads, lHasWrites);
            
            % ASYNCHRONOUS, EXPLICIT_TASKING in child
            % PERIODIC or ASYNCHRONOUS EXPLICIT_TASKING in parent
            %
            % If the parent model is Export Functions, then we can get the
            % relevant parent TIDs from the port group.
            [lExplicitReadsFromMF0, lExplicitWritesFromMF0] = ...
                this.findExplicitRatesForDSM( ...
                lGraphicalName, lHasReads, lHasWrites);
            
            
            % Concatenate all of the timing interfaces we've found 
            newDataReads = [lInheritedDirectReadsFromChild, ...
                lPeriodicDirectReadsFromChild, ...
                lModelWideEventReadsFromChild, ...
                lPeriodicDirectReadsFromMF0, ...
                lExplicitReadsFromMF0];
            
            newDataWrites = [lInheritedDirectWritesFromChild, ...
                lPeriodicDirectWritesFromChild, ...
                lModelWideEventWritesFromChild, ...
                lPeriodicDirectWritesFromMF0, ...
                lExplicitWritesFromMF0];
            
            % Attach them to the ReadWriteDataInterface object
            if this.UseMF0CodeDescriptor
                newSubModelDS.DataReads.clear;
                for idx=1:numel(newDataReads)
                    newSubModelDS.DataReads.add(newDataReads(idx));
                end
                newSubModelDS.DataWrites.clear;
                for idx=1:numel(newDataWrites)
                    newSubModelDS.DataWrites.add(newDataWrites(idx));
                end
            else
                newSubModelDS.DataReads = newDataReads;
                newSubModelDS.DataWrites = newDataWrites;
            end
        end
    end
    
    methods (Access = private)
        function lPeriodicInParent = findEquvalentPeriodicTimingInParent(this, lChildTimingInterfaces)
            % Find equivalent periodic implicit timing periods in parent
            % given timing interfaces from child.
            lChildPeriodicAccess = lChildTimingInterfaces( ...
                strcmp({lChildTimingInterfaces.TimingMode}, 'PERIODIC') & ...
                strcmp({lChildTimingInterfaces.TaskingMode}, 'IMPLICIT_TASKING'));
            lChildAccessTimingPeriods = [lChildPeriodicAccess.SamplePeriod];
            lChildAccessTimingOffsets = [lChildPeriodicAccess.SampleOffset];
            bParentTimingIsInChildList = ...
                ismember(this.ParentTimingPeriods, lChildAccessTimingPeriods) & ...
                ismember(this.ParentTimingOffsets, lChildAccessTimingOffsets);
            lPeriodicInParent = this.ParentTimingProperties(bParentTimingIsInChildList);
        end
        
        function lParentEventTiming = findEquivalentModelWideEventInParent(this, lTimingModes, lChildTimingInterfaces)
            % Find equivalent IRT timing interfaces in parent given timing
            % interfaces from child.
            
            indices = ismember(arrayfun(@(x)char(x.TimingMode), lChildTimingInterfaces,'UniformOutput',false), lTimingModes);
            lInterfWithEvent = lChildTimingInterfaces(indices);
            lHasEventInChild = ~isempty(lInterfWithEvent);
            if lHasEventInChild
                lParentEventTiming = this.ParentTimingProperties(ismember(this.ParentTimingModes, lTimingModes));
            else
                lParentEventTiming = rtw.connectivity.CodeInfoUtils.getEmptyInterfaceCommon('TimingInterface', this.UseMF0CodeDescriptor);
            end
        end
        
        function [lPeriodicDirectReadsFromMF0, lPeriodicDirectWritesFromMF0] = findPeriodicRatesForGlobalDSMAccessedByDescExpFcnMdl(this, lGraphicalName, lHasReads, lHasWrites)
            % Check if the DSM is accessed in a descendant export functions
            % model, and return timing interfaces for the obtained rates
            % and offsets.
            lDSMAccessedByDescExpFcnMdlMap = this.RateSpecOfGlobalDSMAccessedByDescExpFcnMdlMap.entries;
            lRateSpecAssociation = lDSMAccessedByDescExpFcnMdlMap.getByKey(lGraphicalName);
            lPeriodicDirectReadsFromMF0 = rtw.connectivity.CodeInfoUtils.getEmptyInterfaceCommon('TimingInterface', this.UseMF0CodeDescriptor);
            lPeriodicDirectWritesFromMF0 = rtw.connectivity.CodeInfoUtils.getEmptyInterfaceCommon('TimingInterface', this.UseMF0CodeDescriptor);
            
            % The lookup in the map will return empty if not found.
            if ~isempty(lRateSpecAssociation)
                % MF0 gives us the timing period and offset for each DSM
                % accessed through a descendant export functions model.
                lRateSpecInfo = lRateSpecAssociation.rateSpecInfoVect.toArray;
                lChildTimingPeriodsFromMF0 = arrayfun(@(x)x.rateSpec.period, lRateSpecInfo);
                lChildTimingOffsetsFromMF0 = arrayfun(@(x)x.rateSpec.offset, lRateSpecInfo);
                
                % Find the equivalent timing interfaces in the parent
                % model's codeInfo. Don't use short circuit operation because we are &-ing
                % two vectors.
                bParentTimingIsInChildList = ...
                    ismember(this.ParentTimingPeriods, lChildTimingPeriodsFromMF0) & ...
                    ismember(this.ParentTimingOffsets, lChildTimingOffsetsFromMF0);
                
                % We don't know if these timing associations are reads or
                % writes, so attach to both.
                if lHasReads
                    lPeriodicDirectReadsFromMF0 = this.ParentTimingProperties(bParentTimingIsInChildList);
                end
                
                if lHasWrites
                    lPeriodicDirectWritesFromMF0 = this.ParentTimingProperties(bParentTimingIsInChildList);
                end
            end
        end
        
        function [lExplicitReadsFromMF0, lExplicitWritesFromMF0] = findExplicitRatesForDSM(this, lGraphicalName, lHasReads, lHasWrites)
            % See if the DSM is associated with a port group.
            %
            % If so, find the TID.
            %
            % From the TID we can find the associated timing interface in
            % the parent's CodeInfo.
            lNameIntVectAssociation = this.GlobalDSMPortGroups.entries.getByKey(lGraphicalName);
            
            % It could be a shared DSM if not found, so check there too -
            % it won't be both so we can use the same variable.
            if isempty(lNameIntVectAssociation)
                lNameIntVectAssociation = this.DSMPortGroups.entries.getByKey(lGraphicalName);
            end
            lExplicitReadsFromMF0 = rtw.connectivity.CodeInfoUtils.getEmptyInterfaceCommon('TimingInterface', this.UseMF0CodeDescriptor);
            lExplicitWritesFromMF0 = rtw.connectivity.CodeInfoUtils.getEmptyInterfaceCommon('TimingInterface', this.UseMF0CodeDescriptor);
            if ~isempty(lNameIntVectAssociation)
                lIdxVect = lNameIntVectAssociation.idxVect.toArray;
                lAssociatedPortGroups = this.FcnCallPortGroups(lIdxVect+1);
                if ~isempty(lAssociatedPortGroups)
                    lTimingInterfacesForExplicit = arrayfun(@(x)this.ParentTidMap(x), [this.FcnCallPortGroups.tid]);
                    
                    % Can't distinguish between reads and writes - so we
                    % just have to push up both unless we know there are
                    % none at all.
                    if lHasReads
                        lExplicitReadsFromMF0 = lTimingInterfacesForExplicit;
                    end
                    if lHasWrites
                        lExplicitWritesFromMF0 = lTimingInterfacesForExplicit;
                    end
                end
            end
        end
    end
    
    methods (Access = public, Static)
        function lUniqueDS = mergeSubModelDataStoreTimings(lTopModelDS, lSubModelDS, lUseMF0CodeDesc)
            % Given a list of top model data stores and sub model data
            % stores, with non-unique data store names, merge the DataReads
            % and DataWrites of non-unique entries to create a combined
            % list of unique data stores.
            
            lCombinedDS = [lTopModelDS, lSubModelDS];
            lTopModelDSNames = {lTopModelDS.GraphicalName};
            lSubModelDSNames = {lSubModelDS.GraphicalName};
            lCombinedDSNames = [lTopModelDSNames, lSubModelDSNames];
            [~,lUniqueIdx] = unique(lCombinedDSNames, 'stable');
            
            lUniqueDS = lCombinedDS(lUniqueIdx);
            lUniqueDSNames = lCombinedDSNames(lUniqueIdx);
            
            % We now need to concatenate the reads/writes from the entries
            % that we've removed.
            bWasRemovedByUnique = true(1, numel(lCombinedDS));
            bWasRemovedByUnique(lUniqueIdx) = false;
            lResidualDS = lCombinedDS(bWasRemovedByUnique);
            lResidualDSNames = lCombinedDSNames(bWasRemovedByUnique);
            
            for kDI = 1:numel(lResidualDS)
                % We know that if a data store is on the list of residual
                % data stores, there is an instance of the name in
                % dsNames - so all we need is the index.
                [~, idx] = ismember(lResidualDSNames{kDI}, lUniqueDSNames);
                
                % Concatenate the DataReads/DataWrites.                               
                lCurrUniqueDSReads = unique([...
                    rtw.connectivity.CodeInfoUtils.getArray(lUniqueDS(idx).DataReads), ...
                    rtw.connectivity.CodeInfoUtils.getArray(lResidualDS(kDI).DataReads)], 'stable');
                lCurrUniqueDSWrites = unique([...
                    rtw.connectivity.CodeInfoUtils.getArray(lUniqueDS(idx).DataWrites), ...
                    rtw.connectivity.CodeInfoUtils.getArray(lResidualDS(kDI).DataWrites)], 'stable');
                
                if lUseMF0CodeDesc
                    lUniqueDS(idx).DataReads.clear;
                    for uniqueRIdx = 1:numel(lCurrUniqueDSReads)
                        lUniqueDS(idx).DataReads.add(lCurrUniqueDSReads(uniqueRIdx));
                    end
                    
                    lUniqueDS(idx).DataWrites.clear;
                    for uniqueWIdx = 1:numel(lCurrUniqueDSWrites)
                        lUniqueDS(idx).DataWrites.add(lCurrUniqueDSWrites(uniqueWIdx));
                    end
                else
                    lUniqueDS(idx).DataReads = lCurrUniqueDSReads;
                    lUniqueDS(idx).DataWrites = lCurrUniqueDSWrites;
                end
                
            end
        end
        
        function mappedInheritedChildRates = mapChildInheritedRatesToParentRates(childCodeInfo, parentCodeInfoTimingProperties, childInstances)
            % Map any child inherited rates to the rate attached to the
            % model block for each instance. Use Parent to Child TID map
            % from Code Descriptor.
            mappedInheritedChildRates = coder.descriptor.TimingInterface.empty;
            for idx = 1:numel(childInstances)
                childToParentTIDMap = childInstances(idx).ChildTIDToParentTIDMap.toArray;
                for tidIdx = 1:numel(childToParentTIDMap)
                    if strcmp(childCodeInfo.TimingProperties(childToParentTIDMap(tidIdx).ChildTID+1).TimingMode,'INHERITED')
                        mappedInheritedChildRates(end+1) = parentCodeInfoTimingProperties(childToParentTIDMap(tidIdx).ParentTID+1); %#ok<AGROW>
                    end
                end
            end
        end
    end
end
