classdef ExecutionTimeXILScheduleController < coder.profile.ExecutionTimeScheduleController
    %EXECUTIONTIMEXILSCHEDULECONTROLLER shows scheduling for XIL simulation

%   Copyright 2020 The MathWorks, Inc.
    
    properties (Constant=true, Access=private)
        % default value for thread signals
        SchedulingNonActiveValue = int32(0);
        % don't use spaces in the text, otherwise the enum throws a warning
        SchedulingNonActiveText = 'NotRunning';
        % time tolerance to identify two simultaneous events
        TimeTolerance = 1e-9;
    end
    
    properties (Access=private)
        % asyncqueues for threads and simulation time
        HQueueThreads;
        HQueueSimTime;
        
        % the following properties are defined here rather than locally to
        % avoid the need of passing them as arguments when internal
        % functions are called. They represent the internal state of the
        % scheduling
        
        % flag if there is at least a task running
        isRunning = false;
        % overall call stack (tasks + functions), containing the sectionIds
        stackOverall = [];
        % active task stack, containing the task indexes
        stackNestedTasks = [];
        % index of the active task. It is in [1, numTasks]. Use
        % idTasks(currentTaskIdx) to get the task sectionId
        currentTaskIdx = [];
        %
        IDTasks;
        NameIDs;
        % instead of using timerEndValues similarly to
        % timerStartValues, the active tasks/functions are stored in a
        % stack. Dealing with a stack is faster than finding the min
        % value in timerEndValues for each step (items aren't sorted in
        % this array)
        endValStack = [];
        endIdxStack = [];
    end
    
    methods
        function obj = ExecutionTimeXILScheduleController(modelName, timerData, tps)
            obj = obj@coder.profile.ExecutionTimeScheduleController(modelName, timerData, tps);
        end
    end
    
    methods(Access = protected)
        function  interMethods = getInterpolationMethod(this)
            % all the asyncqueus hold the previous value
            num = length(this.HQueueThreads) + length(this.HQueueSimTime);
            interMethods = repmat({'zoh'}, 1, num);
        end
        
        function [hQ, hC] = createAsyncQueues(this, names)
            % create data type for thread signals
            % remove spaces otherwise they will cause problems in the enum
            states = [{this.SchedulingNonActiveText} names(:)'];
            vals = [this.SchedulingNonActiveValue, int32(1:length(names))];
            dtThreads = Simulink.AsyncQueue.DataType.createEnum('ProfilingThreadResults', 'Simulink.IntEnumType', vals, states);
            % create a pair (client and asyncqueue) for each thread
            this.HQueueThreads = Simulink.AsyncQueue.Queue.empty(0, this.NumTasks);
            clientThreads = Simulink.AsyncQueue.SignalClient.empty(0, this.NumTasks);
            num = 0;
            for idTask = 1:this.NumSections
                if ~this.Tasks(idTask)
                    continue;
                end
                % create signal
                sigName = ['Task: ' this.TaskNames{idTask}];
                num = num + 1;
                [clientThreads(num), this.HQueueThreads(num)] = this.createAQueue(sigName, dtThreads, num);
            end
            
            dtSimTime = Simulink.AsyncQueue.DataType;
            sigName = 'Simulation time';
            num = num + 1;
            [clientSimTime, this.HQueueSimTime] = this.createAQueue(sigName, dtSimTime, num);
            
            hQ = [this.HQueueThreads, this.HQueueSimTime];
            hC = [clientThreads, clientSimTime];
        end
        
        function [startTime, stopTime] = plotScheduling(this, nameIds, simTimeStart, simTimeEnd)
            % get raw data
            [timerStartValues, timerEndValues, ~, ~, nodeNumbers, ~, lSimTimes] = this.TimerData.getTimerValues;
            
            % process input. If either simTimeStart or simTimeEnd is
            % provided (~=-1), data is limited based on the simulation time
            shrinkDataSet = simTimeStart > -1 || simTimeEnd > -1;
            limitPlot = false;
            if shrinkDataSet
                if simTimeStart < 0
                    simTimeStart = lSimTimes(1);
                end
                if simTimeEnd < 0
                    simTimeEnd = lSimTimes(end);
                end
                idxToRemove = lSimTimes < simTimeStart | lSimTimes > simTimeEnd;
                timerStartValues(idxToRemove) = [];
                timerEndValues(idxToRemove) = [];
                nodeNumbers(idxToRemove) = [];
                lSimTimes(idxToRemove) = [];
            else
                % if no range restriction is requested, throw a warning if
                % the number of samples is too high and limit the process
                if length(timerStartValues) > this.MaxNumPointsToProcess
                    limitPlot = true;
                    MSLDiagnostic('CoderProfile:ExecutionTime:ProfilingScheduleTooManyPoints').reportAsWarning;
                end
            end
            % check it isn't empty
            if isempty(timerStartValues)
                DAStudio.error('CoderProfile:ExecutionTime:ProfilingScheduleNoData');
            end

            % initialize all internal/state variables
            startTime = timerStartValues(1);
            this.IDTasks = find(this.Tasks);
            this.NameIDs = nameIds;
            latestSimTime = -1.0;
            pointCounter = 0;
            this.isRunning = false;
            this.stackOverall = [];
            this.stackNestedTasks = [];
            this.currentTaskIdx = [];
            idxStart = 1;
            nStart = length(timerStartValues);
            % instead of using timerEndValues similarly to
            % timerStartValues, the active tasks/functions are stored in a
            % stack. Dealing with a stack is faster than finding the min
            % value in timerEndValues for each step (items aren't sorted in
            % this array)
            this.endValStack = [];
            this.endIdxStack = [];
            stopTime = max(timerEndValues);

            % when the simulation starts, all tasks are not active
            lScaledStartTime = double(startTime) / this.TimeScaleFactor;
            for idx = 1:length(this.HQueueThreads)
                this.HQueueThreads(idx).write(lScaledStartTime, this.SchedulingNonActiveValue);
            end
            
            while idxStart <= nStart || ~isempty(this.endValStack)
                % process until there is a start value or the stack of
                % functions to terminate isn't empty
                
                % get next start event
                if idxStart <= nStart
                    valStart = timerStartValues(idxStart);
                    if idxStart < nStart && abs(double(valStart) - double(timerStartValues(idxStart+1))) < this.TimeTolerance
                        DAStudio.error('CoderProfile:ExecutionTime:ProfilingScheduleSameTime');
                    end
                else
                    valStart = Inf;
                end
                % get next end event
                if isempty(this.endValStack)
                    valEnd = Inf;
                else
                    [valEnd, idxPosEnd] = min(this.endValStack);
                    idxEnd = this.endIdxStack(idxPosEnd);
                    if sum((abs(this.endValStack - valEnd) < this.TimeTolerance)) > 1
                        DAStudio.error('CoderProfile:ExecutionTime:ProfilingScheduleSameTime');
                    end
                end
                % check which one must be processed first
                % don't use <=, give priority to ends
                if valStart < valEnd 
                    % start sample
                    itsValEnd = timerEndValues(idxStart);
                    nodeId = nodeNumbers(idxStart) - 1;
                    this.processStartEvent(valStart, itsValEnd, nodeId, idxStart);
                    realExecTime = valStart;
                    simTime = lSimTimes(idxStart);
                    idxStart = idxStart + 1;
                else
                    % end sample
                    nodeId = nodeNumbers(idxEnd) - 1;
                    this.processEndEvent(valEnd, nodeId, idxPosEnd);
                    realExecTime = valEnd;
                    simTime = lSimTimes(idxEnd);
                end

                % update simulation time if needed
                if simTime > latestSimTime
                    lScaledTime = double(realExecTime) / this.TimeScaleFactor;
                    this.HQueueSimTime.write(lScaledTime, simTime);
                    latestSimTime = simTime;
                end
                % check num point limit
                if limitPlot
                    pointCounter = pointCounter + 1;
                    if pointCounter > this.MaxNumPointsToProcess && isempty(this.endValStack)
                        break;
                    end
                end
            end
            
            % end streaming
            this.terminateAQs(realExecTime, latestSimTime);
        end
    end
    
    methods(Access=private)
        function processEndEvent(this, valEnd, nodeId, idxPosEnd)
            % process a scheduling event related to a task/function end
            assert(this.isRunning && ~isempty(this.currentTaskIdx), ...
                'A stop sample but nothing is in execution');
            sectionId = this.RealNodes(nodeId);
            taskIndex = find(sectionId == this.IDTasks);
            lScaledTime = double(valEnd) / this.TimeScaleFactor;
            if isempty(taskIndex)
                % a internal function ends
                assert(this.stackOverall(end) == sectionId, ...
                    'the ending function is not the current one');
                % remove function from call stack
                this.stackOverall(end) = [];
                % plot that the function terminated
                nameId = this.NameIDs(this.stackOverall(end));
                if nameId > 0
                    qThread = this.HQueueThreads(this.currentTaskIdx);
                    qThread.write(lScaledTime, int32(nameId));
                end
            else
                % a task just terminated
                assert(taskIndex == this.currentTaskIdx && ...
                    taskIndex == this.stackNestedTasks(end) && ...
                    this.stackOverall(end) == sectionId);
                % remove task from the call stacks
                this.stackOverall(end) = [];
                this.stackNestedTasks(end) = [];
                if isempty(this.stackNestedTasks)
                    % this was the last task active, the system isn't
                    % active now
                    this.isRunning = false;
                    this.currentTaskIdx = [];
                else
                    % put the first task in queue in execution
                    this.currentTaskIdx = this.stackNestedTasks(end);
                end
                % Stream that the task is inactive
                qThread = this.HQueueThreads(taskIndex);
                qThread.write(lScaledTime, this.SchedulingNonActiveValue);
            end
            % remove the entry from the end event queues
            this.endValStack(idxPosEnd) = [];
            this.endIdxStack(idxPosEnd) = [];
        end
        
        function processStartEvent(this, valStart, valEnd, nodeId, idxStart)
            % process a scheduling event related to a task/function start
            sectionId = this.RealNodes(nodeId);
            taskIndex = find(sectionId == this.IDTasks);
            if isempty(taskIndex)
                % calling a function - not a task
                assert(this.isRunning && ~isempty(this.currentTaskIdx), ...
                    'A function can start only within a task');
            else
                % a task starts
                assert(~this.isRunning || ~isempty(this.currentTaskIdx), ...
                    'currentTaskIdx is empty but there is something in execution');
                assert(this.isRunning || isempty(this.currentTaskIdx) && ...
                    isempty(this.stackNestedTasks), ...
                        'State variables aren''t where there is nothing in execution');
                % mark the task as active and update call stacks
                this.currentTaskIdx = taskIndex;
                this.stackNestedTasks(end+1) = this.currentTaskIdx;
                this.isRunning = true;
            end
            % add sectionId to the overall call stack
            this.stackOverall(end+1) = sectionId;
            % stream to SDI
            nameId = this.NameIDs(sectionId);
            if nameId > 0
                qThread = this.HQueueThreads(this.currentTaskIdx);
                lScaledTime = double(valStart) / this.TimeScaleFactor;
                qThread.write(lScaledTime, int32(nameId));
            end
            % add its end event in the queues
            this.endValStack(end+1) = valEnd;
            this.endIdxStack(end+1) = idxStart;
        end
        
        function terminateAQs(this, endTime, endSimTime)
            % extend non-activate value until the stop time for all tasks
            lScaledTime = double(endTime) / this.TimeScaleFactor;
            for idx = 1:length(this.HQueueThreads)
                this.HQueueThreads(idx).write(lScaledTime, this.SchedulingNonActiveValue);
            end
            % do the same for the simulation time signal
            this.HQueueSimTime.write(lScaledTime, endSimTime);
        end
        
    end
end
