classdef (Hidden = true) SIAdaptor < rtw.connectivity.ISubsysAdaptor
    % SIAdaptor - Adaptor class from coder.descriptor.SubsystemInterface to
    % coder.descriptor.ComponentInterface
    %
    %   This is an undocumented class. Its methods and properties are likely to
    %   change without warning from one release to the next.
    
    %   Copyright 2019-2023 The MathWorks, Inc.
    
    properties(Access = private)
        SubsystemInterface;
    end
 
    methods(Access = public)
        function this = SIAdaptor(aComponentInterface, codeDesc, componentSID)
            this = this@rtw.connectivity.ISubsysAdaptor(aComponentInterface, codeDesc, componentSID);

        end
        function set = getSILUnsupportedPortsSet(this)
            set = this.SubsystemInterface.SILUnsupportedPortsSet.toArray;
        end
    end
    
    
    methods(Access = protected)
        function populateAdaptor(this, codeDesc, subsysSID)

            mf0Model = codeDesc.getMF0Model;
            % assign the SubsystemInterface
            if isempty(subsysSID)
                % RLS has the SID from the code context, which is not
                % available/loaded at this stage.
                % Assume only one subsystem for this workflow 
                assert(this.CodeDescObj.Subsystems.Size == 1, ...
                    'Only one SubsystemInterface is expected in CodeDescriptor');
                ssInterface = this.CodeDescObj.Subsystems(1);
            else
                ssInterface = coder.descriptor.SubsystemInterface.findSubsystemInterfaceBySubsystemSID(mf0Model, subsysSID);
            end
            
            this.SubsystemInterface = ssInterface;

            this.replaceOptimizedIOPortImplementations();
            this.sortTimingInterfaces();
            this.UsesNonFiniteUtils = this.SubsystemInterface.UsesNonFiniteUtils;
            
            % use function interface adapters to make custom Getters
            % available for the function interface properties
            this.populateFunctionsArray('InitializeFunctions', this.SubsystemInterface);
            this.populateFunctionsArray('OutputFunctions', this.SubsystemInterface);
            this.populateFunctionsArray('TerminateFunctions', this.SubsystemInterface);
            this.populateFunctionsArray('UpdateFunctions', this.SubsystemInterface);
            this.populateFunctionsArray('EnableFunction', this.SubsystemInterface);
            this.populateFunctionsArray('DisableFunction', this.SubsystemInterface);
            
            this.SetupRuntimeResourcesFunction = this.createFunctionInterfaceAdaptor(...
                this.SubsystemInterface.SetupRuntimeResourcesFunction);
            
            this.CleanupRuntimeResourcesFunction = this.createFunctionInterfaceAdaptor(...
                this.SubsystemInterface.CleanupRuntimeResourcesFunction);
            
            % do not expose function interfaces from the ComponentInterface
            this.AllocationFunction = rtw.connectivity.SlFIAdaptor.empty;
            this.ConstructorFunction = rtw.connectivity.SlFIAdaptor.empty;           
            this.SystemResetFunction = rtw.connectivity.SlFIAdaptor.empty;
            this.InitConditionsFunction = rtw.connectivity.SlFIAdaptor.empty;
            this.DerivativeFunction = rtw.connectivity.SlFIAdaptor.empty;
            this.SystemInitializeFunction = rtw.connectivity.SlFIAdaptor.empty;
        end
        
        function prop = getProp(this, propName)
            switch propName
                case {'Inports', 'Outports', 'InternalData', 'Types', 'TimingProperties', ...
                      'NonSampledZeroCrossings', 'ZeroCrossingEvents', ...
                      'DataStores', 'Parameters'}
                    % access the property on the SubsystemInterface
                    prop = this.SubsystemInterface.(propName);
                case 'TimingInternalIds'
                    assert(false, 'Should not access TimingInternalIds for SubsystemInterface');
                case 'ServerCallPoints'
                    % It is not possible to create an empty sequence, so we
                    % use a struct with a toArray member that can be
                    % accessed by the property getter in InterfaceAdaptor
                    prop = struct('toArray', []);
                otherwise
                    % access the property on the ComponentInterface
                    prop = this.CodeDescObj.(propName);
            end
        end
        
        function populateInternalParameters(this)
           % setup Internal_Parameters
           
           % At subsystem level 
           % getBreakpointDataInterfacesWithDifferentImplementationThanLUT
           % may return interfaces that are already part of the Parameters
           % property. 
           % Remove duplication.
           
           params = this.SubsystemInterface.Parameters.toArray();
           breakpoints = this.SubsystemInterface.getBreakpointDataInterfacesWithDifferentImplementationThanLUT();
           
           numBreakpoints = numel(breakpoints);
           numParams = numel(params);
           keepIdx = true(numBreakpoints, 1);
           for bIdx = 1:numBreakpoints
               for pIdx = 1:numParams
                   if breakpoints(bIdx).Implementation == params(pIdx).Implementation
                       keepIdx(bIdx) = false;
                       break;
                   end
               end
           end
           
           this.Internal_Parameters = [params, breakpoints(keepIdx)];
        end
    end
    
    methods(Access = public)
        function replaceTiming(this, timingToReplace, newTiming)
            % replace the matching timing interfaces with a new one
            
            timingProperties = this.SubsystemInterface.TimingProperties.toArray();
            isTimingToReplace = strcmp([timingProperties.TimingMode], timingToReplace);
            
            if any(isTimingToReplace)               
                % replace in TimingProperties
                for idx = 1:this.SubsystemInterface.TimingProperties.Size()
                    if isTimingToReplace(idx)
                        this.SubsystemInterface.TimingProperties(idx) = newTiming;
                    end
                end

                % replace in Functions
                rtw.connectivity.ISubsysAdaptor.doReplaceTiming(timingToReplace, ...
                    this.SubsystemInterface.OutputFunctions.toArray, newTiming);
                rtw.connectivity.ISubsysAdaptor.doReplaceTiming(timingToReplace, ...
                    this.SubsystemInterface.UpdateFunctions.toArray, newTiming);
                % replace in IO ports
                rtw.connectivity.ISubsysAdaptor.doReplaceTiming(timingToReplace, ...
                    this.SubsystemInterface.Inports.toArray, newTiming);
                rtw.connectivity.ISubsysAdaptor.doReplaceTiming(timingToReplace, ...
                    this.SubsystemInterface.Outports.toArray, newTiming);
                % TODO: g2353001
                % when local DSMs are available (g2352987) this needs
                % to be reviewed to make sure their TimingInterfaces are
                % updated. Note that those DSMs will be available as
                % DataInterfaces and not as ReadWriteDataInterfaces so more
                % processing may be needed
            end
        end

        function updateConstInport(this)
            % For read-only subsystem inports defined in the parent code, 
            % update them so that SIL/PIL defines its own non-read only 
            % storage and uses that during SIL/PIL unit testing.

            numInportsSeq = this.SubsystemInterface.Inports;    
            % Number of total Inputs to the harness
            numInports = numInportsSeq.Size;                   
            % Looping ports to check if Read-Only
            for k = 1:numInports
                % Check type of Input port
                if isempty(this.SubsystemInterface.Inports(k).Implementation)
                    % skip empty implementations
                    continue;
                end
                portType = this.SubsystemInterface.Inports(k).Implementation.Type;
                % get Base Type of Input port
                portBaseType = rtw.connectivity.CodeInfoTypeUtils.getBaseType(portType);
                % Check if the Input port is read only, if True change
                % ReadOnly to False
                portIsReadOnly = rtw.connectivity.CodeInfoTypeUtils.isReadOnly(portBaseType);                
                if portIsReadOnly
                    this.SubsystemInterface.Inports(k).Implementation.Identifier = ['XIL_UpdateConst_',num2str(k)];
                    this.SubsystemInterface.Inports(k).Implementation.VarOwner = '';
                    this.SubsystemInterface.Inports(k).Implementation.Type.BaseType.ReadOnly = 0;
                end                
            end
        end

        function replaceInportsTiming(this, inportsInfo, mf0Model, componentForDiagnostics)
            % If ParameterService is ON, add PARAMETERCHANGE to
            % timingToReplace
            if slfeature('ParameterService') == 1
                timingToReplace(4) = {'PARAMETERCHANGE'};
            end

            timingProperties = this.SubsystemInterface.TimingProperties;

            rtw.connectivity.ISubsysAdaptor.doReplaceInportsTiming( ...
                timingProperties, ...
                this.Inports, ...
                inportsInfo, ...
                componentForDiagnostics, ...
                @missingRateStrategy);

            function newRate = missingRateStrategy(rateInfo)
                % First look for a periodic rate that matches the one in the temporary model
                allPeriodicRates = rtw.connectivity.CodeInfoUtils.findTimingRates( ...
                    timingProperties.toArray(), 'PERIODIC');
                % Loop over all periodic rates
                for rateIdx = 1:length(allPeriodicRates)
                    currRate = allPeriodicRates(rateIdx);
                    % Replace with a matching periodic rate
                    if rateInfo.period == currRate.SamplePeriod
                        newRate = currRate; 
                        return;
                    end
                end

                % If it cannot be found use an inherited rate.
                stopAtFirst = true;
                newRate = rtw.connectivity.CodeInfoUtils.findTimingRates( ...
                    timingProperties.toArray(), 'INHERITED', stopAtFirst);
                if isempty(newRate)
                    newRate = coder.descriptor.TimingInterface(mf0Model);
                    newRate.TimingMode = 'INHERITED';
                    timingProperties.add(newRate);
                end
            end
        end
        
        function componentInterface = getComponentInterface(this)
            % return the ComponentInterface for the parent model
            componentInterface = this.CodeDescObj;
        end
        
        function addEmptyOutputFunction(this, mf0Model)
            % Add an empty output function to the adaptor to make sure
            % we have a task to transmit associated data
            
            % Use the timing of update functions.
            updateFunctions = this.SubsystemInterface.UpdateFunctions.toArray();
            if ~isempty(updateFunctions)
                timing = coder.descriptor.TimingInterface.empty();
                for idx = 1:length(updateFunctions)
                    timing(idx) = updateFunctions(idx).Timing;
                end
            else
                % There is no update function, so this subsystem only
                % has an initializeFunction and no periodic rates.
                % This can happen with subsystems containing a constant
                % block with inf sample time.
                % The same subsystem configured with ASYNC sample time
                % has an output function.
                timingProperties = this.SubsystemInterface.TimingProperties.toArray();
                % The fact that there's no output nor update function
                % means that there should be no PERIODIC/INHERITED sample 
                % time, look anyway
                stopAtFirst = true;
                timing = rtw.connectivity.CodeInfoUtils.findTimingRates(timingProperties, 'PERIODIC', stopAtFirst);
                if isempty(timing)
                    timing = rtw.connectivity.CodeInfoUtils.findTimingRates(timingProperties, 'INHERITED', stopAtFirst);
                end
                
                if isempty(timing)
                    % try to get a periodic or inherited timing from the parent
                    timingProperties = this.getComponentInterface().TimingProperties.toArray();
                    timing = rtw.connectivity.CodeInfoUtils.findTimingRates(timingProperties, 'PERIODIC', stopAtFirst);
                    if isempty(timing)
                        timing = rtw.connectivity.CodeInfoUtils.findTimingRates(timingProperties, 'INHERITED', stopAtFirst);
                    end
                    
                    if isempty(timing)
                        % I was not able to test this scenario, because
                        % ASYNC subsystems do have an output function.
                        % If no timing was found create a dummy INHERITED 
                        % interface as last resort, this will fail with a
                        % meaningful error when writing the Sfunction 
                        % with 'PIL:pil:UnsupportedInheritedTsWithSampleTimeIndependentSystem' 
                        timing = coder.descriptor.TimingInterface(mf0Model);
                        timing.TimingMode = 'INHERITED';
                    end
                    
                    % add the new timing to the timing properties as
                    % first element, logic as in
                    % rtw.connectivity.CodeInfoSfunction/initializeAllSampleTimesOnPorts
                    % expects the periodic timing to be first
                    this.SubsystemInterface.TimingProperties.insertAt(timing, 1);
                end
            end
            
            this.doAddEmptyOutputFunction(mf0Model, timing);
        end
        
        function addEmptyUpdateFunction(this, mf0Model)
            % Update functions can be suppressed if the subsystem doesn't
            % have states at certain rates. 
            % Add empty update functions to the adaptor to make sure we 
            % have  a matching number of output/update functions.
            
            outputFunctions = this.SubsystemInterface.OutputFunctions.toArray();
            existingUpdateFunctions = this.SubsystemInterface.UpdateFunctions.toArray();           
            periodicOutputFunctions = coder.descriptor.FunctionInterface.empty(); 
            for idx = 1:length(outputFunctions)
                timing = outputFunctions(idx).Timing;
                % TODO: when supporting Simulink functions we may have to
                % remove from this list SLFunctions that cannot be invoked 
                % from outside the application, like we do in
                % CodeInfoUtils.getTasks
                if strcmp(timing.TimingMode,'PERIODIC')
                    periodicOutputFunctions(end+1) = outputFunctions(idx); %#ok<AGROW>
                end
            end
            
            % If there are more periodic output functions than update
            % functions (if we have periodic output fcns we expect all 
            % update fcns to also be periodic)
            if length(periodicOutputFunctions) > length(existingUpdateFunctions)
                existingUpdateFunctionTimings = [existingUpdateFunctions.Timing];
                updateFunctionAdaptors = repmat(rtw.connectivity.FIAdaptor.empty(), ...
                    size(periodicOutputFunctions));

                for idx = 1:length(periodicOutputFunctions)
                    timing = periodicOutputFunctions(idx).Timing;
                    % If the current output function has a matching
                    % update function
                    matchingTimingIndex = find(arrayfun(@(x) timing == x, existingUpdateFunctionTimings));
                    assert(length(matchingTimingIndex) <= 1, 'Should not have more than one matching timing') ;
                    if isempty(matchingTimingIndex)
                        % Create a dummy function interface if there's no
                        % match
                        updateFunction = this.createEmptyFunctionInterface(mf0Model, timing);
                    else
                        % Use the matching update function otherwise
                        updateFunction = existingUpdateFunctions(matchingTimingIndex);
                    end
                    updateFunctionAdaptors(idx) = rtw.connectivity.FIAdaptor(updateFunction);
                end
                % Replace the UpdateFunctions adaptor array with the new one
                this.UpdateFunctions = updateFunctionAdaptors;
            end
        end
    end
    
    methods(Static, Access = private)

        function interface = createEmptyFunctionInterface(mf0Model, timing)
            % Create an empty FunctionInterface and populate its timing 
            
            interface = coder.descriptor.FunctionInterface(mf0Model);
            interface.Timing = timing;
            % add empty prototype so that TargetInterface.writeSectionIncludes
            % can process this function correctly
            prototype = coder.descriptor.types.Prototype(mf0Model);
            interface.Prototype = prototype;
            % tag the interface as an inactive variant, this prevents
            % TargetInterface.writeSectionOutputBody from processing
            % get/set, IO assignment and emitting a ';' to terminate the
            % empty function-call command
            interface.VariantInfo = coder.descriptor.VariantInfo(mf0Model);
            interface.VariantInfo.VariantActive = false;
        end

        function doReplaceIOPortImpls(optimizedImplementations, originalPorts)
            % Replace port implementation with implementation from
            % optimized map
            for key = optimizedImplementations.keys
                port = optimizedImplementations.getByKey(key);
                assert(port.OptimizedImpl.Size == uint64(1), ...
                    'Multiple optimized implementation are not expected.')
                portImpl = port.OptimizedImpl.toArray();
                % PortIdx is zero-based, originalPorts is  one-based
                originalPorts(port.PortIdx + 1).Implementation = portImpl;
            end
    end
    end
       
    methods(Access = private)
        function doAddEmptyOutputFunction(this, mf0Model, timing)
            % Add an empty output function for each timing interface
            for idx = 1:length(timing)
                outputFunction = this.createEmptyFunctionInterface(mf0Model, timing(idx));
                % add the new interface to the OutputFunctions
                this.OutputFunctions(end+1) = rtw.connectivity.FIAdaptor(outputFunction);
            end
        end
        
        function sortTimingInterfaces(this)
            % Sort the subsystem timing interfaces to match the order in 
            % the component interface
            
            compInt = this.CodeDescObj;
            replaceIdx = 1;
            for compIdx = 1:compInt.TimingProperties.Size
                currT = compInt.TimingProperties(compIdx);
                for ssIdx = replaceIdx:this.SubsystemInterface.TimingProperties.Size
                     % if the current timing interface is part of the
                     % subsystem timing interfaces 
                     if currT == this.SubsystemInterface.TimingProperties(ssIdx)
                         % move the timing at replaceIdx to the current
                         % ssIdx position
                         this.SubsystemInterface.TimingProperties(ssIdx) = ...
                             this.SubsystemInterface.TimingProperties(replaceIdx);
                         % replace the timing at replaceIdx with the
                         % current timing
                         this.SubsystemInterface.TimingProperties(replaceIdx) = currT;
                         replaceIdx = replaceIdx + 1;
                         break;
                     end
                end
            end
        end

        function replaceOptimizedIOPortImplementations(this)
            % Optimized IO ports have empty implementations which trigger
            % Connectivity:target:CodeInfoInportUnsupportedImplementation
            % Connectivity:target:CodeInfoOutportUnsupportedImplementation
            % errors. We can replace the empty implementations with the
            % ones captured in the OptimizedInportImplementations and 
            % OptimizedOutportImplementations maps

            rtw.connectivity.SIAdaptor.doReplaceIOPortImpls( ...
                this.SubsystemInterface.OptimizedInportImplementations, ...
                this.SubsystemInterface.Inports);

            rtw.connectivity.SIAdaptor.doReplaceIOPortImpls( ...
                this.SubsystemInterface.OptimizedOutportImplementations, ...
                this.SubsystemInterface.Outports);
        end
    end
end
