classdef SignalTolerancesStore < handle
    %% SIGNALTOLERANCESSTORE stores the signals logged for a given run in the last known
    % simulation along with the system behavior tolerance specification
    
    % Copyright 2018-2023 The MathWorks, Inc.
    
    properties
        SDIRunID int32;
        DataStore table; % Stores the table of all signals along with signal ids
        %and their signal tolerance specification object
        LastHighlightedSignal struct;
    end
    
    methods
        function create(obj, models, sdiRunId)
            % Update run id for processing later
            obj.SDIRunID = sdiRunId;
            dataStore = {};
            
            for idy = 1:numel(models)
                model = models{idy};
                %  Get list of all signals which have signal logging on
                instrumentedSignals = get_param(model, 'InstrumentedSignals');
                if isempty(instrumentedSignals)
                    continue;
                end
                
                dataStorePerModel = obj.collectSignalInformation(instrumentedSignals);
                
                dataStore = [dataStore; dataStorePerModel]; %#ok<AGROW>
            end
            
            % If the current store is empty, assign DataStore property to
            % the logged signals in the model
            % If the model does not have any logged signals, assign
            % DataStore property to empty
            if isempty(obj.DataStore) || isempty(dataStore)
                obj.DataStore = dataStore;
            else
                obj.DataStore = obj.getSignalUpdatesFromModel(dataStore);
            end
        end
        function update(obj, store)
            obj.DataStore = store;
        end
        function clear(obj)
            if ~isempty(obj.DataStore)
                obj.DataStore(1:height(obj.DataStore),:) = [];
            end
        end
        function updateTolerances(obj, tolerances)
            % Update the tolerances values in the store using the client
            % data. The client data contains the tolerance field, value and
            % the signal information that was updated in the signal
            % tolerances table in the setup pane
            storeSignalIds = [obj.DataStore.ID{:}];
            storeSignalBlockPaths = {storeSignalIds.BlockPath};
            storeSignalPorts = [storeSignalIds.Port];
            
            clientRow = tolerances.rowData;
            signalId = clientRow.signalId;
            
            % Query the signalTolerancesStore by signal id to query the path
            % and port index
            blockPathIdx = cellfun(@(x) strcmp(x, signalId.BlockPath), storeSignalBlockPaths, 'UniformOutput', false);
            blockPathIdx = [blockPathIdx{:}];
            portIdx = (storeSignalPorts==signalId.Port);
            
            % For each tolerance field, updateSignalTolerancesStore data
                % error('this is an error');
            toleranceType = tolerances.columnName;
            mdl = bdroot(tolerances.rowData.signalId.BlockPath);
            toleranceValue = evalinGlobalScope(mdl, tolerances.columnValue);
            toleranceStr = mat2str(toleranceValue);
            
            % Query the signalTolerancesStore by matching Path and Port index
            % Retrieve the tolerance for the matched signal using the field name
            existingValue = obj.DataStore(blockPathIdx & portIdx, toleranceType);
            if (~isempty(existingValue.(toleranceType)) && isempty(toleranceValue)) || ... % client value is empty, server value is not empty
                    (~isempty(toleranceValue) && ischar(toleranceStr)) % not empty value from client
                obj.DataStore(blockPathIdx & portIdx, toleranceType) = {toleranceStr};
            end
        end
    end
    methods (Access=private)
        function newSignalsTable = getSignalUpdatesFromModel(obj, dataStorePerModel)
            % Compare the signal information in the model with signal
            % information in data store. Update the store with newly added
            % or removed signals
            
            currSignalsTable = obj.DataStore;
            newSignalsTable = dataStorePerModel;
            
            % Fetch the list of common signals . This will remove any
            % disabled signals from the list
            [~, commonSignalsIdx, ~ ] = intersect(currSignalsTable.Path, newSignalsTable.Path);
            if isempty(commonSignalsIdx)
                return;
            end
            
            commonSignalsTable = currSignalsTable(commonSignalsIdx, :);
            
            % Fetch the list of signals not in data Store but that are
            % enabled in the model (added signals)
            [~, addedSignalsIdx] = setdiff(newSignalsTable.Path, currSignalsTable.Path);
            addedSignalsTable = newSignalsTable(addedSignalsIdx, :);
            
            % Add the signals to the table
            newSignalsTable = [commonSignalsTable; addedSignalsTable];
        end
        
        function tableStore = collectSignalInformation(~, instrumentedSignals)
            signalCount = instrumentedSignals.Count;
            
            % Create signal table
            initStruct = struct('ID', [], 'BlockPath', '', 'PortIndex', [], 'Path', '', 'Name', '', 'AbsTol', [], 'RelTol', [], 'TimeTol', []);
            
            signalStructs(1:signalCount) = initStruct;
            validSignals = false(signalCount,1);
            for sIndex = 1:signalCount
                signal = instrumentedSignals.get(sIndex);
                
                try
                    % Get cell array of blockPaths
                    path = signal.BlockPath.convertToCell();
                    
                    % g1922865, g1922898 - block paths and port  index
                    %registered as "InstrumentedSignals" can be invalid.
                    % Validate the path and port index before constructing
                    % a signal tolerances store row.
                    portHandles = get_param(path{1}, 'PortHandles');
                    validPortHandle = portHandles.Outport(signal.OutputPortIndex); %#ok
                    
                    signalName = signal.getSignalNameFromModel;
                    if isempty(signalName)
                        name = get_param(path{1}, 'Name');
                        signalName = [name, ':', num2str(signal.OutputPortIndex)];
                    end
                    
                    signalStructs(sIndex).BlockPath = path;
                    signalStructs(sIndex).PortIndex = {signal.OutputPortIndex};
                    signalStructs(sIndex).Path = {[path{1} sprintf(':%i', signal.OutputPortIndex)]};
                    signalStructs(sIndex).Name = {signalName};
                    signalStructs(sIndex).AbsTol = {[]};
                    signalStructs(sIndex).RelTol = {[]};
                    signalStructs(sIndex).TimeTol = {[]};
                    signalStructs(sIndex).ID = {struct('BlockPath', path, 'Port', signal.OutputPortIndex)};
                    
                    % mark signal as valid
                    validSignals(sIndex) = true;
                catch err %#ok
                    % On any error in querying the signal name, block path
                    % or port index, invalidate the signal
                    validSignals(sIndex) = false;
                end
            end
            signalStructs = signalStructs(validSignals);
            
            tableStore = table;
            % If there are valid signals
            if ~isempty(signalStructs)
                tableStore = struct2table(signalStructs);
            end
        end
    end
end
