/*
 * Copyright (C) 2013 Google 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:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
 * OWNER OR 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.
 */

/**
 * @constructor
 * @extends {WebInspector.VBox}
 * @param {string} prefix
 */
WebInspector.TimelineOverviewPane = function(prefix)
{
    WebInspector.VBox.call(this);
    this.element.id = prefix + "-overview-pane";

    this._overviewCalculator = new WebInspector.TimelineOverviewCalculator();
    this._overviewGrid = new WebInspector.OverviewGrid(prefix);
    this.element.appendChild(this._overviewGrid.element);
    this._cursorArea = this._overviewGrid.element.createChild("div", "overview-grid-cursor-area");
    this._cursorElement = this._overviewGrid.element.createChild("div", "overview-grid-cursor-position");
    this._cursorArea.addEventListener("mousemove", this._onMouseMove.bind(this), true);
    this._cursorArea.addEventListener("mouseleave", this._hideCursor.bind(this), true);

    this._overviewGrid.setResizeEnabled(false);
    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.Click, this._onClick, this);
    this._overviewControls = [];
    this._markers = new Map();

    this._popoverHelper = new WebInspector.PopoverHelper(this._cursorArea, this._getPopoverAnchor.bind(this), this._showPopover.bind(this), this._onHidePopover.bind(this));
    this._popoverHelper.setTimeout(0);

    this._updateThrottler = new WebInspector.Throttler(100);

    this._cursorEnabled = false;
    this._lastWidth = 0;
}

WebInspector.TimelineOverviewPane.Events = {
    WindowChanged: "WindowChanged"
};

