/*
 * Copyright (C) 2012 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}
 * @implements {WebInspector.TimelineModeView}
 * @param {!WebInspector.TimelineModeViewDelegate} delegate
 * @param {!WebInspector.TimelineModel} model
 * @param {!Array<!WebInspector.TimelineModel.Filter>} filters
 */
WebInspector.CountersGraph = function(delegate, model, filters)
{
    WebInspector.VBox.call(this);

    this.element.id = "memory-graphs-container";

    this._delegate = delegate;
    this._model = model;
    this._filters = filters;
    this._calculator = new WebInspector.CounterGraphCalculator(this._model);

    // Create selectors
    this._infoWidget = new WebInspector.HBox();
    this._infoWidget.element.classList.add("memory-counter-selector-swatches", "timeline-toolbar-resizer");
    this._infoWidget.show(this.element);

    this._graphsContainer = new WebInspector.VBox();
    this._graphsContainer.show(this.element);
    var canvasWidget = new WebInspector.VBoxWithResizeCallback(this._resize.bind(this));
    canvasWidget.show(this._graphsContainer.element);
    this._createCurrentValuesBar();
    this._canvasContainer = canvasWidget.element;
    this._canvasContainer.id = "memory-graphs-canvas-container";
    this._canvas = this._canvasContainer.createChild("canvas");
    this._canvas.id = "memory-counters-graph";

    this._canvasContainer.addEventListener("mouseover", this._onMouseMove.bind(this), true);
    this._canvasContainer.addEventListener("mousemove", this._onMouseMove.bind(this), true);
    this._canvasContainer.addEventListener("mouseleave", this._onMouseLeave.bind(this), true);
    this._canvasContainer.addEventListener("click", this._onClick.bind(this), true);
    // We create extra timeline grid here to reuse its event dividers.
    this._timelineGrid = new WebInspector.TimelineGrid();
    this._canvasContainer.appendChild(this._timelineGrid.dividersElement);

    this._counters = [];
    this._counterUI = [];
}

WebInspector.CountersGraph.prototype = {
    /**
     * @return {?WebInspector.Target}
     */
    target: function()
    {
        return this._model.target();
    },

    _createCurrentValuesBar: function()
    {
        this._currentValuesBar = this._graphsContainer.element.createChild("div");
        this._currentValuesBar.id = "counter-values-bar";
    },

    /**
     * @param {string} uiName
     * @param {string} uiValueTemplate
     * @param {string} color
     * @param {function(number):string=} formatter
     * @return {!WebInspector.CountersGraph.Counter}
     */
    createCounter: function(uiName, uiValueTemplate, color, formatter)
    {
        var counter = new WebInspector.CountersGraph.Counter();
        this._counters.push(counter);
        this._counterUI.push(new WebInspector.CountersGraph.CounterUI(this, uiName, uiValueTemplate, color, counter, formatter));
        return counter;
    },

    /**
     * @override
     * @return {!WebInspector.Widget}
     */
    view: function()
    {
        return this;
    },

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

    /**
     * @override
     */
    reset: function()
    {
        for (var i = 0; i < this._counters.length; ++i) {
            this._counters[i].reset();
            this._counterUI[i].reset();
        }
        this.refresh();
    },

    /**
     * @override
     * @return {?Element}
     */
    resizerElement: function()
    {
        return this._infoWidget.element;
    },

    _resize: function()
    {
        var parentElement = this._canvas.parentElement;
        this._canvas.width = parentElement.clientWidth  * window.devicePixelRatio;
        this._canvas.height = parentElement.clientHeight * window.devicePixelRatio;
        var timelinePaddingLeft = 15;
        this._calculator.setDisplayWindow(this._canvas.width, timelinePaddingLeft);
        this.refresh();
    },

    /**
     * @override
     * @param {number} startTime
     * @param {number} endTime
     */
    setWindowTimes: function(startTime, endTime)
    {
        this._calculator.setWindow(startTime, endTime);
        this.scheduleRefresh();
    },

    scheduleRefresh: function()
    {
        WebInspector.invokeOnceAfterBatchUpdate(this, this.refresh);
    },

    draw: function()
    {
        for (var i = 0; i < this._counters.length; ++i) {
            this._counters[i]._calculateVisibleIndexes(this._calculator);
            this._counters[i]._calculateXValues(this._canvas.width);
        }
        this._clear();

        for (var i = 0; i < this._counterUI.length; i++)
            this._counterUI[i]._drawGraph(this._canvas);
    },

    /**
     * @param {!Event} event
     */
    _onClick: function(event)
    {
        var x = event.x - this._canvasContainer.totalOffsetLeft();
        var minDistance = Infinity;
        var bestTime;
        for (var i = 0; i < this._counterUI.length; ++i) {
            var counterUI = this._counterUI[i];
            if (!counterUI.counter.times.length)
                continue;
            var index = counterUI._recordIndexAt(x);
            var distance = Math.abs(x * window.devicePixelRatio - counterUI.counter.x[index]);
            if (distance < minDistance) {
                minDistance = distance;
                bestTime = counterUI.counter.times[index];
            }
        }
        if (bestTime !== undefined)
            this._delegate.selectEntryAtTime(bestTime);
    },

    /**
     * @param {!Event} event
     */
    _onMouseLeave: function(event)
    {
        delete this._markerXPosition;
        this._clearCurrentValueAndMarker();
    },

    _clearCurrentValueAndMarker: function()
    {
        for (var i = 0; i < this._counterUI.length; i++)
            this._counterUI[i]._clearCurrentValueAndMarker();
    },

    /**
     * @param {!Event} event
     */
    _onMouseMove: function(event)
    {
        var x = event.x - this._canvasContainer.totalOffsetLeft();
        this._markerXPosition = x;
        this._refreshCurrentValues();
    },

    _refreshCurrentValues: function()
    {
        if (this._markerXPosition === undefined)
            return;
        for (var i = 0; i < this._counterUI.length; ++i)
            this._counterUI[i].updateCurrentValue(this._markerXPosition);
    },

    refresh: function()
    {
        this._timelineGrid.updateDividers(this._calculator);
        this.draw();
        this._refreshCurrentValues();
    },

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

    _clear: function()
    {
        var ctx = this._canvas.getContext("2d");
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    },

    /**
     * @override
     * @param {?WebInspector.TracingModel.Event} event
     * @param {string=} regex
     * @param {boolean=} select
     */
    highlightSearchResult: function(event, regex, select)
    {
    },

    /**
     * @override
     * @param {?WebInspector.TracingModel.Event} event
     */
    highlightEvent: function(event)
    {
    },

    /**
     * @override
     * @param {?WebInspector.TimelineSelection} selection
     */
    setSelection: function(selection)
    {
    },

    __proto__: WebInspector.VBox.prototype
}

