/*
 * Copyright (C) 2010 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 {!WebInspector.NetworkRequest} request
 * @param {!WebInspector.NetworkTimeCalculator} calculator
 */
WebInspector.RequestTimingView = function(request, calculator)
{
    WebInspector.VBox.call(this);
    this.element.classList.add("resource-timing-view");

    this._request = request;
    this._calculator = calculator;
}

WebInspector.RequestTimingView.prototype = {
    wasShown: function()
    {
        this._request.addEventListener(WebInspector.NetworkRequest.Events.TimingChanged, this._refresh, this);
        this._request.addEventListener(WebInspector.NetworkRequest.Events.FinishedLoading, this._refresh, this);
        this._calculator.addEventListener(WebInspector.NetworkTimeCalculator.Events.BoundariesChanged, this._refresh, this);
        this._refresh();
    },

    willHide: function()
    {
        this._request.removeEventListener(WebInspector.NetworkRequest.Events.TimingChanged, this._refresh, this);
        this._request.removeEventListener(WebInspector.NetworkRequest.Events.FinishedLoading, this._refresh, this);
        this._calculator.removeEventListener(WebInspector.NetworkTimeCalculator.Events.BoundariesChanged, this._refresh, this);
    },

    _refresh: function()
    {
        if (this._tableElement)
            this._tableElement.remove();

        this._tableElement = WebInspector.RequestTimingView.createTimingTable(this._request, this._calculator.minimumBoundary());
        this.element.appendChild(this._tableElement);
    },

    __proto__: WebInspector.VBox.prototype
}

/** @enum {string} */
WebInspector.RequestTimeRangeNames = {
    Push: "push",
    Queueing: "queueing",
    Blocking: "blocking",
    Connecting: "connecting",
    DNS: "dns",
    Proxy: "proxy",
    Receiving: "receiving",
    ReceivingPush: "receiving-push",
    Sending: "sending",
    ServiceWorker: "serviceworker",
    ServiceWorkerPreparation: "serviceworker-preparation",
    SSL: "ssl",
    Total: "total",
    Waiting: "waiting"
};

WebInspector.RequestTimingView.ConnectionSetupRangeNames = [
    WebInspector.RequestTimeRangeNames.Queueing,
    WebInspector.RequestTimeRangeNames.Blocking,
    WebInspector.RequestTimeRangeNames.Connecting,
    WebInspector.RequestTimeRangeNames.DNS,
    WebInspector.RequestTimeRangeNames.Proxy,
    WebInspector.RequestTimeRangeNames.SSL
].keySet();

/** @typedef {{name: !WebInspector.RequestTimeRangeNames, start: number, end: number}} */
WebInspector.RequestTimeRange;

/**
 * @param {!WebInspector.RequestTimeRangeNames} name
 * @return {string}
 */
WebInspector.RequestTimingView._timeRangeTitle = function(name)
{
    switch (name) {
    case WebInspector.RequestTimeRangeNames.Push: return WebInspector.UIString("Receiving Push");
    case WebInspector.RequestTimeRangeNames.Queueing: return WebInspector.UIString("Queueing");
    case WebInspector.RequestTimeRangeNames.Blocking: return WebInspector.UIString("Stalled");
    case WebInspector.RequestTimeRangeNames.Connecting: return WebInspector.UIString("Initial connection");
    case WebInspector.RequestTimeRangeNames.DNS: return WebInspector.UIString("DNS Lookup");
    case WebInspector.RequestTimeRangeNames.Proxy: return WebInspector.UIString("Proxy negotiation");
    case WebInspector.RequestTimeRangeNames.ReceivingPush: return WebInspector.UIString("Reading Push");
    case WebInspector.RequestTimeRangeNames.Receiving: return WebInspector.UIString("Content Download");
    case WebInspector.RequestTimeRangeNames.Sending: return WebInspector.UIString("Request sent");
    case WebInspector.RequestTimeRangeNames.ServiceWorker: return WebInspector.UIString("Request to ServiceWorker");
    case WebInspector.RequestTimeRangeNames.ServiceWorkerPreparation: return WebInspector.UIString("ServiceWorker Preparation");
    case WebInspector.RequestTimeRangeNames.SSL: return WebInspector.UIString("SSL");
    case WebInspector.RequestTimeRangeNames.Total: return WebInspector.UIString("Total");
    case WebInspector.RequestTimeRangeNames.Waiting: return WebInspector.UIString("Waiting (TTFB)");
    default: return WebInspector.UIString(name);
    }
}

