/*
 * Copyright (C) 2013 Google Inc. All rights reserved.
 * Copyright (C) 2012 Intel 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.Object}
 * @param {!WebInspector.TimelineModel} model
 */
WebInspector.TimelinePresentationModel = function(model)
{
    this._model = model;
    this._filters = [];
    /**
     * @type {!Map.<!WebInspector.TimelineModel.Record, !WebInspector.TimelinePresentationModel.Record>}
     */
    this._recordToPresentationRecord = new Map();
    this.reset();
}

WebInspector.TimelinePresentationModel.prototype = {
    /**
     * @param {number} startTime
     * @param {number} endTime
     */
    setWindowTimes: function(startTime, endTime)
    {
        this._windowStartTime = startTime;
        this._windowEndTime = endTime;
    },

    /**
     * @param {?WebInspector.TimelineModel.Record} record
     * @return {?WebInspector.TimelinePresentationModel.Record}
     */
    toPresentationRecord: function(record)
    {
        return record ? this._recordToPresentationRecord.get(record) || null : null;
    },

    /**
     * @return {!WebInspector.TimelinePresentationModel.Record}
     */
    rootRecord: function()
    {
        return this._rootRecord;
    },

    reset: function()
    {
        this._recordToPresentationRecord.clear();
        this._rootRecord = new WebInspector.TimelinePresentationModel.RootRecord();
        /** @type {!Object.<string, !WebInspector.TimelinePresentationModel.Record>} */
        this._coalescingBuckets = {};
    },

    /**
     * @param {!WebInspector.TimelineModel.Record} record
     */
    addRecord: function(record)
    {
        if (WebInspector.TracingModel.isTopLevelEvent(record.traceEvent())) {
            var records = record.children();
            for (var i = 0; i < records.length; ++i)
                this._innerAddRecord(this._rootRecord, records[i]);
        } else {
            this._innerAddRecord(this._rootRecord, record);
        }
    },

    /**
     * @param {!WebInspector.TimelinePresentationModel.Record} parentRecord
     * @param {!WebInspector.TimelineModel.Record} record
     */
    _innerAddRecord: function(parentRecord, record)
    {
        var coalescingBucket;

        // On main thread, only coalesce if the last event is of same type.
        if (parentRecord === this._rootRecord)
            coalescingBucket = record.thread() ? record.type() : "mainThread";
        var coalescedRecord = this._findCoalescedParent(record, parentRecord, coalescingBucket);
        if (coalescedRecord)
            parentRecord = coalescedRecord;

        var formattedRecord = new WebInspector.TimelinePresentationModel.ActualRecord(record, parentRecord);
        this._recordToPresentationRecord.set(record, formattedRecord);

        formattedRecord._collapsed = parentRecord === this._rootRecord;
        if (coalescingBucket)
            this._coalescingBuckets[coalescingBucket] = formattedRecord;

        for (var i = 0; record.children() && i < record.children().length; ++i)
            this._innerAddRecord(formattedRecord, record.children()[i]);

        if (parentRecord.coalesced())
            this._updateCoalescingParent(formattedRecord);
    },

    /**
     * @param {!WebInspector.TimelineModel.Record} record
     * @param {!WebInspector.TimelinePresentationModel.Record} newParent
     * @param {string=} bucket
     * @return {?WebInspector.TimelinePresentationModel.Record}
     */
    _findCoalescedParent: function(record, newParent, bucket)
    {
        const coalescingThresholdMillis = 5;

        var lastRecord = bucket ? this._coalescingBuckets[bucket] : newParent._presentationChildren.peekLast();
        if (lastRecord && lastRecord.coalesced())
            lastRecord = lastRecord._presentationChildren.peekLast();
        var startTime = record.startTime();
        var endTime = record.endTime();
        if (!lastRecord)
            return null;
        if (lastRecord.record().type() !== record.type())
            return null;
        if (!WebInspector.TimelineUIUtils.isCoalescable(record.type()))
            return null;
        if (lastRecord.record().endTime() + coalescingThresholdMillis < startTime)
            return null;
        if (endTime + coalescingThresholdMillis < lastRecord.record().startTime())
            return null;
        if (lastRecord.presentationParent().coalesced())
            return lastRecord.presentationParent();
        return this._replaceWithCoalescedRecord(lastRecord);
    },

    /**
     * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
     * @return {!WebInspector.TimelinePresentationModel.Record}
     */
    _replaceWithCoalescedRecord: function(presentationRecord)
    {
        var record = presentationRecord.record();
        var parent = presentationRecord._presentationParent;
        var coalescedRecord = new WebInspector.TimelinePresentationModel.CoalescedRecord(record);

        coalescedRecord._collapsed = true;
        coalescedRecord._presentationChildren.push(presentationRecord);
        presentationRecord._presentationParent = coalescedRecord;
        if (presentationRecord.hasWarnings() || presentationRecord.childHasWarnings())
            coalescedRecord._childHasWarnings = true;

        coalescedRecord._presentationParent = parent;
        parent._presentationChildren[parent._presentationChildren.indexOf(presentationRecord)] = coalescedRecord;

        return coalescedRecord;
    },

    /**
     * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
     */
    _updateCoalescingParent: function(presentationRecord)
    {
        var parentRecord = presentationRecord._presentationParent;
        if (parentRecord.endTime() < presentationRecord.endTime())
            parentRecord._endTime = presentationRecord.endTime();
    },

    /**
     * @param {?RegExp} textFilter
     */
    setTextFilter: function(textFilter)
    {
        var records = this._recordToPresentationRecord.valuesArray();
        for (var i = 0; i < records.length; ++i)
            records[i]._expandedOrCollapsedWhileFiltered = false;
        this._textFilter = textFilter;
    },

    refreshRecords: function()
    {
        this.reset();
        var modelRecords = this._model.records();
        for (var i = 0; i < modelRecords.length; ++i)
            this.addRecord(modelRecords[i]);
    },

    invalidateFilteredRecords: function()
    {
        delete this._filteredRecords;
    },

    /**
     * @return {!Array.<!WebInspector.TimelinePresentationModel.Record>}
     */
    filteredRecords: function()
    {
        if (!this._rootRecord.presentationChildren().length)
            this.refreshRecords();
        if (this._filteredRecords)
            return this._filteredRecords;

        var recordsInWindow = [];

        var stack = [{children: this._rootRecord._presentationChildren, index: 0, parentIsCollapsed: false, parentRecord: {}}];
        var revealedDepth = 0;

        function revealRecordsInStack() {
            for (var depth = revealedDepth + 1; depth < stack.length; ++depth) {
                if (stack[depth - 1].parentIsCollapsed) {
                    stack[depth].parentRecord._presentationParent._expandable = true;
                    return;
                }
                stack[depth - 1].parentRecord._collapsed = false;
                recordsInWindow.push(stack[depth].parentRecord);
                stack[depth].windowLengthBeforeChildrenTraversal = recordsInWindow.length;
                stack[depth].parentIsRevealed = true;
                revealedDepth = depth;
            }
        }

        while (stack.length) {
            var entry = stack[stack.length - 1];
            var records = entry.children;
            if (records && entry.index < records.length) {
                var record = records[entry.index];
                ++entry.index;
                if (record.startTime() < this._windowEndTime && record.endTime() > this._windowStartTime) {
                    if (this._model.isVisible(record.record())) {
                        record._presentationParent._expandable = true;
                        if (this._textFilter)
                            revealRecordsInStack();
                        if (!entry.parentIsCollapsed) {
                            recordsInWindow.push(record);
                            revealedDepth = stack.length;
                            entry.parentRecord._collapsed = false;
                        }
                    }
                }

                record._expandable = false;

                stack.push({children: record._presentationChildren,
                            index: 0,
                            parentIsCollapsed: entry.parentIsCollapsed || (record._collapsed && (!this._textFilter || record._expandedOrCollapsedWhileFiltered)),
                            parentRecord: record,
                            windowLengthBeforeChildrenTraversal: recordsInWindow.length});
            } else {
                stack.pop();
                revealedDepth = Math.min(revealedDepth, stack.length - 1);
                entry.parentRecord._visibleChildrenCount = recordsInWindow.length - entry.windowLengthBeforeChildrenTraversal;
            }
        }

        this._filteredRecords = recordsInWindow;
        return recordsInWindow;
    },

    __proto__: WebInspector.Object.prototype
}

