/*
 * Copyright (C) 2017 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.
 */

WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.ContentView
{
    constructor(resource, delegate)
    {
        super(null);

        console.assert(resource instanceof WI.Resource);
        console.assert(delegate);

        this._resource = resource;
        this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
        this._resource.addEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this);
        this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this);

        this._delegate = delegate;

        this._searchQuery = null;
        this._searchResults = null;
        this._searchDOMChanges = [];
        this._searchIndex = -1;
        this._automaticallyRevealFirstSearchResult = false;
        this._bouncyHighlightElement = null;
        this._popover = null;
        this._popoverCallStackIconElement = null;

        this._redirectDetailsSections = [];

        this.element.classList.add("resource-details", "resource-headers");
        this.element.tabIndex = 0;

        this._needsSummaryRefresh = false;
        this._needsRedirectHeadersRefresh = false;
        this._needsRequestHeadersRefresh = false;
        this._needsResponseHeadersRefresh = false;
    }

    // Protected

    initialLayout()
    {
        super.initialLayout();

        this._summarySection = new WI.ResourceDetailsSection(WI.UIString("Summary"), "summary");
        this.element.appendChild(this._summarySection.element);
        this._refreshSummarySection();

        this._refreshRedirectHeadersSections();

        this._requestHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "headers");
        this.element.appendChild(this._requestHeadersSection.element);
        this._refreshRequestHeadersSection();

        this._responseHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Response"), "headers");
        this.element.appendChild(this._responseHeadersSection.element);
        this._refreshResponseHeadersSection();

        if (this._resource.urlComponents.queryString) {
            this._queryStringSection = new WI.ResourceDetailsSection(WI.UIString("Query String Parameters"));
            this.element.appendChild(this._queryStringSection.element);
            this._refreshQueryStringSection();
        }

        if (this._resource.requestData) {
            this._requestDataSection = new WI.ResourceDetailsSection(WI.UIString("Request Data"));
            this.element.appendChild(this._requestDataSection.element);
            this._refreshRequestDataSection();
        }

        this._needsSummaryRefresh = false;
        this._needsRedirectHeadersRefresh = false;
        this._needsRequestHeadersRefresh = false;
        this._needsResponseHeadersRefresh = false;
    }

    layout()
    {
        super.layout();

        if (this._needsSummaryRefresh) {
            this._refreshSummarySection();
            this._needsSummaryRefresh = false;
        }

        if (this._needsRedirectHeadersRefresh) {
            this._refreshRedirectHeadersSections();
            this._needsRedirectHeadersRefresh = false;
        }

        if (this._needsRequestHeadersRefresh) {
            this._refreshRequestHeadersSection();
            this._needsRequestHeadersRefresh = false;
        }

        if (this._needsResponseHeadersRefresh) {
            this._refreshResponseHeadersSection();
            this._needsResponseHeadersRefresh = false;
        }
    }

    hidden()
    {
        super.hidden();

        if (this._popover)
            this._popover.dismiss();
    }

    closed()
    {
        this._resource.removeEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
        this._resource.removeEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this);
        this._resource.removeEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this);

        super.closed();
    }

    get supportsSearch()
    {
        return true;
    }

    get numberOfSearchResults()
    {
        return this._searchResults ? this._searchResults.length : null;
    }

    get hasPerformedSearch()
    {
        return this._searchResults !== null;
    }

    set automaticallyRevealFirstSearchResult(reveal)
    {
        this._automaticallyRevealFirstSearchResult = reveal;

        // If we haven't shown a search result yet, reveal one now.
        if (this._automaticallyRevealFirstSearchResult && this.numberOfSearchResults > 0) {
            if (this._searchIndex === -1)
                this.revealNextSearchResult();
        }
    }

    performSearch(query)
    {
        if (query === this._searchQuery)
            return;

        WI.revertDOMChanges(this._searchDOMChanges);

        this._searchQuery = query;
        this._searchResults = [];
        this._searchDOMChanges = [];
        this._searchIndex = -1;

        this._perfomSearchOnKeyValuePairs();

        this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);

        if (this._automaticallyRevealFirstSearchResult && this._searchResults.length > 0)
            this.revealNextSearchResult();
    }

    searchCleared()
    {
        WI.revertDOMChanges(this._searchDOMChanges);

        this._searchQuery = null;
        this._searchResults = null;
        this._searchDOMChanges = [];
        this._searchIndex = -1;
    }

    revealPreviousSearchResult(changeFocus)
    {
        if (!this.numberOfSearchResults)
            return;

        if (this._searchIndex > 0)
            --this._searchIndex;
        else
            this._searchIndex = this._searchResults.length - 1;

        this._revealSearchResult(this._searchIndex, changeFocus);
    }

    revealNextSearchResult(changeFocus)
    {
        if (!this.numberOfSearchResults)
            return;

        if (this._searchIndex + 1 < this._searchResults.length)
            ++this._searchIndex;
        else
            this._searchIndex = 0;

        this._revealSearchResult(this._searchIndex, changeFocus);
    }

    // Private

    _responseSourceDisplayString(responseSource)
    {
        switch (responseSource) {
        case WI.Resource.ResponseSource.Network:
            return WI.UIString("Network");
        case WI.Resource.ResponseSource.MemoryCache:
            return WI.UIString("Memory Cache");
        case WI.Resource.ResponseSource.DiskCache:
            return WI.UIString("Disk Cache");
        case WI.Resource.ResponseSource.ServiceWorker:
            return WI.UIString("Service Worker");
        case WI.Resource.ResponseSource.InspectorOverride:
            return WI.UIString("Local Override");
        case WI.Resource.ResponseSource.Unknown:
        default:
            return null;
        }
    }

    _refreshSummarySection()
    {
        let detailsElement = this._summarySection.detailsElement;
        detailsElement.removeChildren();

        this._summarySection.toggleError(this._resource.hadLoadingError());

        for (let redirect of this._resource.redirects)
            this._summarySection.appendKeyValuePair(WI.UIString("URL"), redirect.url.insertWordBreakCharacters(), "url");
        this._summarySection.appendKeyValuePair(WI.UIString("URL"), this._resource.displayURL.insertWordBreakCharacters(), "url");

        let status = emDash;
        if (!isNaN(this._resource.statusCode))
            status = this._resource.statusCode + (this._resource.statusText ? " " + this._resource.statusText : "");
        this._summarySection.appendKeyValuePair(WI.UIString("Status"), status);

        // FIXME: <https://webkit.org/b/178827> Web Inspector: Should be able to link directly to the ServiceWorker that handled a particular load

        let source = this._responseSourceDisplayString(this._resource.responseSource) || emDash;
        this._summarySection.appendKeyValuePair(WI.UIString("Source"), source);

        if (this._resource.remoteAddress)
            this._summarySection.appendKeyValuePair(WI.UIString("Address"), this._resource.remoteAddress);

        let initiatorLocation = this._resource.initiatorSourceCodeLocation;
        if (initiatorLocation) {

            let fragment = document.createDocumentFragment();

            const options = {
                dontFloat: true,
                ignoreSearchTab: true,
                ignoreNetworkTab: true,
            };
            let link = WI.createSourceCodeLocationLink(initiatorLocation, options);
            fragment.appendChild(link);

            let callFrames = this._resource.initiatorCallFrames;
            if (callFrames) {
                this._popoverCallStackIconElement = document.createElement("img");
                this._popoverCallStackIconElement.className = "call-stack";
                fragment.appendChild(this._popoverCallStackIconElement);

                this._popoverCallStackIconElement.addEventListener("click", (event) => {
                    if (!this._popover) {
                        this._popover = new WI.Popover(this);
                        this._popover.windowResizeHandler = () => { this._presentPopoverBelowCallStackElement(); };
                    }

                    const selectable = false;
                    let callFramesTreeOutline = new WI.TreeOutline(selectable);
                    callFramesTreeOutline.disclosureButtons = false;
                    let callFrameTreeController = new WI.CallFrameTreeController(callFramesTreeOutline);
                    callFrameTreeController.callFrames = callFrames;

                    let popoverContent = document.createElement("div");
                    popoverContent.appendChild(callFrameTreeController.treeOutline.element);
                    this._popover.content = popoverContent;

                    this._presentPopoverBelowCallStackElement();
                });
            }

            let pair = this._summarySection.appendKeyValuePair(WI.UIString("Initiator"), fragment);
            pair.classList.add("initiator");

            if (this._popover && this._popover.visible)
                this._presentPopoverBelowCallStackElement();
        }
    }

    _refreshRedirectHeadersSections()
    {
        let referenceElement = this._redirectDetailsSections.length ? this._redirectDetailsSections.lastValue.element : this._summarySection.element;

        for (let i = this._redirectDetailsSections.length; i < this._resource.redirects.length; ++i) {
            let redirect = this._resource.redirects[i];

            let redirectRequestSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "redirect");

            // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
            redirectRequestSection.appendKeyValuePair(`${redirect.requestMethod} ${redirect.urlComponents.path}`, null, "h1-status");

            for (let key in redirect.requestHeaders)
                redirectRequestSection.appendKeyValuePair(key, redirect.requestHeaders[key], "header");

            referenceElement = this.element.insertBefore(redirectRequestSection.element, referenceElement.nextElementSibling);
            this._redirectDetailsSections.push(redirectRequestSection);

            let redirectResponseSection = new WI.ResourceDetailsSection(WI.UIString("Redirect Response"), "redirect");

            // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
            redirectResponseSection.appendKeyValuePair(`${redirect.responseStatusCode} ${redirect.responseStatusText}`, null, "h1-status");

            for (let key in redirect.responseHeaders)
                redirectResponseSection.appendKeyValuePair(key, redirect.responseHeaders[key], "header");

            referenceElement = this.element.insertBefore(redirectResponseSection.element, referenceElement.nextElementSibling);
            this._redirectDetailsSections.push(redirectResponseSection);
        }
    }

    _refreshRequestHeadersSection()
    {
        let detailsElement = this._requestHeadersSection.detailsElement;
        detailsElement.removeChildren();

        // A revalidation request still sends a request even though we served from cache, so show the request.
        if (this._resource.statusCode !== 304) {
            if (this._resource.responseSource === WI.Resource.ResponseSource.MemoryCache) {
                this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request, served from the memory cache."));
                return;
            }
            if (this._resource.responseSource === WI.Resource.ResponseSource.DiskCache) {
                this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request, served from the disk cache."));
                return;
            }
        }

        let protocol = this._resource.protocol || "";
        let urlComponents = this._resource.urlComponents;
        if (protocol.startsWith("http/1")) {
            // HTTP/1.1 request line:
            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
            let requestLine = `${this._resource.requestMethod} ${urlComponents.path} ${protocol.toUpperCase()}`;
            this._requestHeadersSection.appendKeyValuePair(requestLine, null, "h1-status");
        } else if (protocol === "h2") {
            // HTTP/2 Request pseudo headers:
            // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
            this._requestHeadersSection.appendKeyValuePair(":method", this._resource.requestMethod, "h2-pseudo-header");
            this._requestHeadersSection.appendKeyValuePair(":scheme", urlComponents.scheme, "h2-pseudo-header");
            this._requestHeadersSection.appendKeyValuePair(":authority", WI.h2Authority(urlComponents), "h2-pseudo-header");
            this._requestHeadersSection.appendKeyValuePair(":path", WI.h2Path(urlComponents), "h2-pseudo-header");
        }

        let requestHeaders = this._resource.requestHeaders;
        for (let key in requestHeaders)
            this._requestHeadersSection.appendKeyValuePair(key, requestHeaders[key], "header");

        if (!detailsElement.firstChild)
            this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request headers"));
    }

    _refreshResponseHeadersSection()
    {
        let detailsElement = this._responseHeadersSection.detailsElement;
        detailsElement.removeChildren();

        if (!this._resource.hasResponse()) {
            this._responseHeadersSection.markIncompleteSectionWithLoadingIndicator();
            return;
        }

        this._responseHeadersSection.toggleIncomplete(false);

        let protocol = this._resource.protocol || "";
        if (protocol.startsWith("http/1")) {
            // HTTP/1.1 response status line:
            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
            let responseLine = `${protocol.toUpperCase()} ${this._resource.statusCode} ${this._resource.statusText}`;
            this._responseHeadersSection.appendKeyValuePair(responseLine, null, "h1-status");
        } else if (protocol === "h2") {
            // HTTP/2 Response pseudo headers:
            // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
            this._responseHeadersSection.appendKeyValuePair(":status", this._resource.statusCode, "h2-pseudo-header");
        }

        let responseHeaders = this._resource.responseHeaders;
        for (let key in responseHeaders) {
            // Split multiple Set-Cookie response headers out into their multiple headers instead of as a combined value.
            if (key.toLowerCase() === "set-cookie") {
                let responseCookies = this._resource.responseCookies;
                console.assert(responseCookies.length > 0);
                for (let cookie of responseCookies)
                    this._responseHeadersSection.appendKeyValuePair(key, cookie.header, "header");
                continue;
            }

            this._responseHeadersSection.appendKeyValuePair(key, responseHeaders[key], "header");
        }

        if (!detailsElement.firstChild)
            this._responseHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No response headers"));
    }

    _refreshQueryStringSection()
    {
        if (!this._queryStringSection)
            return;

        let detailsElement = this._queryStringSection.detailsElement;
        detailsElement.removeChildren();

        let queryString = this._resource.urlComponents.queryString;
        let queryStringPairs = parseQueryString(queryString, true);
        for (let {name, value} of queryStringPairs)
            this._queryStringSection.appendKeyValuePair(name, value);
    }

    _refreshRequestDataSection()
    {
        if (!this._requestDataSection)
            return;

        let detailsElement = this._requestDataSection.detailsElement;
        detailsElement.removeChildren();

        let requestData = this._resource.requestData;
        let requestDataContentType = this._resource.requestDataContentType || "";

        if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) {
            // Simple form data that should be parsable like a query string.
            this._requestDataSection.appendKeyValuePair(WI.UIString("MIME Type"), requestDataContentType);
            let queryStringPairs = parseQueryString(requestData, true);
            for (let {name, value} of queryStringPairs)
                this._requestDataSection.appendKeyValuePair(name, value);
            return;
        }

        let mimeTypeComponents = parseMIMEType(requestDataContentType);
        let mimeType = mimeTypeComponents.type;
        let boundary = mimeTypeComponents.boundary;
        let encoding = mimeTypeComponents.encoding;

        this._requestDataSection.appendKeyValuePair(WI.UIString("MIME Type"), mimeType);
        if (boundary)
            this._requestDataSection.appendKeyValuePair(WI.UIString("Boundary"), boundary);
        if (encoding)
            this._requestDataSection.appendKeyValuePair(WI.UIString("Encoding"), encoding);

        let goToButton = detailsElement.appendChild(WI.createGoToArrowButton());
        goToButton.addEventListener("click", () => { this._delegate.headersContentViewGoToRequestData(this); });
        this._requestDataSection.appendKeyValuePair(WI.UIString("Request Data"), goToButton);
    }

    _perfomSearchOnKeyValuePairs()
    {
        let searchRegex = WI.SearchUtilities.searchRegExpForString(this._searchQuery, WI.SearchUtilities.defaultSettings);
        if (!searchRegex) {
            this.searchCleared();
            this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange);
            return;
        }

        let elements = this.element.querySelectorAll(".key, .value");
        for (let element of elements) {
            let matchRanges = [];
            let text = element.textContent;
            let match;
            while (match = searchRegex.exec(text))
                matchRanges.push({offset: match.index, length: match[0].length});

            if (matchRanges.length) {
                let highlightedNodes = WI.highlightRangesWithStyleClass(element, matchRanges, "search-highlight", this._searchDOMChanges);
                this._searchResults.pushAll(highlightedNodes);
            }
        }
    }

    _revealSearchResult(index, changeFocus)
    {
        let highlightElement = this._searchResults[index];
        if (!highlightElement)
            return;

        highlightElement.scrollIntoViewIfNeeded();

        if (!this._bouncyHighlightElement) {
            this._bouncyHighlightElement = document.createElement("div");
            this._bouncyHighlightElement.className = "bouncy-highlight";
            this._bouncyHighlightElement.addEventListener("animationend", (event) => {
                this._bouncyHighlightElement.remove();
            });
        }

        this._bouncyHighlightElement.remove();

        let computedStyles = window.getComputedStyle(highlightElement);
        let highlightElementRect = highlightElement.getBoundingClientRect();
        let contentViewRect = this.element.getBoundingClientRect();
        let contentViewScrollTop = this.element.scrollTop;
        let contentViewScrollLeft = this.element.scrollLeft;

        this._bouncyHighlightElement.textContent = highlightElement.textContent;
        this._bouncyHighlightElement.style.top = (highlightElementRect.top - contentViewRect.top + contentViewScrollTop) + "px";
        this._bouncyHighlightElement.style.left = (highlightElementRect.left - contentViewRect.left + contentViewScrollLeft) + "px";
        this._bouncyHighlightElement.style.fontWeight = computedStyles.fontWeight;

        this.element.appendChild(this._bouncyHighlightElement);
    }

    _presentPopoverBelowCallStackElement()
    {
        let bounds = WI.Rect.rectFromClientRect(this._popoverCallStackIconElement.getBoundingClientRect());
        this._popover.present(bounds.pad(2), [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]);
    }

    _resourceMetricsDidChange(event)
    {
        this._needsSummaryRefresh = true;
        this._needsRequestHeadersRefresh = true;
        this._needsResponseHeadersRefresh = true;
        this.needsLayout();
    }

    _resourceRequestHeadersDidChange(event)
    {
        this._needsSummaryRefresh = true;
        this._needsRedirectHeadersRefresh = true;
        this._needsRequestHeadersRefresh = true;
        this.needsLayout();
    }

    _resourceResponseReceived(event)
    {
        this._needsSummaryRefresh = true;
        this._needsResponseHeadersRefresh = true;
        this.needsLayout();
    }
};