/**
 * @param {!WebInspector.NetworkRequest} request
 * @param {number} navigationStart
 * @return {!Array.<!WebInspector.RequestTimeRange>}
 */
WebInspector.RequestTimingView.calculateRequestTimeRanges = function(request, navigationStart)
{
    var result = [];
    /**
     * @param {!WebInspector.RequestTimeRangeNames} name
     * @param {number} start
     * @param {number} end
     */
    function addRange(name, start, end)
    {
        if (start < Number.MAX_VALUE && start <= end)
            result.push({name: name, start: start, end: end});
    }

    /**
     * @param {!Array.<number>} numbers
     * @return {number|undefined}
     */
    function firstPositive(numbers)
    {
        for (var i = 0; i < numbers.length; ++i) {
            if (numbers[i] > 0)
                return numbers[i];
        }
        return undefined;
    }

    /**
     * @param {!WebInspector.RequestTimeRangeNames} name
     * @param {number} start
     * @param {number} end
     */
    function addOffsetRange(name, start, end)
    {
        if (start >= 0 && end >= 0)
            addRange(name, startTime + (start / 1000), startTime + (end / 1000));
    }

    var timing = request.timing;
    if (!timing) {
        var start = request.issueTime() !== -1 ? request.issueTime() : request.startTime !== -1 ? request.startTime : 0;
        var middle = (request.responseReceivedTime === -1) ? Number.MAX_VALUE : request.responseReceivedTime;
        var end = (request.endTime === -1) ? Number.MAX_VALUE : request.endTime;
        addRange(WebInspector.RequestTimeRangeNames.Total, start, end);
        addRange(WebInspector.RequestTimeRangeNames.Blocking, start, middle);
        addRange(WebInspector.RequestTimeRangeNames.Receiving, middle, end);
        return result;
    }

    var issueTime = request.issueTime();
    var startTime = timing.requestTime;
    var endTime = firstPositive([request.endTime, request.responseReceivedTime]) || startTime;

    addRange(WebInspector.RequestTimeRangeNames.Total, issueTime < startTime ? issueTime : startTime, endTime);
    if (timing.pushStart) {
        var pushEnd = timing.pushEnd || endTime;
        // Only show the part of push that happened after the navigation/reload.
        // Pushes that happened on the same connection before we started main request will not be shown.
        if (pushEnd > navigationStart)
            addRange(WebInspector.RequestTimeRangeNames.Push, Math.max(timing.pushStart, navigationStart), pushEnd);
    }
    if (issueTime < startTime)
        addRange(WebInspector.RequestTimeRangeNames.Queueing, issueTime, startTime);

    if (request.fetchedViaServiceWorker) {
        addOffsetRange(WebInspector.RequestTimeRangeNames.Blocking, 0, timing.workerStart);
        addOffsetRange(WebInspector.RequestTimeRangeNames.ServiceWorkerPreparation, timing.workerStart, timing.workerReady);
        addOffsetRange(WebInspector.RequestTimeRangeNames.ServiceWorker, timing.workerReady, timing.sendEnd);
        addOffsetRange(WebInspector.RequestTimeRangeNames.Waiting, timing.sendEnd, timing.receiveHeadersEnd);
    } else if (!timing.pushStart) {
        var blocking = firstPositive([timing.dnsStart, timing.connectStart, timing.sendStart]) || 0;
        addOffsetRange(WebInspector.RequestTimeRangeNames.Blocking, 0, blocking);
        addOffsetRange(WebInspector.RequestTimeRangeNames.Proxy, timing.proxyStart, timing.proxyEnd);
        addOffsetRange(WebInspector.RequestTimeRangeNames.DNS, timing.dnsStart, timing.dnsEnd);
        addOffsetRange(WebInspector.RequestTimeRangeNames.Connecting, timing.connectStart, timing.connectEnd);
        addOffsetRange(WebInspector.RequestTimeRangeNames.SSL, timing.sslStart, timing.sslEnd);
        addOffsetRange(WebInspector.RequestTimeRangeNames.Sending, timing.sendStart, timing.sendEnd);
        addOffsetRange(WebInspector.RequestTimeRangeNames.Waiting, timing.sendEnd, timing.receiveHeadersEnd);
    }

    if (request.endTime !== -1)
        addRange(timing.pushStart ? WebInspector.RequestTimeRangeNames.ReceivingPush : WebInspector.RequestTimeRangeNames.Receiving, request.responseReceivedTime, endTime);

    return result;
}

