/*
 * Copyright (C) 2013, 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.TimelineDataGrid = class TimelineDataGrid extends WebInspector.DataGrid
{
    constructor(columns, treeOutline, synchronizerDelegate, editCallback, deleteCallback)
    {
        super(columns, editCallback, deleteCallback);

        if (treeOutline)
            this._treeOutlineDataGridSynchronizer = new WebInspector.TreeOutlineDataGridSynchronizer(treeOutline, this, synchronizerDelegate);

        this.element.classList.add("timeline");

        this._sortDelegate = null;
        this._scopeBarColumns = [];

        // Check if any of the cells can be filtered.
        for (var [identifier, column] of this.columns) {
            var scopeBar = column.scopeBar;

            if (!scopeBar)
                continue;

            this._scopeBarColumns.push(identifier);
            scopeBar.columnIdentifier = identifier;
            scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectedItemsDidChange, this);
        }

        if (this._scopeBarColumns.length > 1) {
            console.error("Creating a TimelineDataGrid with more than one filterable column is not yet supported.");
            return;
        }

        this.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._dataGridSelectedNodeChanged, this);
        this.addEventListener(WebInspector.DataGrid.Event.SortChanged, this._sort, this);

        this.columnChooserEnabled = true;
    }

    static createColumnScopeBar(prefix, map)
    {
        prefix = prefix + "-timeline-data-grid-";

        var scopeBarItems = [];
        for (var [key, value] of map) {
            var id = prefix + key;
            var item = new WebInspector.ScopeBarItem(id, value);
            item.value = key;
            scopeBarItems.push(item);
        }

        var allItem = new WebInspector.ScopeBarItem(prefix + "type-all", WebInspector.UIString("All"));
        scopeBarItems.unshift(allItem);

        return new WebInspector.ScopeBar(prefix + "scope-bar", scopeBarItems, allItem, true);
    }

    // Public

    get sortDelegate()
    {
        return this._sortDelegate;
    }

    set sortDelegate(delegate)
    {
        delegate = delegate || null;
        if (this._sortDelegate === delegate)
            return;

        this._sortDelegate = delegate;

        if (this.sortOrder !== WebInspector.DataGrid.SortOrder.Indeterminate)
            this.dispatchEventToListeners(WebInspector.DataGrid.Event.SortChanged);
    }

    reset()
    {
        // May be overridden by subclasses. If so, they should call the superclass.

        if (!this._treeOutlineDataGridSynchronizer)
            this.removeChildren();

        this._hidePopover();
    }

    shown()
    {
        // May be overridden by subclasses. If so, they should call the superclass.

        if (this._treeOutlineDataGridSynchronizer)
            this._treeOutlineDataGridSynchronizer.synchronize();
    }

    hidden()
    {
        // May be overridden by subclasses. If so, they should call the superclass.

        this._hidePopover();
    }

    treeElementForDataGridNode(dataGridNode)
    {
        if (!this._treeOutlineDataGridSynchronizer)
            return null;

        return this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
    }

    dataGridNodeForTreeElement(treeElement)
    {
        if (!this._treeOutlineDataGridSynchronizer)
            return null;

        return this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(treeElement);
    }

    callFramePopoverAnchorElement()
    {
        // Implemented by subclasses.
        return null;
    }

    addRowInSortOrder(treeElement, dataGridNode, parentTreeElementOrDataGridNode)
    {
        let parentDataGridNode;
        let childElement = dataGridNode;

        if (treeElement) {
            console.assert(this._treeOutlineDataGridSynchronizer);
            if (!this._treeOutlineDataGridSynchronizer)
                return;

            this._treeOutlineDataGridSynchronizer.associate(treeElement, dataGridNode);

            console.assert(!parentTreeElementOrDataGridNode || parentTreeElementOrDataGridNode instanceof WebInspector.TreeElement);

            let parentTreeElement = parentTreeElementOrDataGridNode || this._treeOutlineDataGridSynchronizer.treeOutline;
            parentDataGridNode = parentTreeElement.root ? this : this._treeOutlineDataGridSynchronizer.dataGridNodeForTreeElement(parentTreeElement);

            parentTreeElementOrDataGridNode = parentTreeElement;
            childElement = treeElement;
        } else {
            parentTreeElementOrDataGridNode = parentTreeElementOrDataGridNode || this;
            parentDataGridNode = parentTreeElementOrDataGridNode;
        }

        if (this.sortColumnIdentifier) {
            let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, parentDataGridNode.children, this._sortComparator.bind(this));

            // If parent is a tree element, the synchronizer will insert into the data grid.
            parentTreeElementOrDataGridNode.insertChild(childElement, insertionIndex);
        } else {
            // If parent is a tree element, the synchronizer will append to the data grid.
            parentTreeElementOrDataGridNode.appendChild(childElement);
        }
    }

    shouldIgnoreSelectionEvent()
    {
        return this._ignoreSelectionEvent || false;
    }

    // Protected

    dataGridNodeNeedsRefresh(dataGridNode)
    {
        if (!this._dirtyDataGridNodes)
            this._dirtyDataGridNodes = new Set;
        this._dirtyDataGridNodes.add(dataGridNode);

        if (this._scheduledDataGridNodeRefreshIdentifier)
            return;

        this._scheduledDataGridNodeRefreshIdentifier = requestAnimationFrame(this._refreshDirtyDataGridNodes.bind(this));
    }

    hasCustomFilters()
    {
        return true;
    }

    matchNodeAgainstCustomFilters(node)
    {
        if (!super.matchNodeAgainstCustomFilters(node))
            return false;

        for (let identifier of this._scopeBarColumns) {
            let scopeBar = this.columns.get(identifier).scopeBar;
            if (!scopeBar || scopeBar.defaultItem.selected)
                continue;

            let value = node.data[identifier];
            if (!scopeBar.selectedItems.some((scopeBarItem) => scopeBarItem.value === value))
                return false;
        }

        return true;
    }

    // Private

    _refreshDirtyDataGridNodes()
    {
        if (this._scheduledDataGridNodeRefreshIdentifier) {
            cancelAnimationFrame(this._scheduledDataGridNodeRefreshIdentifier);
            this._scheduledDataGridNodeRefreshIdentifier = undefined;
        }

        if (!this._dirtyDataGridNodes)
            return;

        let selectedNode = this.selectedNode;
        let sortComparator = this._sortComparator.bind(this);

        if (this._treeOutlineDataGridSynchronizer)
            this._treeOutlineDataGridSynchronizer.enabled = false;

        for (let dataGridNode of this._dirtyDataGridNodes) {
            dataGridNode.refresh();

            if (!this.sortColumnIdentifier)
                continue;

            if (dataGridNode === selectedNode)
                this._ignoreSelectionEvent = true;

            console.assert(!dataGridNode.parent || dataGridNode.parent === this);
            if (dataGridNode.parent === this)
                this.removeChild(dataGridNode);

            let insertionIndex = insertionIndexForObjectInListSortedByFunction(dataGridNode, this.children, sortComparator);
            this.insertChild(dataGridNode, insertionIndex);

            if (dataGridNode === selectedNode) {
                selectedNode.revealAndSelect();
                this._ignoreSelectionEvent = false;
            }

            if (!this._treeOutlineDataGridSynchronizer)
                continue;

            let treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
            let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
            console.assert(treeElement);

            treeOutline.reattachIfIndexChanged(treeElement, insertionIndex);

            // Adding the tree element back to the tree outline subjects it to filters.
            // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
            dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
        }

        if (this._treeOutlineDataGridSynchronizer)
            this._treeOutlineDataGridSynchronizer.enabled = true;

        this._dirtyDataGridNodes = null;
    }

    _sort()
    {
        if (!this.children.length)
            return;

        let sortColumnIdentifier = this.sortColumnIdentifier;
        if (!sortColumnIdentifier)
            return;

        let selectedNode = this.selectedNode;
        this._ignoreSelectionEvent = true;

        let treeOutline;
        if (this._treeOutlineDataGridSynchronizer) {
            this._treeOutlineDataGridSynchronizer.enabled = false;

            treeOutline = this._treeOutlineDataGridSynchronizer.treeOutline;
            if (treeOutline.selectedTreeElement)
                treeOutline.selectedTreeElement.deselect(true);
        }

        // Collect parent nodes that need their children sorted. So this in two phases since
        // traverseNextNode would get confused if we sort the tree while traversing it.
        let parentDataGridNodes = [this];
        let currentDataGridNode = this.children[0];
        while (currentDataGridNode) {
            if (currentDataGridNode.children.length)
                parentDataGridNodes.push(currentDataGridNode);
            currentDataGridNode = currentDataGridNode.traverseNextNode(false, null, true);
        }

        // Sort the children of collected parent nodes.
        for (let parentDataGridNode of parentDataGridNodes) {
            let childDataGridNodes = parentDataGridNode.children.slice();
            parentDataGridNode.removeChildren();

            let parentTreeElement;
            if (this._treeOutlineDataGridSynchronizer) {
                parentTreeElement = parentDataGridNode === this ? treeOutline : this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(parentDataGridNode);
                console.assert(parentTreeElement);

                parentTreeElement.removeChildren();
            }

            childDataGridNodes.sort(this._sortComparator.bind(this));

            for (let dataGridNode of childDataGridNodes) {
                if (this._treeOutlineDataGridSynchronizer) {
                    let treeElement = this._treeOutlineDataGridSynchronizer.treeElementForDataGridNode(dataGridNode);
                    console.assert(treeElement);

                    if (parentTreeElement)
                        parentTreeElement.appendChild(treeElement);

                    // Adding the tree element back to the tree outline subjects it to filters.
                    // Make sure we keep the hidden state in-sync while the synchronizer is disabled.
                    dataGridNode.element.classList.toggle("hidden", treeElement.hidden);
                }

                parentDataGridNode.appendChild(dataGridNode);
            }
        }

        if (this._treeOutlineDataGridSynchronizer)
            this._treeOutlineDataGridSynchronizer.enabled = true;

        if (selectedNode)
            selectedNode.revealAndSelect();

        this._ignoreSelectionEvent = false;
    }

    _sortComparator(node1, node2)
    {
        var sortColumnIdentifier = this.sortColumnIdentifier;
        if (!sortColumnIdentifier)
            return 0;

        var sortDirection = this.sortOrder === WebInspector.DataGrid.SortOrder.Ascending ? 1 : -1;

        if (this._sortDelegate && typeof this._sortDelegate.dataGridSortComparator === "function") {
            let result = this._sortDelegate.dataGridSortComparator(sortColumnIdentifier, sortDirection, node1, node2);
            if (typeof result === "number")
                return result;
        }

        var value1 = node1.data[sortColumnIdentifier];
        var value2 = node2.data[sortColumnIdentifier];

        if (typeof value1 === "number" && typeof value2 === "number") {
            if (isNaN(value1) && isNaN(value2))
                return 0;
            if (isNaN(value1))
                return -sortDirection;
            if (isNaN(value2))
                return sortDirection;
            return (value1 - value2) * sortDirection;
        }

        if (typeof value1 === "string" && typeof value2 === "string")
            return value1.extendedLocaleCompare(value2) * sortDirection;

        if (value1 instanceof WebInspector.CallFrame || value2 instanceof WebInspector.CallFrame) {
            // Sort by function name if available, then fall back to the source code object.
            value1 = value1 && value1.functionName ? value1.functionName : (value1 && value1.sourceCodeLocation ? value1.sourceCodeLocation.sourceCode : "");
            value2 = value2 && value2.functionName ? value2.functionName : (value2 && value2.sourceCodeLocation ? value2.sourceCodeLocation.sourceCode : "");
        }

        if (value1 instanceof WebInspector.SourceCode || value2 instanceof WebInspector.SourceCode) {
            value1 = value1 ? value1.displayName || "" : "";
            value2 = value2 ? value2.displayName || "" : "";
        }

        if (value1 instanceof WebInspector.SourceCodeLocation || value2 instanceof WebInspector.SourceCodeLocation) {
            value1 = value1 ? value1.displayLocationString() || "" : "";
            value2 = value2 ? value2.displayLocationString() || "" : "";
        }

        // For everything else (mostly booleans).
        return (value1 < value2 ? -1 : (value1 > value2 ? 1 : 0)) * sortDirection;
    }

    _scopeBarSelectedItemsDidChange(event)
    {
        this.filterDidChange();
    }

    _dataGridSelectedNodeChanged(event)
    {
        if (!this.selectedNode) {
            this._hidePopover();
            return;
        }

        var record = this.selectedNode.record;
        if (!record || !record.callFrames || !record.callFrames.length) {
            this._hidePopover();
            return;
        }

        this._showPopoverForSelectedNodeSoon();
    }

    _showPopoverForSelectedNodeSoon()
    {
        if (this._showPopoverTimeout)
            return;

        this._showPopoverTimeout = setTimeout(() => {
            if (!this._popover) {
                this._popover = new WebInspector.Popover;
                this._popover.windowResizeHandler = () => { this._updatePopoverForSelectedNode(false); };
            }
            this._updatePopoverForSelectedNode(true);
            this._showPopoverTimeout = undefined;
        }, WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout);
    }

    _hidePopover()
    {
        if (this._showPopoverTimeout) {
            clearTimeout(this._showPopoverTimeout);
            this._showPopoverTimeout = undefined;
        }

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

        if (this._hidePopoverContentClearTimeout)
            clearTimeout(this._hidePopoverContentClearTimeout);

        this._hidePopoverContentClearTimeout = setTimeout(() => {
            if (this._popoverCallStackTreeOutline)
                this._popoverCallStackTreeOutline.removeChildren();
        }, WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout);
    }

    _updatePopoverForSelectedNode(updateContent)
    {
        if (!this._popover || !this.selectedNode)
            return;

        let targetPopoverElement = this.callFramePopoverAnchorElement();
        console.assert(targetPopoverElement, "TimelineDataGrid subclass should always return a valid element from callFramePopoverAnchorElement.");
        if (!targetPopoverElement)
            return;

        // The element might be hidden if it does not have a width and height.
        let rect = WebInspector.Rect.rectFromClientRect(targetPopoverElement.getBoundingClientRect());
        if (!rect.size.width && !rect.size.height)
            return;

        if (this._hidePopoverContentClearTimeout) {
            clearTimeout(this._hidePopoverContentClearTimeout);
            this._hidePopoverContentClearTimeout = undefined;
        }

        let targetFrame = rect.pad(2);
        let preferredEdges = [WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_X];

        if (updateContent)
            this._popover.presentNewContentWithFrame(this._createPopoverContent(), targetFrame, preferredEdges);
        else
            this._popover.present(targetFrame, preferredEdges);
    }

    _createPopoverContent()
    {
        if (!this._popoverCallStackTreeOutline) {
            this._popoverCallStackTreeOutline = new WebInspector.TreeOutline;
            this._popoverCallStackTreeOutline.disclosureButtons = false;
            this._popoverCallStackTreeOutline.element.classList.add("timeline-data-grid");
            this._popoverCallStackTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._popoverCallStackTreeSelectionDidChange, this);
        } else
            this._popoverCallStackTreeOutline.removeChildren();

        var callFrames = this.selectedNode.record.callFrames;
        for (var i = 0; i < callFrames.length; ++i) {
            var callFrameTreeElement = new WebInspector.CallFrameTreeElement(callFrames[i]);
            this._popoverCallStackTreeOutline.appendChild(callFrameTreeElement);
        }

        var content = document.createElement("div");
        content.className = "timeline-data-grid-popover";
        content.appendChild(this._popoverCallStackTreeOutline.element);
        return content;
    }

    _popoverCallStackTreeSelectionDidChange(event)
    {
        let treeElement = event.data.selectedElement;
        if (!treeElement)
            return;

        this._popover.dismiss();

        console.assert(treeElement instanceof WebInspector.CallFrameTreeElement, "TreeElements in TimelineDataGrid popover should always be CallFrameTreeElements");
        var callFrame = treeElement.callFrame;
        if (!callFrame.sourceCodeLocation)
            return;

        WebInspector.showSourceCodeLocation(callFrame.sourceCodeLocation, {
            ignoreNetworkTab: true,
            ignoreSearchTab: true,
        });
    }
};

WebInspector.TimelineDataGrid.HasNonDefaultFilterStyleClassName = "has-non-default-filter";
WebInspector.TimelineDataGrid.DelayedPopoverShowTimeout = 250;
WebInspector.TimelineDataGrid.DelayedPopoverHideContentClearTimeout = 500;
