classdef ExecutionTimelineView < handle
    %EXECUTIONTIMELINEVIEW Contains the view logic (i.e. plotting) for the
    %execution time-line plot.

    %   Copyright 2014-2018 The MathWorks, Inc.

    properties
        AxesHandle;
        ViewModel;
        
        AxesHandlePosBar;
        StartTextBox;
        RangeTextBox;

        PatchColours;
        TaskOffsets;
        TaskMaxCallDepth;
    end

    properties
        FigureHandle;
        
        % vector saying if the entry for a Section has been already created
        % in the legend
        IsSectionInLegend;
    end

    methods
        function this = ExecutionTimelineView(viewModel)
            this.ViewModel = viewModel;

            this.initialise();
        end
    end

    methods (Access = private)
        function initialise(this)

            this.FigureHandle = figure;
            this.AxesHandle = axes('Units', 'normalized');
            set(this.AxesHandle, 'Tag', 'ExecutionTimeAxes');

            % Display figure Toolbar
            set(this.FigureHandle,'ToolBar','figure');
            this.FigureHandle.Name = 'Execution Profile';
            this.FigureHandle.NumberTitle = 'off';

            set(this.FigureHandle, 'UserData', this);

            % Make the left margin wider to accommodate task names
            extra_left_margin = 0.06;
            posDefault = get(this.AxesHandle,'Position');
            pos = posDefault;
            pos(1)=pos(1)+extra_left_margin;
            pos(3)=pos(3)-extra_left_margin;
            set(this.AxesHandle, 'Position', pos);

            lower_margin=0.06;
            pos(2)=pos(2)+lower_margin;
            pos(4)=pos(4)-lower_margin;
            set(this.AxesHandle,'Position',pos);

            ui_x = pos(1);
            ui_y = 0.03;

            ui_x2 = ui_x-0.02;
            uicontrol('Style', 'Text', ...
                'String', 'Start:',...
                'BackgroundColor',get(this.FigureHandle,'Color'),...
                'HorizontalAlignment','Right',...
                'Units','normalized','Position',[ui_x2, ui_y+0.005, 0.06, 0.04]);

            ui_x2 = ui_x2+0.07;
            this.StartTextBox = uicontrol('Style','edit','Min',1,'Max',1,...
                'HorizontalAlignment','Left',...
                'Callback', @(h, d) loc_ui_callback(this, h),...
                'Units','normalized','Position',[ui_x2, ui_y, 0.1, 0.05]);

            ui_x2 = ui_x2+0.12;
            uicontrol('Style', 'Text', ...
                'String', 'Range:',...
                'BackgroundColor',get(this.FigureHandle,'Color'),...
                'HorizontalAlignment','Right',...
                'Units','normalized','Position',[ui_x2, ui_y+0.005, 0.08, 0.04]);

            ui_x2 = ui_x2+0.09;
            this.RangeTextBox = uicontrol('Style','edit','Min',1,'Max',1,...
                'HorizontalAlignment','Right',...
                'Callback', @(h, d) loc_ui_callback(this, h),...
                'Units','normalized','Position',[ui_x2, ui_y, 0.1, 0.05]);


            ui_x3 = ui_x + 0.5300 +0.13;
            posBarColor = [0.7 0.7 0.7];
            this.AxesHandlePosBar = axes('Units','normalized',...
                'Position', [ui_x3, ui_y, 0.1 0.04],...
                'XTick',[], 'YTick',[],...
                'XColor', posBarColor,...
                'YColor', posBarColor,...
                'Color', posBarColor);

            % Make main axes the current axes
            set(this.FigureHandle, 'CurrentAxes', this.AxesHandle);

            this.TaskMaxCallDepth = [this.ViewModel.Tasks.MaxCallDepth];
            this.TaskOffsets = [0 cumsum(this.TaskMaxCallDepth(1:end-1) + 0.5)];

            rng('default');
            visibleSectionCount = length(this.ViewModel.VisibleSections);

            for ii2=1:visibleSectionCount
                this.PatchColours(ii2, :) = [
                    1
                    0.1+0.9*(ii2/visibleSectionCount)
                    0.1+0.9*mod(ii2+floor(visibleSectionCount/2),...
                    visibleSectionCount)/visibleSectionCount ...
                    ];
            end

            % Create the vector and initialize all elements as false - not
            % present in the legend. The vector size is equal to the total
            % number of sections.
            this.IsSectionInLegend = false(1, length(this.ViewModel.VisibleSections));
            
            lOperationType = 'Initialize';
            i_update_plot(this, ...
                this.ViewModel.XLimit(1), ...
                this.ViewModel.XLimit(2),...
                lOperationType);

            taskMarkerYPosition = this.TaskOffsets(this.ViewModel.TaskToHighlightIndex) + 1;

            % Display vertical markers and task highlighting
            if ~isempty(this.ViewModel.VerticalMarkers)
                addVerticalMarkers(this, this.ViewModel.VerticalMarkers, this.ViewModel.YLimit, taskMarkerYPosition,...
                    this.ViewModel.TaskEndXPosition);
            end

            % Enforce the specified x limits as axis may have been widened
            set(this.AxesHandle, 'XLim', this.ViewModel.XLimit);

            updateXLimits(this, this.AxesHandle);

            set(this.AxesHandle, 'YLim', this.ViewModel.YLimit);

            % Configure zoom callback and zoom motion
            hz = zoom(this.FigureHandle);
            setAxesZoomMotion(hz,this.AxesHandle,'horizontal');
            set(hz,'ActionPostCallback',@(h,evd) mypostcallback_zoom(this, h, evd));
            hz.UIContextMenu = uicontextmenu(this.FigureHandle); % Replace default context menu with empty
            set(hz,'Enable','on');

            % Configure pan callback and pan motion
            hp = pan(this.FigureHandle);
            setAxesPanMotion(hp,this.AxesHandle,'horizontal');
            set(hp,'ActionPostCallback',@(h,evd) mypostcallback_pan(this, h, evd));
            hp.UIContextMenu = uicontextmenu(this.FigureHandle); % Replace default context menu with empty
            set(this.FigureHandle,'Visible','on')

            % Add Title and labels to plots

            title(this.AxesHandle, 'Estimated Execution Time-line')

            if ~isempty(this.ViewModel.TicksPerSecond)
                xlabel(this.AxesHandle, 'Time in seconds')
            else
                xlabel(this.AxesHandle, 'Time in clock ticks')
            end

            % Configure the Y-axis

            % Calculate the position of the ticks
            this.AxesHandle.YTick = this.TaskOffsets + 1.5;
            this.AxesHandle.YTickLabel = {this.ViewModel.Tasks.Name};
            this.AxesHandle.TickLabelInterpreter = 'None';

            this.FigureHandle.HandleVisibility = 'off';
            plotedit(this.FigureHandle, 'setenabletools', 'off');
        end

        function plotTimeline(this)
            arrayfun(@(block) plotExecutionBlock(this, block), this.ViewModel.ExecutionBlocks);
        end

        function plotExecutionBlock(this, block)
            taskOffset = this.TaskOffsets(block.TaskIndex == [this.ViewModel.Tasks.Index]);

            hAxes = this.AxesHandle;

            for i = 1:length(block.Invocations)

                invocation = block.Invocations(i);
                timeStart = invocation.TimeStart;
                timeEnd = invocation.TimeEnd;

                patchX = [timeStart timeEnd timeEnd timeStart];
                yValUpper = block.CallDepth + 0.85 + taskOffset;
                yValLower = block.CallDepth + 0.15 + taskOffset;
                parentYValUpper = block.CallDepth - 0.15 + taskOffset;
                parentYValLower = block.CallDepth - 0.85 + taskOffset;

                % Blank out parent section while child is active
                if block.CallDepth > 1
                    patchY_blank = [parentYValLower parentYValLower ...
                        parentYValUpper parentYValUpper];
                    patchHandle = patch(hAxes, patchX, patchY_blank, [1 1 1], 'EdgeColor', 'w');
                    hasbehavior(patchHandle, 'legend', false);
                    lineHandle = line(hAxes, [timeStart timeStart], [parentYValLower parentYValUpper], ...
                        'LineStyle',':', 'Color','k');
                    hasbehavior(lineHandle, 'legend', false);
                    if (timeEnd ~= this.ViewModel.ExecutionTimeRange(2)) % filter case where data is truncated
                        lineHandle = line(hAxes, [timeEnd timeEnd], [parentYValLower parentYValUpper],  ...
                            'LineStyle',':', 'Color','k');
                        hasbehavior(lineHandle, 'legend', false);
                    end
                end

                if block.IsPreemption
                    assert(block.CallDepth > 1, 'Preemption can only occur if a task is active')
                    box_x = [timeStart timeStart timeEnd timeEnd timeStart];
                    box_y = [parentYValLower parentYValUpper parentYValUpper...
                        parentYValLower parentYValLower];
                    lineHandle = line(hAxes, box_x, box_y, 'Color','w', ...
                        'LineStyle','-');
                    hasbehavior(lineHandle, 'legend', false);
                    lineHandle = line(hAxes, box_x, box_y, 'Color','b', ...
                        'LineStyle',':');
                    hasbehavior(lineHandle, 'legend', false);
                    textStr = 'P';
                    text(hAxes, double(timeStart+timeEnd)/2,...
                        double(parentYValUpper+parentYValLower)/2, textStr,...
                        'HorizontalAlignment','center',...
                        'Clipping','on');
                    yValCoreNumText = double(parentYValLower);
                    xValCoreNumText = double(timeEnd);
                elseif block.IsSuspended
                    textStr = sprintf('%s%d', invocation.WarningIndicator, block.SectionIndex);
                    box_y = [parentYValLower parentYValUpper parentYValUpper parentYValLower parentYValLower];
                    box_x = [timeStart timeStart timeEnd timeEnd timeStart];
                    lineHandle = line(hAxes, box_x, box_y, 'Color','w', ...
                        'LineStyle','-');
                    hasbehavior(lineHandle, 'legend', false);
                    lineHandle = line(hAxes, box_x, box_y, 'Color','b', ...
                        'LineStyle',':');
                    hasbehavior(lineHandle, 'legend', false);
                    text(hAxes, double(timeStart+timeEnd)/2,...
                        double(parentYValUpper+parentYValLower)/2, 'S',...
                        'HorizontalAlignment','center',...
                        'Clipping','on');
                    lineHandle = line(hAxes, [timeStart timeStart],[parentYValLower parentYValUpper],'Color','k');
                    hasbehavior(lineHandle, 'legend', false);
                    yValCoreNumText = double(parentYValLower);
                    xValCoreNumText = double(timeEnd);
                    text(hAxes, double(timeStart),...
                        (double(parentYValUpper+parentYValLower)/2), textStr,...
                        'HorizontalAlignment','left',...
                        'BackgroundColor',[1 1 .6],...
                        'Clipping','on');
                else
                    textStr = sprintf('%s%d', invocation.WarningIndicator, block.SectionIndex);
                    patchColor = this.PatchColours(block.SectionIndex,:);
                    patchColor(1) = block.CallDepth/(max(this.TaskMaxCallDepth) + 1);
                    patchY = [yValLower yValLower yValUpper yValUpper];
                    n = strrep([int2str(block.SectionIndex) ': ' block.Name], '_', '\_');
                    patchHandle = patch(hAxes, patchX, patchY, patchColor, 'DisplayName', n);
                    % If section is not already in the legend, allow the
                    % automatic legend behavior for this patch. Otherwise,
                    % disable automatic legend behavior for this patch to
                    % stop it appearing multiple times.
                    if this.IsSectionInLegend(block.SectionIndex)
                        hasbehavior(patchHandle, 'legend', false);
                    else
                        this.IsSectionInLegend(block.SectionIndex) = true;
                    end
                    lineHandle = line(hAxes, [timeStart timeStart],[yValLower yValUpper],'Color','k');
                    hasbehavior(lineHandle, 'legend', false);
                    yValCoreNumText = double(yValLower);
                    xValCoreNumText = double(timeStart);
                    text(hAxes, double(timeStart),...
                        (double(yValUpper+yValLower)/2), textStr,...
                        'HorizontalAlignment','left',...
                        'BackgroundColor',[1 1 .6],...
                        'Clipping','on');
                end
                if  invocation.ShowCoreNumber
                    text(hAxes, xValCoreNumText, yValCoreNumText-0.01, num2str(invocation.Core),...
                        'VerticalAlignment','top', ...
                        'HorizontalAlignment', 'center',...
                        'Clipping','on');
                end
            end
        end

        function i_update_plot(this, xStart, xEnd, lOperationType)

            if ~isempty(this.ViewModel)

                % Set new X-limits
                this.ViewModel.XLimit = [xStart xEnd];

                % Notify view model that we need to update
                update(this.ViewModel, lOperationType);

                %plot using object
                plotTimeline(this);

                set(this.AxesHandle, 'XLim', this.ViewModel.XLimit);
            end
        end

        function updateXLimits(this, hax)

            posBarColor = get(this.AxesHandlePosBar,'Color');
            xLimOuter = [0 this.ViewModel.ExecutionTimeRange(2)];
            xlim = get(hax,'XLim');
            posBarXLim = [min(xlim(1), xLimOuter(1)), max(xlim(2), xLimOuter(2))];

            cla(this.AxesHandlePosBar)
            % Show the actual plotted range
            patch(this.AxesHandlePosBar, ...
                [xlim(1) xlim(2) xlim(2) xlim(1)], [0.1 0.1 0.9 0.9],posBarColor-0.1)
            set(this.AxesHandlePosBar, 'XLim', posBarXLim)

            % Show the  extent of available data
            if (xLimOuter(1) > xlim(1))
                line(this.AxesHandlePosBar, [xLimOuter(1) xLimOuter(1)], [0 1],...
                    'Color',posBarColor+0.1);
            end

            if (xLimOuter(2) < xlim(2))
                line(this.AxesHandlePosBar, [xLimOuter(2) xLimOuter(2)], [0 1],...
                    'Color',posBarColor+0.1);
            end
            set(hax.Parent, 'CurrentAxes', hax);

            set(this.StartTextBox, 'String', sprintf('%g',xlim(1)));
            set(this.RangeTextBox, 'String', sprintf('%g',diff(xlim)));

        end

        function mypostcallback_pan(varargin)

            lOperationType = 'Pan';
            mypostcallback(lOperationType, varargin{:});

        end

        function mypostcallback_zoom(varargin)

            lOperationType = 'Zoom';
            mypostcallback(lOperationType, varargin{:});

        end

        function mypostcallback(lOperationType, this, ~, ~)
            % After a zoom or pan operation we may need to re-render the graphic, e.g. if
            % the axis range has increased.

            newLim = get(this.AxesHandle,'XLim');
            i_update_plot(this, newLim(1), newLim(2), lOperationType);
            updateXLimits(this, this.AxesHandle);

            zoom_h = zoom(this.FigureHandle);
            direction = get(zoom_h, 'Direction');
            if strcmp(direction, 'out')
                i_increment_zoom_outer_limit(this);
            end
        end

        function i_increment_zoom_outer_limit(this)

            xlim = get(this.AxesHandle,'XLim');
            xlimSave = xlim;
            range=xlim(2)-xlim(1);
            xLimOuter = [0 this.ViewModel.ExecutionTimeRange(2)];

            % Don't allow zoom out to full range in one shot as it may
            % take a very long time to render
            xlimReset = [max(xlim(1)-0.5*range, xLimOuter(1)), ...
                min(xlim(2)+0.5*range, xLimOuter(2))];

            set(this.AxesHandle, 'XLim', xlimReset);
            zoom(this.FigureHandle, 'reset')
            set(this.AxesHandle, 'XLim', xlimSave);
        end

        function addVerticalMarkers(this, verticalMarkers, yLimFixed, ...
                taskMarkerYPosition,...
                taskEndXPosition)

            hAxes = this.AxesHandle;

            for i=1:length(verticalMarkers)
                xpos = verticalMarkers(i);
                line(hAxes, [xpos xpos], yLimFixed, 'LineStyle',':', 'Color', 'b');
            end

            % Identify the time when the task became due to start and the time when it
            % actually completed
            line(hAxes, [verticalMarkers(1) taskEndXPosition], [taskMarkerYPosition taskMarkerYPosition], ...
                'Marker', '^',...
                'LineWidth',2,...
                'LineStyle','-', ...
                'Color', 'b', ...
                'MarkerEdgeColor','b',...
                'MarkerFaceColor','b');

        end

        function loc_ui_callback(this, hObj)

            switch hObj
                case {this.StartTextBox, this.RangeTextBox}
                    newVal = str2double(get(hObj, 'String'));
                    xLimOld = get(this.AxesHandle, 'XLim');
                    xStart = xLimOld(1);
                    range = diff(xLimOld);
                    if ~isempty(newVal) && isfinite(newVal)
                        xLimOuter = [0 this.ViewModel.ExecutionTimeRange(2)];
                        if hObj == this.RangeTextBox
                            range = newVal;
                            range = min(range, diff(xLimOuter));
                            range = max(range, eps);
                        else
                            xStart = newVal;
                            xStart = max(xStart, xLimOuter(1));
                            xStart = min(xStart, xLimOuter(2));
                        end
                    end
                    xEnd = xStart+range;
                    lOperationType = 'Other';
                    i_update_plot(this, xStart, xEnd, lOperationType);
                    updateXLimits(this, this.AxesHandle);
            end
        end
    end
end