/**
 * @constructor
 */
WebInspector.CountersGraph.Counter = function()
{
    this.times = [];
    this.values = [];
}

WebInspector.CountersGraph.Counter.prototype = {
    /**
     * @param {number} time
     * @param {number} value
     */
    appendSample: function(time, value)
    {
        if (this.values.length && this.values.peekLast() === value)
            return;
        this.times.push(time);
        this.values.push(value);
    },

    reset: function()
    {
        this.times = [];
        this.values = [];
    },

    /**
     * @param {number} value
     */
    setLimit: function(value)
    {
        this._limitValue = value;
    },

    /**
     * @return {!{min: number, max: number}}
     */
    _calculateBounds: function()
    {
        var maxValue;
        var minValue;
        for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
            var value = this.values[i];
            if (minValue === undefined || value < minValue)
                minValue = value;
            if (maxValue === undefined || value > maxValue)
                maxValue = value;
        }
        minValue = minValue || 0;
        maxValue = maxValue || 1;
        if (this._limitValue) {
            if (maxValue > this._limitValue * 0.5)
                maxValue = Math.max(maxValue, this._limitValue);
            minValue = Math.min(minValue, this._limitValue);
        }
        return { min: minValue, max: maxValue };
    },

    /**
     * @param {!WebInspector.CounterGraphCalculator} calculator
     */
    _calculateVisibleIndexes: function(calculator)
    {
        var start = calculator.minimumBoundary();
        var end = calculator.maximumBoundary();

        // Maximum index of element whose time <= start.
        this._minimumIndex = Number.constrain(this.times.upperBound(start) - 1, 0, this.times.length - 1);

        // Minimum index of element whose time >= end.
        this._maximumIndex = Number.constrain(this.times.lowerBound(end), 0, this.times.length - 1);

        // Current window bounds.
        this._minTime = start;
        this._maxTime = end;
    },

    /**
     * @param {number} width
     */
    _calculateXValues: function(width)
    {
        if (!this.values.length)
            return;

        var xFactor = width / (this._maxTime - this._minTime);

        this.x = new Array(this.values.length);
        for (var i = this._minimumIndex + 1; i <= this._maximumIndex; i++)
            this.x[i] = xFactor * (this.times[i] - this._minTime);
    }
}

/**
 * @constructor
 * @param {!WebInspector.CountersGraph} memoryCountersPane
 * @param {string} title
 * @param {string} currentValueLabel
 * @param {string} graphColor
 * @param {!WebInspector.CountersGraph.Counter} counter
 * @param {(function(number): string)|undefined} formatter
 */
WebInspector.CountersGraph.CounterUI = function(memoryCountersPane, title, currentValueLabel, graphColor, counter, formatter)
{
    this._memoryCountersPane = memoryCountersPane;
    this.counter = counter;
    this._formatter = formatter || Number.withThousandsSeparator;
    var container = memoryCountersPane._infoWidget.element.createChild("div", "memory-counter-selector-info");

    this._setting = WebInspector.settings.createSetting("timelineCountersGraph-" + title, true);
    this._filter = new WebInspector.ToolbarCheckbox(title, title, this._setting);
    var color = WebInspector.Color.parse(graphColor).setAlpha(0.5).asString(WebInspector.Color.Format.RGBA);
    if (color) {
        this._filter.element.backgroundColor = color;
        this._filter.element.borderColor = "transparent";
    }
    this._filter.inputElement.addEventListener("click", this._toggleCounterGraph.bind(this));
    container.appendChild(this._filter.element);
    this._range = this._filter.element.createChild("span", "range");

    this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value");
    this._value.style.color = graphColor;
    this.graphColor = graphColor;
    this.limitColor = WebInspector.Color.parse(graphColor).setAlpha(0.3).asString(WebInspector.Color.Format.RGBA);
    this.graphYValues = [];
    this._verticalPadding = 10;

    this._currentValueLabel = currentValueLabel;
    this._marker = memoryCountersPane._canvasContainer.createChild("div", "memory-counter-marker");
    this._marker.style.backgroundColor = graphColor;
    this._clearCurrentValueAndMarker();
}