WebInspector.TimelineOverviewPane.prototype = {
    /**
     * @param {!Element} element
     * @param {!Event} event
     * @return {!Element|!AnchorBox|undefined}
     */
    _getPopoverAnchor: function(element, event)
    {
        return this._cursorArea;
    },

    /**
     * @param {!Element} anchor
     * @param {!WebInspector.Popover} popover
     */
    _showPopover: function(anchor, popover)
    {
        this._popover = popover;
        this._buildPopoverContents().then(maybeShowPopover.bind(this));
        /**
         * @this {WebInspector.TimelineOverviewPane}
         * @param {!DocumentFragment} fragment
         */
        function maybeShowPopover(fragment)
        {
            if (!fragment.firstChild)
                return;
            var content = new WebInspector.TimelineOverviewPane.PopoverContents();
            this._popoverContents = content.contentElement.createChild("div");
            this._popoverContents.appendChild(fragment);
            popover.showView(content, this._cursorElement);
        }
    },

    _onHidePopover: function()
    {
        this._popover = null;
        this._popoverContents = null;
    },

    /**
     * @param {!Event} event
     */
    _onMouseMove: function(event)
    {
        if (!this._cursorEnabled)
            return;
        var x = event.offsetX + event.target.offsetLeft;
        this._cursorElement.style.left = x + "px";
        this._cursorElement.style.visibility = "visible";
        if (!this._popover)
            return;
        this._buildPopoverContents().then(updatePopover.bind(this));
        this._popover.positionElement(this._cursorElement);

        /**
         * @param {!DocumentFragment} fragment
         * @this {WebInspector.TimelineOverviewPane}
         */
        function updatePopover(fragment)
        {
            if (!this._popoverContents)
                return;
            this._popoverContents.removeChildren();
            this._popoverContents.appendChild(fragment);
        }
    },

    /**
     * @return {!Promise<!DocumentFragment>}
     */
    _buildPopoverContents: function()
    {
        var cursor = this._cursorElement;
        var x = cursor.offsetLeft;
        var promises = [];
        for (var control of this._overviewControls)
            promises.push(control.popoverElementPromise(x));

        return Promise.all(promises).then(buildFragment);

        /**
         * @param {!Array<?Element>} elements
         * @return {!DocumentFragment}
         */
        function buildFragment(elements)
        {
            var fragment = cursor.ownerDocument.createDocumentFragment();
            for (var element of elements) {
                if (element)
                    fragment.appendChild(element);
            }
            return fragment;
        }
    },

    _hideCursor: function()
    {
        this._cursorElement.style.visibility = "hidden";
    },

    /**
     * @override
     */
    wasShown: function()
    {
        this._update();
    },

    /**
     * @override
     */
    onResize: function()
    {
        var width = this.element.offsetWidth;
        if (width === this._lastWidth)
            return;
        this._lastWidth = width;
        this.scheduleUpdate();
    },

    /**
     * @param {!Array.<!WebInspector.TimelineOverview>} overviewControls
     */
    setOverviewControls: function(overviewControls)
    {
        for (var i = 0; i < this._overviewControls.length; ++i)
            this._overviewControls[i].dispose();

        for (var i = 0; i < overviewControls.length; ++i) {
            overviewControls[i].setCalculator(this._overviewCalculator);
            overviewControls[i].show(this._overviewGrid.element);
        }
        this._overviewControls = overviewControls;
        this._update();
    },

    /**
     * @param {number} minimumBoundary
     * @param {number} maximumBoundary
     */
    setBounds: function(minimumBoundary, maximumBoundary)
    {
        this._overviewCalculator.setBounds(minimumBoundary, maximumBoundary);
        this._overviewGrid.setResizeEnabled(true);
        this._cursorEnabled = true;
    },

    scheduleUpdate: function()
    {
        this._updateThrottler.schedule(process.bind(this));
        /**
         * @this {WebInspector.TimelineOverviewPane}
         * @param {!WebInspector.Throttler.FinishCallback} callback
         */
        function process(callback)
        {
            this._update();
            callback();
        }
    },

    _update: function()
    {
        if (!this.isShowing())
            return;
        this._overviewCalculator.setDisplayWindow(this._overviewGrid.clientWidth());
        for (var i = 0; i < this._overviewControls.length; ++i)
            this._overviewControls[i].update();
        this._overviewGrid.updateDividers(this._overviewCalculator);
        this._updateMarkers();
        this._updateWindow();
    },

    /**
     * @param {!Map<number, !Element>} markers
     */
    setMarkers: function(markers)
    {
        this._markers = markers;
        this._updateMarkers();
    },

    _updateMarkers: function()
    {
        var filteredMarkers = new Map();
        for (var time of this._markers.keys()) {
            var marker = this._markers.get(time);
            var position = Math.round(this._overviewCalculator.computePosition(time));
            // Limit the number of markers to one per pixel.
            if (filteredMarkers.has(position))
                continue;
            filteredMarkers.set(position, marker);
            marker.style.left = position + "px";
        }
        this._overviewGrid.removeEventDividers();
        this._overviewGrid.addEventDividers(filteredMarkers.valuesArray());
    },

    reset: function()
    {
        this._overviewCalculator.reset();
        this._overviewGrid.reset();
        this._overviewGrid.setResizeEnabled(false);
        this._overviewGrid.updateDividers(this._overviewCalculator);
        this._cursorEnabled = false;
        this._hideCursor();
        this._markers = new Map();
        for (var i = 0; i < this._overviewControls.length; ++i)
            this._overviewControls[i].reset();
        this._popoverHelper.hidePopover();
        this._update();
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _onClick: function(event)
    {
        var domEvent = /** @type {!Event} */ (event.data);
        for (var overviewControl of this._overviewControls) {
            if (overviewControl.onClick(domEvent)) {
                event.preventDefault();
                return;
            }
        }
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _onWindowChanged: function(event)
    {
        if (this._muteOnWindowChanged)
            return;
        // Always use first control as a time converter.
        if (!this._overviewControls.length)
            return;
        var windowTimes = this._overviewControls[0].windowTimes(this._overviewGrid.windowLeft(), this._overviewGrid.windowRight());
        this._windowStartTime = windowTimes.startTime;
        this._windowEndTime = windowTimes.endTime;
        this.dispatchEventToListeners(WebInspector.TimelineOverviewPane.Events.WindowChanged, windowTimes);
    },

    /**
     * @param {number} startTime
     * @param {number} endTime
     */
    requestWindowTimes: function(startTime, endTime)
    {
        if (startTime === this._windowStartTime && endTime === this._windowEndTime)
            return;
        this._windowStartTime = startTime;
        this._windowEndTime = endTime;
        this._updateWindow();
        this.dispatchEventToListeners(WebInspector.TimelineOverviewPane.Events.WindowChanged, { startTime: startTime, endTime: endTime });
    },

    _updateWindow: function()
    {
        if (!this._overviewControls.length)
            return;
        var windowBoundaries = this._overviewControls[0].windowBoundaries(this._windowStartTime, this._windowEndTime);
        this._muteOnWindowChanged = true;
        this._overviewGrid.setWindow(windowBoundaries.left, windowBoundaries.right);
        this._muteOnWindowChanged = false;
    },

    __proto__: WebInspector.VBox.prototype
}

/**
 * @constructor
 * @extends {WebInspector.VBox}
 */
WebInspector.TimelineOverviewPane.PopoverContents = function()
{
    WebInspector.VBox.call(this, true);
    this.contentElement.classList.add("timeline-overview-popover");
}

WebInspector.TimelineOverviewPane.PopoverContents.prototype = {
    __proto__: WebInspector.VBox.prototype
}

/**
 * @constructor
 * @implements {WebInspector.TimelineGrid.Calculator}
 */
WebInspector.TimelineOverviewCalculator = function()
{
    this.reset();
}

WebInspector.TimelineOverviewCalculator.prototype = {
    /**
     * @override
     * @return {number}
     */
    paddingLeft: function()
    {
        return this._paddingLeft;
    },

    /**
     * @override
     * @param {number} time
     * @return {number}
     */
    computePosition: function(time)
    {
        return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft;
    },

    /**
     * @param {number} position
     * @return {number}
     */
    positionToTime: function(position)
    {
        return (position - this._paddingLeft) / this._workingArea * this.boundarySpan() + this._minimumBoundary;
    },

    /**
     * @param {number} minimumBoundary
     * @param {number} maximumBoundary
     */
    setBounds: function(minimumBoundary, maximumBoundary)
    {
        this._minimumBoundary = minimumBoundary;
        this._maximumBoundary = maximumBoundary;
    },

    /**
     * @param {number} clientWidth
     * @param {number=} paddingLeft
     */
    setDisplayWindow: function(clientWidth, paddingLeft)
    {
        this._paddingLeft = paddingLeft || 0;
        this._workingArea = clientWidth - this._paddingLeft;
    },

    reset: function()
    {
        this.setBounds(0, 1000);
    },

    /**
     * @override
     * @param {number} value
     * @param {number=} precision
     * @return {string}
     */
    formatTime: function(value, precision)
    {
        return Number.preciseMillisToString(value - this.zeroTime(), precision);
    },

    /**
     * @override
     * @return {number}
     */
    maximumBoundary: function()
    {
        return this._maximumBoundary;
    },

    /**
     * @override
     * @return {number}
     */
    minimumBoundary: function()
    {
        return this._minimumBoundary;
    },

    /**
     * @override
     * @return {number}
     */
    zeroTime: function()
    {
        return this._minimumBoundary;
    },

    /**
     * @override
     * @return {number}
     */
    boundarySpan: function()
    {
        return this._maximumBoundary - this._minimumBoundary;
    }
}

/**
 * @interface
 */
WebInspector.TimelineOverview = function()
{
}

WebInspector.TimelineOverview.prototype = {
    /**
     * @param {?Element} parentElement
     * @param {!Element=} insertBefore
     */
    show: function(parentElement, insertBefore) { },

    update: function() { },

    dispose: function() { },

    reset: function() { },

    /**
     * @param {number} x
     * @return {!Promise<?Element>}
     */
    popoverElementPromise: function(x) { },

    /**
     * @param {!Event} event
     * @return {boolean}
     */
    onClick: function(event) { },

    /**
     * @param {number} windowLeft
     * @param {number} windowRight
     * @return {!{startTime: number, endTime: number}}
     */
    windowTimes: function(windowLeft, windowRight) { },

    /**
     * @param {number} startTime
     * @param {number} endTime
     * @return {!{left: number, right: number}}
     */
    windowBoundaries: function(startTime, endTime) { },

    timelineStarted: function() { },

    timelineStopped: function() { },
}

/**
 * @constructor
 * @extends {WebInspector.VBox}
 * @implements {WebInspector.TimelineOverview}
 */
WebInspector.TimelineOverviewBase = function()
{
    WebInspector.VBox.call(this);
    /** @type {?WebInspector.TimelineOverviewCalculator} */
    this._calculator = null;
    this._canvas = this.element.createChild("canvas", "fill");
    this._context = this._canvas.getContext("2d");
}

WebInspector.TimelineOverviewBase.prototype = {
    /**
     * @override
     */
    update: function()
    {
        this.resetCanvas();
    },

    /**
     * @override
     */
    dispose: function()
    {
        this.detach();
    },

    /**
     * @override
     */
    reset: function()
    {
    },

    /**
     * @override
     * @param {number} x
     * @return {!Promise<?Element>}
     */
    popoverElementPromise: function(x)
    {
        return Promise.resolve(/** @type {?Element} */ (null));
    },

    /**
     * @override
     */
    timelineStarted: function()
    {
    },

    /**
     * @override
     */
    timelineStopped: function()
    {
    },

    /**
     * @param {!WebInspector.TimelineOverviewCalculator} calculator
     */
    setCalculator: function(calculator)
    {
        this._calculator = calculator;
    },

    /**
     * @override
     * @param {!Event} event
     * @return {boolean}
     */
    onClick: function(event)
    {
        return false;
    },

    /**
     * @override
     * @param {number} windowLeft
     * @param {number} windowRight
     * @return {!{startTime: number, endTime: number}}
     */
    windowTimes: function(windowLeft, windowRight)
    {
        var absoluteMin = this._calculator.minimumBoundary();
        var timeSpan = this._calculator.maximumBoundary() - absoluteMin;
        return {
            startTime: absoluteMin + timeSpan * windowLeft,
            endTime: absoluteMin + timeSpan * windowRight
        };
    },

    /**
     * @override
     * @param {number} startTime
     * @param {number} endTime
     * @return {!{left: number, right: number}}
     */
    windowBoundaries: function(startTime, endTime)
    {
        var absoluteMin = this._calculator.minimumBoundary();
        var timeSpan = this._calculator.maximumBoundary() - absoluteMin;
        var haveRecords = absoluteMin > 0;
        return {
            left: haveRecords && startTime ? Math.min((startTime - absoluteMin) / timeSpan, 1) : 0,
            right: haveRecords && endTime < Infinity ? (endTime - absoluteMin) / timeSpan : 1
        };
    },

    resetCanvas: function()
    {
        this._canvas.width = this.element.clientWidth * window.devicePixelRatio;
        this._canvas.height = this.element.clientHeight * window.devicePixelRatio;
    },

    __proto__: WebInspector.VBox.prototype
}