/**
 * @param {!WebInspector.NetworkRequest} request
 * @param {number} navigationStart
 * @return {!Element}
 */
WebInspector.RequestTimingView.createTimingTable = function(request, navigationStart)
{
    var tableElement = createElementWithClass("table", "network-timing-table");
    var colgroup = tableElement.createChild("colgroup");
    colgroup.createChild("col", "labels");
    colgroup.createChild("col", "bars");
    colgroup.createChild("col", "duration");

    var timeRanges = WebInspector.RequestTimingView.calculateRequestTimeRanges(request, navigationStart);
    var startTime = timeRanges.map(r => r.start).reduce((a, b) => Math.min(a, b));
    var endTime = timeRanges.map(r => r.end).reduce((a, b) => Math.max(a, b));
    var scale = 100 / (endTime - startTime);

    var connectionHeader;
    var dataHeader;
    var totalDuration = 0;

    for (var i = 0; i < timeRanges.length; ++i) {
        var range = timeRanges[i];
        var rangeName = range.name;
        if (rangeName === WebInspector.RequestTimeRangeNames.Total) {
            totalDuration = range.end - range.start;
            continue;
        }
        if (rangeName === WebInspector.RequestTimeRangeNames.Push) {
            createHeader(WebInspector.UIString("Server Push"));
        } else if (WebInspector.RequestTimingView.ConnectionSetupRangeNames[rangeName]) {
            if (!connectionHeader)
                connectionHeader = createHeader(WebInspector.UIString("Connection Setup"));
        } else {
            if (!dataHeader)
                dataHeader = createHeader(WebInspector.UIString("Request/Response"));
        }

        var left = (scale * (range.start - startTime));
        var right = (scale * (endTime - range.end));
        var duration = range.end - range.start;

        var tr = tableElement.createChild("tr");
        tr.createChild("td").createTextChild(WebInspector.RequestTimingView._timeRangeTitle(rangeName));

        var row = tr.createChild("td").createChild("div", "network-timing-row");
        var bar = row.createChild("span", "network-timing-bar " + rangeName);
        bar.style.left = left + "%";
        bar.style.right = right + "%";
        bar.textContent = "\u200B"; // Important for 0-time items to have 0 width.
        var label = tr.createChild("td").createChild("div", "network-timing-bar-title");
        label.textContent = Number.secondsToString(duration, true);
    }

    if (!request.finished) {
        var cell = tableElement.createChild("tr").createChild("td", "caution");
        cell.colSpan = 3;
        cell.createTextChild(WebInspector.UIString("CAUTION: request is not finished yet!"));
    }

    var footer = tableElement.createChild("tr", "network-timing-footer");
    var note = footer.createChild("td");
    note.colSpan = 2;
    note.appendChild(WebInspector.linkifyDocumentationURLAsNode("profile/network-performance/resource-loading#view-network-timing-details-for-a-specific-resource", WebInspector.UIString("Explanation")));
    footer.createChild("td").createTextChild(Number.secondsToString(totalDuration, true));

    return tableElement;

    /**
     * param {string} title
     */
    function createHeader(title)
    {
        var dataHeader = tableElement.createChild("tr", "network-timing-table-header");
        dataHeader.createChild("td").createTextChild(title);
        dataHeader.createChild("td").createTextChild("");
        dataHeader.createChild("td").createTextChild(WebInspector.UIString("TIME"));
        return dataHeader;
    }
}