WebInspector.CountersGraph.CounterUI.prototype = {
    reset: function()
    {
        this._range.textContent = "";
    },

    /**
     * @param {number} minValue
     * @param {number} maxValue
     */
    setRange: function(minValue, maxValue)
    {
        var min = this._formatter(minValue);
        var max = this._formatter(maxValue);
        this._range.textContent = WebInspector.UIString("[%s\u2009\u2013\u2009%s]", min, max);
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _toggleCounterGraph: function(event)
    {
        this._value.classList.toggle("hidden", !this._filter.checked());
        this._memoryCountersPane.refresh();
    },

    /**
     * @param {number} x
     * @return {number}
     */
    _recordIndexAt: function(x)
    {
        return this.counter.x.upperBound(x * window.devicePixelRatio, null, this.counter._minimumIndex + 1, this.counter._maximumIndex + 1) - 1;
    },

    /**
     * @param {number} x
     */
    updateCurrentValue: function(x)
    {
        if (!this.visible() || !this.counter.values.length || !this.counter.x)
            return;
        var index = this._recordIndexAt(x);
        var value = Number.withThousandsSeparator(this.counter.values[index]);
        this._value.textContent = WebInspector.UIString(this._currentValueLabel, value);
        var y = this.graphYValues[index] / window.devicePixelRatio;
        this._marker.style.left = x + "px";
        this._marker.style.top = y + "px";
        this._marker.classList.remove("hidden");
    },

    _clearCurrentValueAndMarker: function()
    {
        this._value.textContent = "";
        this._marker.classList.add("hidden");
    },

    /**
     * @param {!HTMLCanvasElement} canvas
     */
    _drawGraph: function(canvas)
    {
        var ctx = canvas.getContext("2d");
        var width = canvas.width;
        var height = canvas.height - 2 * this._verticalPadding;
        if (height <= 0) {
            this.graphYValues = [];
            return;
        }
        var originY = this._verticalPadding;
        var counter = this.counter;
        var values = counter.values;

        if (!values.length)
            return;

        var bounds = counter._calculateBounds();
        var minValue = bounds.min;
        var maxValue = bounds.max;
        this.setRange(minValue, maxValue);

        if (!this.visible())
            return;

        var yValues = this.graphYValues;
        var maxYRange = maxValue - minValue;
        var yFactor = maxYRange ? height / (maxYRange) : 1;

        ctx.save();
        ctx.lineWidth = window.devicePixelRatio;
        if (ctx.lineWidth % 2)
            ctx.translate(0.5, 0.5);
        ctx.beginPath();
        var value = values[counter._minimumIndex];
        var currentY = Math.round(originY + height - (value - minValue) * yFactor);
        ctx.moveTo(0, currentY);
        for (var i = counter._minimumIndex; i <= counter._maximumIndex; i++) {
            var x = Math.round(counter.x[i]);
            ctx.lineTo(x, currentY);
            var currentValue = values[i];
            if (typeof currentValue !== "undefined")
                value = currentValue;
            currentY = Math.round(originY + height - (value - minValue) * yFactor);
            ctx.lineTo(x, currentY);
            yValues[i] = currentY;
        }
        yValues.length = i;
        ctx.lineTo(width, currentY);
        ctx.strokeStyle = this.graphColor;
        ctx.stroke();
        if (counter._limitValue) {
            var limitLineY = Math.round(originY + height - (counter._limitValue - minValue) * yFactor);
            ctx.moveTo(0, limitLineY);
            ctx.lineTo(width, limitLineY);
            ctx.strokeStyle = this.limitColor;
            ctx.stroke();
        }
        ctx.closePath();
        ctx.restore();
    },

    /**
     * @return {boolean}
     */
    visible: function()
    {
        return this._filter.checked();
    }
}

/**
 * @constructor
 * @param {!WebInspector.TimelineModel} model
 * @implements {WebInspector.TimelineGrid.Calculator}
 */
WebInspector.CounterGraphCalculator = function(model)
{
    this._model = model;
}

WebInspector.CounterGraphCalculator._minWidth = 5;

WebInspector.CounterGraphCalculator.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;
    },

    setWindow: 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 - WebInspector.CounterGraphCalculator._minWidth - this._paddingLeft;
    },

    /**
     * @override
     * @param {number} value
     * @param {number=} precision
     * @return {string}
     */
    formatValue: 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._model.minimumRecordTime();
    },

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