/**
 * @constructor
 * @param {?WebInspector.TimelinePresentationModel.Record} parentRecord
 */
WebInspector.TimelinePresentationModel.Record = function(parentRecord)
{
    /**
     * @type {!Array.<!WebInspector.TimelinePresentationModel.Record>}
     */
    this._presentationChildren = [];

    if (parentRecord) {
        this._presentationParent = parentRecord;
        parentRecord._presentationChildren.push(this);
    }
}

WebInspector.TimelinePresentationModel.Record.prototype = {
    /**
     * @return {number}
     */
    startTime: function()
    {
        throw new Error("Not implemented.");
    },

    /**
     * @return {number}
     */
    endTime: function()
    {
        throw new Error("Not implemented.");
    },

    /**
     * @return {number}
     */
    selfTime: function()
    {
        throw new Error("Not implemented.");
    },

    /**
     * @return {!WebInspector.TimelineModel.Record}
     */
    record: function()
    {
        throw new Error("Not implemented.");
    },

    /**
     * @return {!Array.<!WebInspector.TimelinePresentationModel.Record>}
     */
    presentationChildren: function()
    {
        return this._presentationChildren;
    },

    /**
     * @return {boolean}
     */
    coalesced: function()
    {
        return false;
    },

    /**
     * @return {boolean}
     */
    collapsed: function()
    {
        return this._collapsed;
    },

    /**
     * @param {boolean} collapsed
     */
    setCollapsed: function(collapsed)
    {
        this._collapsed = collapsed;
        this._expandedOrCollapsedWhileFiltered = true;
    },

    /**
     * @return {?WebInspector.TimelinePresentationModel.Record}
     */
    presentationParent: function()
    {
        return this._presentationParent || null;
    },

    /**
     * @return {number}
     */
    visibleChildrenCount: function()
    {
        return this._visibleChildrenCount || 0;
    },

    /**
     * @return {boolean}
     */
    expandable: function()
    {
        return !!this._expandable;
    },

    /**
     * @return {boolean}
     */
    hasWarnings: function()
    {
        return false;
    },

    /**
     * @return {boolean}
     */
    childHasWarnings: function()
    {
        return this._childHasWarnings;
    },

    /**
     * @return {?WebInspector.TimelineRecordListRow}
     */
    listRow: function()
    {
        return this._listRow;
    },

    /**
     * @param {!WebInspector.TimelineRecordListRow} listRow
     */
    setListRow: function(listRow)
    {
        this._listRow = listRow;
    },

    /**
     * @return {?WebInspector.TimelineRecordGraphRow}
     */
    graphRow: function()
    {
        return this._graphRow;
    },

    /**
     * @param {!WebInspector.TimelineRecordGraphRow} graphRow
     */
    setGraphRow: function(graphRow)
    {
        this._graphRow = graphRow;
    }
}

