/*
 * Copyright (C) 2015 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WebInspector.TimelineRecordFrame = class TimelineRecordFrame extends WebInspector.Object
{
    constructor(graphDataSource, record)
    {
        super();

        this._element = document.createElement("div");
        this._element.classList.add("timeline-record-frame");

        this._graphDataSource = graphDataSource;
        this._record = record || null;
        this._filtered = false;
    }

    // Public

    get element()
    {
        return this._element;
    }

    get record()
    {
        return this._record;
    }

    set record(record)
    {
        this._record = record;
    }

    get selected()
    {
        return this._element.classList.contains("selected");
    }

    set selected(x)
    {
        if (this.selected === x)
            return;

        this._element.classList.toggle("selected");
    }

    get filtered()
    {
        return this._filtered;
    }

    set filtered(x)
    {
        if (this._filtered === x)
            return;

        this._filtered = x;
        this._element.classList.toggle("filtered");
    }

    refresh(graphDataSource)
    {
        if (!this._record)
            return false;

        var frameIndex = this._record.frameIndex;
        var graphStartFrameIndex = Math.floor(graphDataSource.startTime);
        var graphEndFrameIndex = graphDataSource.endTime;

        // If this frame is completely before or after the bounds of the graph, return early.
        if (frameIndex < graphStartFrameIndex || frameIndex > graphEndFrameIndex)
            return false;

        this._element.style.width = (1 / graphDataSource.timelineOverview.secondsPerPixel) + "px";

        var graphDuration = graphDataSource.endTime - graphDataSource.startTime
        var recordLeftPosition = (frameIndex - graphDataSource.startTime) / graphDuration;
        this._updateElementPosition(this._element, recordLeftPosition, "left");
        this._updateChildElements(graphDataSource);

        return true;
    }

    // Private

    _calculateFrameDisplayData(graphDataSource)
    {
        var secondsPerBlock = (graphDataSource.graphHeightSeconds / graphDataSource.height) * WebInspector.TimelineRecordFrame.MinimumHeightPixels;
        var segments = [];
        var invisibleSegments = [];
        var currentSegment = null;

        function updateDurationRemainder(segment)
        {
            if (segment.duration <= secondsPerBlock) {
                segment.remainder = 0;
                return;
            }

            var roundedDuration = Math.roundTo(segment.duration, secondsPerBlock);
            segment.remainder = Math.max(segment.duration - roundedDuration, 0);
        }

        function pushCurrentSegment()
        {
            updateDurationRemainder(currentSegment);
            segments.push(currentSegment);
            if (currentSegment.duration < secondsPerBlock)
                invisibleSegments.push({segment: currentSegment, index: segments.length - 1});

            currentSegment = null;
        }

        // Frame segments aren't shown at arbitrary pixel heights, but are divided into blocks of pixels. One block
        // represents the minimum displayable duration of a rendering frame, in seconds. Contiguous tasks less than a
        // block high are grouped until the minimum is met, or a task meeting the minimum is found. The group is then
        // added to the list of segment candidates. Large tasks (one block or more) are not grouped with other tasks
        // and are simply added to the candidate list.
        for (var key in WebInspector.RenderingFrameTimelineRecord.TaskType) {
            var taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
            var duration = this._record.durationForTask(taskType);
            if (duration === 0)
                continue;

            if (currentSegment && duration >= secondsPerBlock)
                pushCurrentSegment();

            if (!currentSegment)
                currentSegment = {taskType: null, longestTaskDuration: 0, duration: 0, remainder: 0};

            currentSegment.duration += duration;
            if (duration > currentSegment.longestTaskDuration) {
                currentSegment.taskType = taskType;
                currentSegment.longestTaskDuration = duration;
            }

            if (currentSegment.duration >= secondsPerBlock)
                pushCurrentSegment();
        }

        if (currentSegment)
            pushCurrentSegment();

        // A frame consisting of a single segment is always visible.
        if (segments.length === 1) {
            segments[0].duration = Math.max(segments[0].duration, secondsPerBlock);
            invisibleSegments = [];
        }

        // After grouping sub-block tasks, a second pass is needed to handle those groups that are still beneath the
        // minimum displayable duration. Each sub-block task has one or two adjacent display segments greater than one
        // block. The rounded-off time from these tasks is added to the sub-block, if it's sufficient to create a full
        // block. Failing that, the task is merged with an adjacent segment.
        invisibleSegments.sort(function(a, b) { return a.segment.duration - b.segment.duration; });

        for (var item of invisibleSegments) {
            var segment = item.segment;
            var previousSegment = item.index > 0 ? segments[item.index - 1] : null;
            var nextSegment = item.index < segments.length - 1 ? segments[item.index + 1] : null;
            console.assert(previousSegment || nextSegment, "Invisible segment should have at least one adjacent visible segment.");

            // Try to increase the segment's size to exactly one block, by taking subblock time from neighboring segments.
            // If there are two neighbors, the one with greater subblock duration is borrowed from first.
            var adjacentSegments;
            var availableDuration;
            if (previousSegment && nextSegment) {
                adjacentSegments = previousSegment.remainder > nextSegment.remainder ? [previousSegment, nextSegment] : [nextSegment, previousSegment];
                availableDuration = previousSegment.remainder + nextSegment.remainder;
            } else {
                adjacentSegments = [previousSegment || nextSegment];
                availableDuration = adjacentSegments[0].remainder;
            }

            if (availableDuration < (secondsPerBlock - segment.duration)) {
                // Merge with largest adjacent segment.
                var targetSegment;
                if (previousSegment && nextSegment)
                    targetSegment = previousSegment.duration > nextSegment.duration ? previousSegment : nextSegment;
                else
                    targetSegment = previousSegment || nextSegment;

                targetSegment.duration += segment.duration;
                updateDurationRemainder(targetSegment);
                continue;
            }

            adjacentSegments.forEach(function(adjacentSegment) {
                if (segment.duration >= secondsPerBlock)
                    return;
                var remainder = Math.min(secondsPerBlock - segment.duration, adjacentSegment.remainder);
                segment.duration += remainder;
                adjacentSegment.remainder -= remainder;
            });
        }

        // Round visible segments to the nearest block, and compute the rounded frame duration.
        var frameDuration = 0;
        segments = segments.filter(function(segment) {
            if (segment.duration < secondsPerBlock)
                return false;
            segment.duration = Math.roundTo(segment.duration, secondsPerBlock);
            frameDuration += segment.duration;
            return true;
        });

        return {frameDuration, segments};
    }

    _updateChildElements(graphDataSource)
    {
        this._element.removeChildren();

        console.assert(this._record);
        if (!this._record)
            return;

        if (graphDataSource.graphHeightSeconds === 0)
            return;

        var frameElement = document.createElement("div");
        frameElement.classList.add("frame");
        this._element.appendChild(frameElement);

        // Display data must be recalculated when the overview graph's vertical axis changes.
        if (this._record.__displayData && this._record.__displayData.graphHeightSeconds !== graphDataSource.graphHeightSeconds)
            this._record.__displayData = null;

        if (!this._record.__displayData) {
            this._record.__displayData = this._calculateFrameDisplayData(graphDataSource);
            this._record.__displayData.graphHeightSeconds = graphDataSource.graphHeightSeconds;
        }

        var frameHeight = this._record.__displayData.frameDuration / graphDataSource.graphHeightSeconds;
        if (frameHeight >= 0.95)
            this._element.classList.add("tall");
        else
            this._element.classList.remove("tall");

        this._updateElementPosition(frameElement, frameHeight, "height");

        for (var segment of this._record.__displayData.segments) {
            var element = document.createElement("div");
            this._updateElementPosition(element, segment.duration / this._record.__displayData.frameDuration, "height");
            element.classList.add("duration", segment.taskType);
            frameElement.insertBefore(element, frameElement.firstChild);
        }
    }

    _updateElementPosition(element, newPosition, property)
    {
        newPosition *= 100;
        newPosition = newPosition.toFixed(2);

        var currentPosition = parseFloat(element.style[property]).toFixed(2);
        if (currentPosition !== newPosition)
            element.style[property] = newPosition + "%";
    }
};

WebInspector.TimelineRecordFrame.MinimumHeightPixels = 3;
WebInspector.TimelineRecordFrame.MaximumWidthPixels = 14;
WebInspector.TimelineRecordFrame.MinimumWidthPixels = 4;
