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

var controllers = controllers || {};

(function(){

var kCheckoutUnavailableMessage = 'Failed! Garden-o-matic needs a local server to modify your working copy. Please run "webkit-patch garden-o-matic" start the local server.';

// FIXME: Where should this function go?
function rebaselineWithStatusUpdates(failureInfoList, resultsByTest)
{
    var statusView = new ui.StatusArea('Rebaseline');
    var id = statusView.newId();

    var failuresToRebaseline = [];
    var testNamesLogged = [];
    failureInfoList.forEach(function(failureInfo) {
        if (isAnyReftest(failureInfo.testName, resultsByTest)) {
            if (testNamesLogged.indexOf(failureInfo.testName) == -1) {
                statusView.addMessage(id, failureInfo.testName + ' is a ref test, skipping');
                testNamesLogged.push(failureInfo.testName);
            }
        } else {
            failuresToRebaseline.push(failureInfo);
            if (testNamesLogged.indexOf(failureInfo.testName) == -1) {
                statusView.addMessage(id, 'Rebaselining ' + failureInfo.testName + '...');
                testNamesLogged.push(failureInfo.testName);
            }
        }
    });

    if (failuresToRebaseline.length) {
        checkout.rebaseline(failuresToRebaseline, function() {
            statusView.addFinalMessage(id, 'Rebaseline done! Please land with "webkit-patch land-cowhand".');
    }, function(failureInfo) {
        statusView.addMessage(id, failureInfo.testName + ' on ' + ui.displayNameForBuilder(failureInfo.builderName));
    }, function() {
        statusView.addFinalMessage(id, kCheckoutUnavailableMessage);
    });
    } else {
        statusView.addFinalMessage(id, 'No non-reftests left to rebaseline!')
    }
}

// FIXME: This is duplicated from ui/results.js :(.
function isAnyReftest(testName, resultsByTest)
{
    return Object.keys(resultsByTest[testName]).map(function(builder) {
        return resultsByTest[testName][builder];
    }).some(function(resultNode) {
        return resultNode.reftest_type && resultNode.reftest_type.length;
    });
}

// FIXME: Where should this function go?
function updateExpectationsWithStatusUpdates(failureInfoList)
{
    var statusView = new ui.StatusArea('Expectations Update');
    var id = statusView.newId();

    var testNames = base.uniquifyArray(failureInfoList.map(function(failureInfo) { return failureInfo.testName; }));
    var testName = testNames.length == 1 ? testNames[0] : testNames.length + ' tests';
    statusView.addMessage(id, 'Updating expectations of ' + testName + '...');

    checkout.updateExpectations(failureInfoList, function() {
        statusView.addFinalMessage(id, 'Expectations update done! Please land with "webkit-patch land-cowhand".');
    }, function() {
        statusView.addFinalMessage(id, kCheckoutUnavailableMessage);
    });
}

controllers.ResultsDetails = base.extends(Object, {
    init: function(view, resultsByTest)
    {
        this._view = view;
        this._resultsByTest = resultsByTest;
        this._view.setResultsByTest(resultsByTest);

        this._view.firstResult();

        $(this._view).bind('next', this.onNext.bind(this));
        $(this._view).bind('previous', this.onPrevious.bind(this));
        $(this._view).bind('rebaseline', this.onRebaseline.bind(this));
        $(this._view).bind('expectfailure', this.onUpdateExpectations.bind(this));
    },
    onNext: function()
    {
        this._view.nextResult();
    },
    onPrevious: function()
    {
        this._view.previousResult();
    },
    _failureInfoList: function()
    {
        var testName = this._view.currentTestName();
        return Object.keys(this._resultsByTest[testName]).map(function(builderName) {
            return results.failureInfoForTestAndBuilder(this._resultsByTest, testName, builderName);
        }.bind(this));
    },
    onRebaseline: function()
    {
        rebaselineWithStatusUpdates(this._failureInfoList(), this._resultsByTest);
        this._view.nextTest();
    },
    onUpdateExpectations: function()
    {
        updateExpectationsWithStatusUpdates(this._failureInfoList());
    }
});

controllers.ExpectedFailures = base.extends(Object, {
    init: function(model, view, delegate)
    {
        this._model = model;
        this._view = view;
        this._delegate = delegate;
    },
    update: function()
    {
        var expectedFailures = results.expectedFailuresByTest(this._model.resultsByBuilder);
        var failingTestsList = Object.keys(expectedFailures);

        $(this._view).empty();
        base.forEachDirectory(failingTestsList, function(label, testsFailingInDirectory) {
            var listItem = new ui.failures.ListItem(label, testsFailingInDirectory);
            this._view.appendChild(listItem);
            $(listItem).bind('examine', function() {
                this.onExamine(testsFailingInDirectory);
            }.bind(this));
        }.bind(this));
    },
    onExamine: function(failingTestsList)
    {
        var resultsView = new ui.results.View({
            fetchResultsURLs: results.fetchResultsURLs
        });
        var failuresByTest = base.filterDictionary(
            results.expectedFailuresByTest(this._model.resultsByBuilder),
            function(key) {
                return failingTestsList.indexOf(key) != -1;
            });
        var controller = new controllers.ResultsDetails(resultsView, failuresByTest);
        this._delegate.showResults(resultsView);
    }
});

var FailureStreamController = base.extends(Object, {
    _resultsFilter: null,
    _keyFor: function(failureAnalysis) { throw "Not implemented!"; },
    _createFailureView: function(failureAnalysis) { throw "Not implemented!"; },

    init: function(model, view, delegate)
    {
        this._model = model;
        this._view = view;
        this._delegate = delegate;
        this._testFailures = new base.UpdateTracker();
    },
    update: function(failureAnalysis)
    {
        var key = this._keyFor(failureAnalysis);
        var failure = this._testFailures.get(key);
        if (!failure) {
            failure = this._createFailureView(failureAnalysis);
            this._view.add(failure);
            $(failure).bind('examine', function() {
                this.onExamine(failure);
            }.bind(this));
            $(failure).bind('rebaseline', function() {
                this.onRebaseline(failure);
            }.bind(this));
            $(failure).bind('expectfailure', function() {
                this.onUpdateExpectations(failure);
            }.bind(this));
        }
        failure.addFailureAnalysis(failureAnalysis);
        this._testFailures.update(key, failure);
        return failure;
    },
    purge: function() {
        this._testFailures.purge(function(failure) {
            failure.dismiss();
        });
        this._testFailures.forEach(function(failure) {
            failure.purge();
        });
    },
    onExamine: function(failures)
    {
        var resultsView = new ui.results.View({
            fetchResultsURLs: results.fetchResultsURLs
        });

        var testNameList = failures.testNameList();
        var failuresByTest = base.filterDictionary(
            this._resultsFilter(this._model.resultsByBuilder),
            function(key) {
                return testNameList.indexOf(key) != -1;
            });

        var controller = new controllers.ResultsDetails(resultsView, failuresByTest);
        this._delegate.showResults(resultsView);
    },
    _toFailureInfoList: function(failures)
    {
        return base.flattenArray(failures.testNameList().map(model.unexpectedFailureInfoForTestName));
    },
    onRebaseline: function(failures)
    {
        var testNameList = failures.testNameList();
        var failuresByTest = base.filterDictionary(
            this._resultsFilter(this._model.resultsByBuilder),
            function(key) {
                return testNameList.indexOf(key) != -1;
            });

        rebaselineWithStatusUpdates(this._toFailureInfoList(failures), failuresByTest);
    },
    onUpdateExpectations: function(failures)
    {
        updateExpectationsWithStatusUpdates(this._toFailureInfoList(failures));
    }
});

controllers.UnexpectedFailures = base.extends(FailureStreamController, {
    _resultsFilter: results.unexpectedFailuresByTest,

    _impliedFirstFailingRevision: function(failureAnalysis)
    {
        return failureAnalysis.newestPassingRevision + 1;
    },
    _keyFor: function(failureAnalysis)
    {
        return failureAnalysis.newestPassingRevision + "+" + failureAnalysis.oldestFailingRevision;
    },
    _createFailureView: function(failureAnalysis)
    {
        var failure = new ui.notifications.FailingTestsSummary();
        model.commitDataListForRevisionRange(this._impliedFirstFailingRevision(failureAnalysis), failureAnalysis.oldestFailingRevision).forEach(function(commitData) {
            var suspiciousCommit = failure.addCommitData(commitData);
            $(suspiciousCommit).bind('rollout', function() {
                this.onRollout(commitData.revision, failure.testNameList());
            }.bind(this));
            $(failure).bind('blame', function() {
                this.onBlame(failure, commitData);
            }.bind(this));
        }, this);

        return failure;
    },
    update: function(failureAnalysis)
    {
        var failure = FailureStreamController.prototype.update.call(this, failureAnalysis);
        failure.updateBuilderResults(model.buildersInFlightForRevision(this._impliedFirstFailingRevision(failureAnalysis)));
    },
    length: function()
    {
        return this._testFailures.length();
    },
    onBlame: function(failure, commitData)
    {
        failure.pinToCommitData(commitData);
        $('.action', failure).each(function() {
            // FIXME: This isn't the right way of finding and disabling this action.
            if (this.textContent == 'Blame')
                this.disabled = true;
        });
    },
    onRollout: function(revision, testNameList)
    {
        checkout.rollout(revision, ui.rolloutReasonForTestNameList(testNameList), $.noop, function() {
            // FIXME: We should have a better error UI.
            alert(kCheckoutUnavailableMessage);
        });
    }
});

controllers.Failures = base.extends(FailureStreamController, {
    _resultsFilter: results.expectedFailuresByTest,

    _keyFor: function(failureAnalysis)
    {
        return base.dirName(failureAnalysis.testName);
    },
    _createFailureView: function(failureAnalysis)
    {
        return new ui.notifications.FailingTests();
    },
});

controllers.FailingBuilders = base.extends(Object, {
    init: function(view, message)
    {
        this._view = view;
        this._message = message;
        this._notification = null;
    },
    hasFailures: function()
    {
        return !!this._notification;
    },
    update: function(failuresList)
    {
        if (Object.keys(failuresList).length == 0) {
            if (this._notification) {
                this._notification.dismiss();
                this._notification = null;
            }
            return;
        }
        if (!this._notification) {
            this._notification = new ui.notifications.BuildersFailing(this._message);
            this._view.add(this._notification);
        }
        // FIXME: We should provide regression ranges for the failing builders.
        // This doesn't seem to happen often enough to worry too much about that, however.
        this._notification.setFailingBuilders(failuresList);
    }
});

})();