/**
 * @constructor
 * @extends {WebInspector.TimelinePresentationModel.Record}
 * @param {!WebInspector.TimelineModel.Record} record
 * @param {?WebInspector.TimelinePresentationModel.Record} parentRecord
 */
WebInspector.TimelinePresentationModel.ActualRecord = function(record, parentRecord)
{
    WebInspector.TimelinePresentationModel.Record.call(this, parentRecord);
    this._record = record;

    if (this.hasWarnings()) {
        for (var parent = this._presentationParent; parent && !parent._childHasWarnings; parent = parent._presentationParent)
            parent._childHasWarnings = true;
    }
}

WebInspector.TimelinePresentationModel.ActualRecord.prototype = {
    /**
     * @override
     * @return {number}
     */
    startTime: function()
    {
        return this._record.startTime();
    },

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

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

    /**
     * @override
     * @return {!WebInspector.TimelineModel.Record}
     */
    record: function()
    {
        return this._record;
    },

    /**
     * @override
     * @return {boolean}
     */
    hasWarnings: function()
    {
        return !!this._record.warnings();
    },

    __proto__: WebInspector.TimelinePresentationModel.Record.prototype
}

/**
 * @constructor
 * @extends {WebInspector.TimelinePresentationModel.Record}
 * @param {!WebInspector.TimelineModel.Record} record
 */
WebInspector.TimelinePresentationModel.CoalescedRecord = function(record)
{
    WebInspector.TimelinePresentationModel.Record.call(this, null);
    this._startTime = record.startTime();
    this._endTime = record.endTime();
}

WebInspector.TimelinePresentationModel.CoalescedRecord.prototype = {
    /**
     * @override
     * @return {number}
     */
    startTime: function()
    {
        return this._startTime;
    },

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

    /**
     * @override
     * @return {number}
     */
    selfTime: function()
    {
        return 0;
    },

    /**
     * @override
     * @return {!WebInspector.TimelineModel.Record}
     */
    record: function()
    {
        return this._presentationChildren[0].record();
    },

    /**
     * @override
     * @return {boolean}
     */
    coalesced: function()
    {
        return true;
    },

    /**
     * @override
     * @return {boolean}
     */
    hasWarnings: function()
    {
        return false;
    },

    __proto__: WebInspector.TimelinePresentationModel.Record.prototype
}

/**
 * @constructor
 * @extends {WebInspector.TimelinePresentationModel.Record}
 */
WebInspector.TimelinePresentationModel.RootRecord = function()
{
    WebInspector.TimelinePresentationModel.Record.call(this, null);
}

WebInspector.TimelinePresentationModel.RootRecord.prototype = {
    /**
     * @override
     * @return {boolean}
     */
    hasWarnings: function()
    {
        return false;
    },

    __proto__: WebInspector.TimelinePresentationModel.Record.prototype
}
