
class AnalysisTaskChartPane extends ChartPaneBase {
    constructor()
    {
        super('analysis-task-chart-pane');
        this._page = null;
        this._showForm = false;
    }

    setPage(page) { this._page = page; }
    setShowForm(show)
    {
        this._showForm = show;
        this.enqueueToRender();
    }
    router() { return this._page.router(); }

    _mainSelectionDidChange(selection, didEndDrag)
    {
        super._mainSelectionDidChange(selection);
        if (didEndDrag)
            this.enqueueToRender();
    }

    didConstructShadowTree()
    {
        this.part('form').listenToAction('startTesting', (name, repetitionCount, repetitionType, commitSetMap, notifyOnCompletion) => {
            this.dispatchAction('newTestGroup', name, repetitionCount, repetitionType, commitSetMap, notifyOnCompletion);
        });
    }

    render()
    {
        super.render();
        const points = this._mainChart ? this._mainChart.selectedPoints('current') : null;

        this.content('form').style.display = this._showForm ? null : 'none';
        if (this._showForm) {
            const form = this.part('form');
            form.setCommitSetMap(points && points.length() >= 2 ? {'A': points.firstPoint().commitSet(), 'B': points.lastPoint().commitSet()} : null);
            form.enqueueToRender();
        }
    }

    static paneFooterTemplate() { return '<customizable-test-group-form id="form"></customizable-test-group-form>'; }

    static cssTemplate()
    {
        return super.cssTemplate() + `
            #form {
                margin: 0.5rem;
            }
        `;
    }
}

ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);

class AnalysisTaskResultsPane extends ComponentBase {
    constructor()
    {
        super('analysis-task-results-pane');
        this._showForm = false;
        this._repositoryList = [];
        this._renderRepositoryListLazily = new LazilyEvaluatedFunction(this._renderRepositoryList.bind(this));
        this._updateCommitViewerLazily = new LazilyEvaluatedFunction(this._updateCommitViewer.bind(this));
    }

    setPoints(startPoint, endPoint, metric)
    {
        const resultsViewer = this.part('results-viewer');
        this._repositoryList = startPoint ? Repository.sortByNamePreferringOnesWithURL(startPoint.commitSet().repositories()) : [];
        resultsViewer.setPoints(startPoint, endPoint, metric);
        resultsViewer.enqueueToRender();
    }

    setTestGroups(testGroups, currentGroup)
    {
        this.part('results-viewer').setTestGroups(testGroups, currentGroup);
        if (currentGroup)
            this.part('form').updateWithTestGroup(currentGroup);
        this.enqueueToRender();
    }

    setAnalysisResultsView(analysisResultsView)
    {
        this.part('results-viewer').setAnalysisResultsView(analysisResultsView);
        this.enqueueToRender();
    }

    setShowForm(show)
    {
        this._showForm = show;
        this.enqueueToRender();
    }

    didConstructShadowTree()
    {
        const resultsViewer = this.part('results-viewer');
        resultsViewer.listenToAction('testGroupClick', (testGroup) => {
            this.enqueueToRender();
            this.dispatchAction('showTestGroup', testGroup)
        });
        resultsViewer.setRangeSelectorLabels(['A', 'B']);
        resultsViewer.listenToAction('rangeSelectorClick', () => this.enqueueToRender());

        const repositoryPicker = this.content('commit-viewer-repository');
        repositoryPicker.addEventListener('change', () => this.enqueueToRender());
        this.part('commit-viewer').setShowRepositoryName(false);

        this.part('form').listenToAction('startTesting', (name, repetitionCount, repetitionType, commitSetMap, notifyOnCompletion) => {
            this.dispatchAction('newTestGroup', name, repetitionCount, repetitionType, commitSetMap, notifyOnCompletion);
        });
    }

    render()
    {
        const resultsViewer = this.part('results-viewer');

        const repositoryPicker = this._renderRepositoryListLazily.evaluate(this._repositoryList);
        const repository = Repository.findById(repositoryPicker.value);
        const range = resultsViewer.selectedRange();
        this._updateCommitViewerLazily.evaluate(repository, range['A'], range['B']);

        this.content('form').style.display = this._showForm ? null : 'none';
        if (!this._showForm)
            return;

        const selectedRange = this.part('results-viewer').selectedRange();
        const firstCommitSet = selectedRange['A'];
        const secondCommitSet = selectedRange['B'];
        const form = this.part('form');
        form.setCommitSetMap(firstCommitSet && secondCommitSet ? {'A': firstCommitSet, 'B': secondCommitSet} : null);
        form.enqueueToRender();
    }

    _renderRepositoryList(repositoryList)
    {
        const element = ComponentBase.createElement;
        const selectElement = this.content('commit-viewer-repository');
        this.renderReplace(selectElement,
            repositoryList.map((repository) => {
                return element('option', {value: repository.id()}, repository.label());
            }));
        return selectElement;
    }

    _updateCommitViewer(repository, preceedingCommitSet, lastCommitSet)
    {
        if (repository && preceedingCommitSet && lastCommitSet && !preceedingCommitSet.equals(lastCommitSet)) {
            const precedingRevision = preceedingCommitSet.revisionForRepository(repository);
            const lastRevision = lastCommitSet.revisionForRepository(repository);
            if (precedingRevision && lastRevision && precedingRevision != lastRevision) {
                this.part('commit-viewer').view(repository, precedingRevision, lastRevision);
                return;
            }
        }
        this.part('commit-viewer').view(null, null, null);
    }

    static htmlTemplate()
    {
        return `
            <div id="results-container">
                <div id="results-viewer-container">
                    <analysis-results-viewer id="results-viewer"></analysis-results-viewer>
                </div>
                <div id="commit-pane">
                    <select id="commit-viewer-repository"></select>
                    <commit-log-viewer id="commit-viewer"></commit-log-viewer>
                </div>
            </div>
            <customizable-test-group-form id="form"></customizable-test-group-form>
        `;
    }

    static cssTemplate()
    {
        return `
            #results-container {
                position: relative;
                text-align: center;
                padding-right: 20rem;
            }
            #results-viewer-container {
                overflow-x: scroll;
                overflow-y: hidden;
            }
            #commit-pane {
                position: absolute;
                width: 20rem;
                height: 100%;
                top: 0;
                right: 0;
            }
            #form {
                margin: 0.5rem;
            }
        `;
    }
}

ComponentBase.defineElement('analysis-task-results-pane', AnalysisTaskResultsPane);

class AnalysisTaskConfiguratorPane extends ComponentBase {
    constructor()
    {
        super('analysis-task-configurator-pane');
        this._currentGroup = null;
    }

    didConstructShadowTree()
    {
        const form = this.part('form');
        form.setHasTask(true);
        form.listenToAction('startTesting', (...args) => {
            this.dispatchAction('createCustomTestGroup', ...args);
        });
    }

    setTestGroups(testGroups, currentGroup)
    {
        this._currentGroup = currentGroup;
        const form = this.part('form');
        if (!form.hasCommitSets() && currentGroup)
            form.setConfigurations(currentGroup.test(), currentGroup.platform(), currentGroup.initialRepetitionCount(), currentGroup.requestedCommitSets());
        this.enqueueToRender();
    }

    render()
    {
        super.render();
    }

    static htmlTemplate()
    {
        return `<custom-configuration-test-group-form id="form"></custom-configuration-test-group-form>`;
    }

    static cssTemplate()
    {
        return `
            #form {
                margin: 1rem;
            }
        `;
    }
}

ComponentBase.defineElement('analysis-task-configurator-pane', AnalysisTaskConfiguratorPane);

class AnalysisTaskTestGroupPane extends ComponentBase {

    constructor()
    {
        super('analysis-task-test-group-pane');
        this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this._renderTestGroups.bind(this));
        this._renderTestGroupVisibilityLazily = new LazilyEvaluatedFunction(this._renderTestGroupVisibility.bind(this));
        this._renderTestGroupNamesLazily = new LazilyEvaluatedFunction(this._renderTestGroupNames.bind(this));
        this._renderCurrentTestGroupLazily = new LazilyEvaluatedFunction(this._renderCurrentTestGroup.bind(this));
        this._testGroupMap = new Map;
        this._testGroups = [];
        this._bisectingCommitSetByTestGroup = null;
        this._currentTestGroup = null;
        this._showHiddenGroups = false;
        this._allTestGroupIdSetForCurrentTask = null;
    }

    didConstructShadowTree()
    {
        this.content('hide-button').onclick = () => this.dispatchAction('toggleTestGroupVisibility', this._currentTestGroup);
        this.part('retry-form').listenToAction('startTesting', (repetitionCount, repetitionType, notifyOnCompletion) => {
            this.dispatchAction('retryTestGroup', this._currentTestGroup, repetitionCount, repetitionType, notifyOnCompletion);
        });
        this.part('bisect-form').listenToAction('startTesting', (repetitionCount, repetitionType, notifyOnCompletion) => {
            const bisectingCommitSet = this._bisectingCommitSetByTestGroup.get(this._currentTestGroup);
            const [oneCommitSet, anotherCommitSet] = this._currentTestGroup.requestedCommitSets();
            const commitSets = [oneCommitSet, bisectingCommitSet, anotherCommitSet];
            this.dispatchAction('bisectTestGroup', this._currentTestGroup, commitSets, repetitionCount, repetitionType, notifyOnCompletion);
        });
    }

    setTestGroups(testGroups, currentTestGroup, showHiddenGroups)
    {
        this._testGroups = testGroups;
        this._currentTestGroup = currentTestGroup;
        this._showHiddenGroups = showHiddenGroups;
        this.part('revision-table').setTestGroup(currentTestGroup);
        this.part('results-viewer').setTestGroup(currentTestGroup);

        const analysisTask = currentTestGroup.task();
        const allTestGroupIdsForCurrentTask = TestGroup.findAllByTask(analysisTask.id()).map((testGroup) => testGroup.id());
        const testGroupChanged = !this._allTestGroupIdSetForCurrentTask
            || this._allTestGroupIdSetForCurrentTask.size !== allTestGroupIdsForCurrentTask.length
            || !allTestGroupIdsForCurrentTask.every((testGroupId) => this._allTestGroupIdSetForCurrentTask.has(testGroupId));

        const computedForCurrentTestGroup = this._bisectingCommitSetByTestGroup && this._bisectingCommitSetByTestGroup.has(currentTestGroup);

        if (!testGroupChanged && computedForCurrentTestGroup) {
            this.enqueueToRender();
            return;
        }

        if (testGroupChanged) {
            this._bisectingCommitSetByTestGroup = new Map;
            this._allTestGroupIdSetForCurrentTask = new Set(allTestGroupIdsForCurrentTask);
        }

        analysisTask.commitSetsFromTestGroupsAndMeasurementSet().then(async (availableCommitSets) => {
            const commitSetClosestToMiddle = await CommitSetRangeBisector.commitSetClosestToMiddleOfAllCommits(currentTestGroup.requestedCommitSets(), availableCommitSets);
            this._bisectingCommitSetByTestGroup.set(currentTestGroup, commitSetClosestToMiddle);
            this.enqueueToRender();
        });
    }

    setAnalysisResults(analysisResults, metric)
    {
        this.part('revision-table').setAnalysisResults(analysisResults);
        this.part('results-viewer').setAnalysisResults(analysisResults, metric);
        this.enqueueToRender();
    }

    render()
    {
        this._renderTestGroupsLazily.evaluate(this._showHiddenGroups, ...this._testGroups);
        this._renderTestGroupVisibilityLazily.evaluate(...this._testGroups.map((group) => group.isHidden() ? 'hidden' : 'visible'));
        this._renderTestGroupNamesLazily.evaluate(...this._testGroups.map((group) => group.label()));
        this._renderCurrentTestGroup(this._currentTestGroup);
    }

    _renderTestGroups(showHiddenGroups, ...testGroups)
    {
        const element = ComponentBase.createElement;
        const link = ComponentBase.createLink;

        this._testGroupMap = new Map;
        const testGroupItems = testGroups.map((group) => {
            const text = new EditableText(group.label());
            text.listenToAction('update', () => this.dispatchAction('renameTestGroup', group, text.editedText()));

            const listItem = element('li', link(text, group.label(), () => this.dispatchAction('showTestGroup', group)));

            this._testGroupMap.set(group, {text, listItem});
            return listItem;
        });

        this.renderReplace(this.content('test-group-list'), [testGroupItems,
            showHiddenGroups ? [] : element('li', {class: 'test-group-list-show-all'}, link('Show hidden tests', () => {
                this.dispatchAction('showHiddenTestGroups');
            }))]);
    }

    _renderTestGroupVisibility(...groupVisibilities)
    {
        for (let i = 0; i < groupVisibilities.length; i++)
            this._testGroupMap.get(this._testGroups[i]).listItem.className = groupVisibilities[i];
    }

    _renderTestGroupNames(...groupNames)
    {
        for (let i = 0; i < groupNames.length; i++)
            this._testGroupMap.get(this._testGroups[i]).text.setText(groupNames[i]);
    }

    _renderCurrentTestGroup(currentGroup)
    {
        const selected = this.content('test-group-list').querySelector('.selected');
        if (selected)
            selected.classList.remove('selected');

        if (currentGroup) {
            this.part('retry-form').updateWithTestGroup(currentGroup);
            this.part('bisect-form').updateWithTestGroup(currentGroup);
            this._testGroupMap.get(currentGroup).listItem.classList.add('selected');

            if (currentGroup.hasRetries())
                this.renderReplace(this.content('retry-summary'), this._retrySummary(currentGroup));

            const authoredBy = currentGroup.author() ? `by "${currentGroup.author()}"` : '';
            this.renderReplace(this.content('request-summary'), `Scheduled ${authoredBy} at ${currentGroup.createdAt()}`)

            this.renderReplace(this.content('hide-button'), currentGroup.isHidden() ? 'Unhide' : 'Hide');

            this.renderReplace(this.content('pending-request-cancel-warning'),
                currentGroup.hasPending() ? '(cancels pending requests)' : []);
        }

        this.content('retry-form').style.display = currentGroup ? null : 'none';
        this.content('bisect-form').style.display = currentGroup && this._bisectingCommitSetByTestGroup.get(currentGroup) ? null : 'none';
    }

    _retrySummary(testGroup)
    {
        if (!testGroup.hasRetries())
            return '';

        if (testGroup.retryCountsAreSameForAllCommitSets()) {
            const retryCount = testGroup.retryCountForCommitSet(testGroup.requestedCommitSets()[0]);
            return `${testGroup.initialRepetitionCount()} requested, ${retryCount} added due to failures.`;
        }

        const retrySummaryParts = testGroup.requestedCommitSets()
            .map((commitSet) => ({commitSet, retryCount: testGroup.retryCountForCommitSet(commitSet)}))
            .filter((item) => item.retryCount)
            .map((item) => `${item.retryCount} added to ${testGroup.labelForCommitSet(item.commitSet)}`);

        const retrySummary = new Intl.ListFormat('en', {style: 'long', type: 'conjunction'}).format(retrySummaryParts);

        return `${testGroup.initialRepetitionCount()} requested, ${retrySummary} due to failures.`;
    }

    static htmlTemplate()
    {
        return `
            <ul id="test-group-list"></ul>
            <div id="test-group-details">
                <test-group-results-viewer id="results-viewer"></test-group-results-viewer>
                <test-group-revision-table id="revision-table"></test-group-revision-table>
                <div id="retry-summary" class="summary"></div>
                <div id="request-summary" class="summary"></div>
                <test-group-form id="retry-form">Retry</test-group-form>
                <test-group-form id="bisect-form">Bisect</test-group-form>
                <button id="hide-button">Hide</button>
                <span id="pending-request-cancel-warning">(cancels pending requests)</span>
            </div>`;
    }

    static cssTemplate()
    {
        return `
            :host {
                display: flex !important;
                font-size: 0.9rem;
            }

            #test-group-list {
                flex: none;
                margin: 0;
                padding: 0.2rem 0;
                list-style: none;
                border-right: solid 1px #ccc;
                white-space: nowrap;
                min-width: 8rem;
            }

            li {
                display: block;
                font-size: 0.9rem;
            }

            li > a {
                display: block;
                color: inherit;
                text-decoration: none;
                margin: 0;
                padding: 0.2rem;
            }

            li.test-group-list-show-all {
                font-size: 0.8rem;
                margin-top: 0.5rem;
                padding-right: 1rem;
                text-align: center;
                color: #999;
            }

            li.test-group-list-show-all:not(.selected) a:hover {
                background: inherit;
            }

            li.selected > a {
                background: rgba(204, 153, 51, 0.1);
            }

            li.hidden {
                color: #999;
            }

            li:not(.selected) > a:hover {
                background: #eee;
            }

            div.summary {
                padding-left: 1rem;
            }

            #test-group-details {
                display: table-cell;
                margin-bottom: 1rem;
                padding: 0 0.5rem;
                margin: 0;
            }

            #retry-form, #bisect-form {
                display: block;
                margin: 0.5rem;
            }

            #hide-button {
                margin: 0.5rem;
            }`;
    }
}

ComponentBase.defineElement('analysis-task-test-group-pane', AnalysisTaskTestGroupPane);

class AnalysisTaskPage extends PageWithHeading {
    constructor()
    {
        super('Analysis Task');
        this._renderTaskNameAndStatusLazily = new LazilyEvaluatedFunction(this._renderTaskNameAndStatus.bind(this));
        this._renderCauseAndFixesLazily = new LazilyEvaluatedFunction(this._renderCauseAndFixes.bind(this));
        this._renderRelatedTasksLazily = new LazilyEvaluatedFunction(this._renderRelatedTasks.bind(this));
        this._resetVariables();
    }

    title() { return this._task ? this._task.label() : 'Analysis Task'; }
    routeName() { return 'analysis/task'; }

    _resetVariables()
    {
        this._task = null;
        this._metric = null;
        this._triggerable = null;
        this._relatedTasks = null;
        this._testGroups = null;
        this._testGroupLabelMap = new Map;
        this._analysisResults = null;
        this._measurementSet = null;
        this._startPoint = null;
        this._endPoint = null;
        this._errorMessage = null;
        this._currentTestGroup = null;
        this._filteredTestGroups = null;
        this._showHiddenTestGroups = false;
    }

    updateFromSerializedState(state)
    {
        this._resetVariables();
        if (state.remainingRoute) {
            const taskId = parseInt(state.remainingRoute);
            AnalysisTask.fetchById(taskId).then(this._didFetchTask.bind(this)).then(() => {
                this._fetchRelatedInfoForTaskId(taskId);
            }, (error) => {
                this._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
                this.enqueueToRender();
            });
        } else if (state.buildRequest) {
            const buildRequestId = parseInt(state.buildRequest);
            AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then((task) => {
                this._fetchRelatedInfoForTaskId(task.id());
            }, (error) => {
                this._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
                this.enqueueToRender();
            });
        }
    }

    didConstructShadowTree()
    {
        this.part('analysis-task-name').listenToAction('update', () => this._updateTaskName(this.part('analysis-task-name').editedText()));

        this.content('change-type-form').onsubmit = ComponentBase.createEventHandler((event) => this._updateChangeType(event));

        this.part('chart-pane').listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));

        const resultsPane = this.part('results-pane');
        resultsPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
        resultsPane.listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));

        this.part('configurator-pane').listenToAction('createCustomTestGroup', this._createCustomTestGroup.bind(this));

        const groupPane = this.part('group-pane');
        groupPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
        groupPane.listenToAction('showHiddenTestGroups', () => this._showAllTestGroups());
        groupPane.listenToAction('renameTestGroup', (testGroup, newName) => this._updateTestGroupName(testGroup, newName));
        groupPane.listenToAction('toggleTestGroupVisibility', (testGroup) => this._hideCurrentTestGroup(testGroup));
        groupPane.listenToAction('retryTestGroup', (testGroup, repetitionCount, repetitionType, notifyOnCompletion) => this._retryCurrentTestGroup(testGroup, repetitionCount, repetitionType, notifyOnCompletion));
        groupPane.listenToAction('bisectTestGroup', (testGroup, commitSets, repetitionCount, repetitionType, notifyOnCompletion) => this._bisectCurrentTestGroup(testGroup, commitSets, repetitionCount, repetitionType, notifyOnCompletion));

        this.part('cause-list').listenToAction('addItem', (repository, revision) => {
            this._associateCommit('cause', repository, revision);
        });
        this.part('fix-list').listenToAction('addItem', (repository, revision) => {
            this._associateCommit('fix', repository, revision);
        });
    }

    _fetchRelatedInfoForTaskId(taskId)
    {
        TestGroup.fetchForTask(taskId).then(this._didFetchTestGroups.bind(this));
        AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
        AnalysisTask.fetchRelatedTasks(taskId).then((relatedTasks) => {
            this._relatedTasks = relatedTasks;
            this.enqueueToRender();
        });
    }

    _didFetchTask(task)
    {
        console.assert(!this._task);

        this._task = task;
        const bugList = this.part('bug-list');
        this.part('bug-list').setTask(this._task);
        this.enqueueToRender();

        if (task.isCustom())
            return task;

        const platform = task.platform();
        const metric = task.metric();
        const lastModified = platform.lastModified(metric);
        const triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
        this._triggerable = triggerable && !triggerable.isDisabled() ? triggerable : null;
        this._metric = metric;

        this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
        this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));

        const chart = this.part('chart-pane');
        const domain = ChartsPage.createDomainForAnalysisTask(task);
        chart.configure(platform.id(), metric.id());
        chart.setOverviewDomain(domain[0], domain[1]);
        chart.setMainDomain(domain[0], domain[1]);

        return task;
    }

    _didFetchMeasurement()
    {
        console.assert(this._task);
        console.assert(this._measurementSet);
        var series = this._measurementSet.fetchedTimeSeries('current', false, false);
        var startPoint = series.findById(this._task.startMeasurementId());
        var endPoint = series.findById(this._task.endMeasurementId());

        if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
            return;

        this.part('results-pane').setPoints(startPoint, endPoint, this._task.metric());

        this._startPoint = startPoint;
        this._endPoint = endPoint;
        this.enqueueToRender();
    }

    _didFetchTestGroups(testGroups)
    {
        this._testGroups = testGroups.sort(function (a, b) { return +a.createdAt() - b.createdAt(); });
        this._didUpdateTestGroupHiddenState();
        this._assignTestResultsIfPossible();
        this.enqueueToRender();
    }

    _showAllTestGroups()
    {
        this._showHiddenTestGroups = true;
        this._didUpdateTestGroupHiddenState();
        this.enqueueToRender();
    }

    _didUpdateTestGroupHiddenState()
    {
        if (!this._showHiddenTestGroups)
            this._filteredTestGroups = this._testGroups.filter(function (group) { return !group.isHidden(); });
        else
            this._filteredTestGroups = this._testGroups;
        this._showTestGroup(this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null);
    }

    _didFetchAnalysisResults(results)
    {
        this._analysisResults = results;
        if (this._assignTestResultsIfPossible())
            this.enqueueToRender();
    }

    _assignTestResultsIfPossible()
    {
        if (!this._task || !this._testGroups || !this._analysisResults)
            return false;

        let metric = this._metric;
        this.part('group-pane').setAnalysisResults(this._analysisResults, metric);
        if (metric) {
            const view = this._analysisResults.viewForMetric(metric);
            this.part('results-pane').setAnalysisResultsView(view);
        }

        return true;
    }

    render()
    {
        super.render();

        Instrumentation.startMeasuringTime('AnalysisTaskPage', 'render');

        this.content().querySelector('.error-message').textContent = this._errorMessage || '';

        this._renderTaskNameAndStatusLazily.evaluate(this._task, this._task ? this._task.name() : null, this._task ? this._task.changeType() : null);
        this._renderCauseAndFixesLazily.evaluate(this._startPoint, this._task, this.part('cause-list'), this._task ? this._task.causes() : []);
        this._renderCauseAndFixesLazily.evaluate(this._startPoint, this._task, this.part('fix-list'), this._task ? this._task.fixes() : []);
        this._renderRelatedTasksLazily.evaluate(this._task, this._relatedTasks);

        this.content('chart-pane').style.display = this._task && !this._task.isCustom() ? null : 'none';
        this.part('chart-pane').setShowForm(!!this._triggerable);

        this.content('results-pane').style.display = this._task && !this._task.isCustom() ? null : 'none';
        this.part('results-pane').setShowForm(!!this._triggerable);

        this.content('configurator-pane').style.display = this._task && this._task.isCustom() ? null : 'none';

        Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
    }

    _renderTaskNameAndStatus(task, taskName, changeType)
    {
        this.part('analysis-task-name').setText(taskName);
        if (task && !task.isCustom()) {
            const link = ComponentBase.createLink;
            const platform = task.platform();
            const metric = task.metric();
            const subtitle = `${metric.fullName()} on ${platform.label()}`;
            this.renderReplace(this.content('platform-metric-names'),
                link(subtitle, this.router().url('charts', ChartsPage.createStateForAnalysisTask(task))));
        }
        this.content('change-type').value = changeType || 'unconfirmed';
    }

    _renderRelatedTasks(task, relatedTasks)
    {
        const element = ComponentBase.createElement;
        const link = ComponentBase.createLink;
        this.renderReplace(this.content('related-tasks-list'), (task && relatedTasks ? relatedTasks : []).map((otherTask) => {
                let suffix = '';
                const taskLabel = otherTask.label();
                if (otherTask.metric() && otherTask.metric() != task.metric() && taskLabel.indexOf(otherTask.metric().label()) < 0)
                    suffix += ` with "${otherTask.metric().label()}"`;
                if (otherTask.platform() && otherTask.platform() != task.platform() && taskLabel.indexOf(otherTask.platform().label()) < 0)
                    suffix += ` on ${otherTask.platform().label()}`;
                return element('li', [link(taskLabel, this.router().url(`analysis/task/${otherTask.id()}`)), suffix]);
            }));
    }

    _renderCauseAndFixes(startPoint, task, list, commits)
    {
        const hasData = startPoint && task;
        this.content('cause-fix').style.display = hasData ? null : 'none';
        if (!hasData)
            return;

        const commitSet = startPoint.commitSet();
        const repositoryList = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());

        const makeItem = (commit) => {
            return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
                'Disassociate this commit', this._dissociateCommit.bind(this, commit));
        }

        list.setKindList(repositoryList);
        list.setList(commits.map((commit) => makeItem(commit)));
    }

    _showTestGroup(testGroup)
    {
        this._currentTestGroup = testGroup;
        this.part('configurator-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
        this.part('results-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
        const groupsInReverseChronology = this._filteredTestGroups.slice(0).reverse();
        const showHiddenGroups = !this._testGroups.some((group) => group.isHidden()) || this._showHiddenTestGroups;
        this.part('group-pane').setTestGroups(groupsInReverseChronology, this._currentTestGroup, showHiddenGroups);
        this.enqueueToRender();
    }

    _updateTaskName(newName)
    {
        console.assert(this._task);

        return this._task.updateName(newName).then(() => {
            this.enqueueToRender();
        }, (error) => {
            this.enqueueToRender();
            alert('Failed to update the name: ' + error);
        });
    }

    _updateTestGroupName(testGroup, newName)
    {
        return testGroup.updateName(newName).then(() => {
            this._showTestGroup(this._currentTestGroup);
            this.enqueueToRender();
        }, (error) => {
            this.enqueueToRender();
            alert('Failed to hide the test name: ' + error);
        });
    }

    _hideCurrentTestGroup(testGroup)
    {
        return testGroup.updateHiddenFlag(!testGroup.isHidden()).then(() => {
            this._didUpdateTestGroupHiddenState();
            this.enqueueToRender();
        }, function (error) {
            this._mayHaveMutatedTestGroupHiddenState();
            this.enqueueToRender();
            alert('Failed to update the group: ' + error);
        });
    }

    _updateChangeType(event)
    {
        event.preventDefault();
        console.assert(this._task);

        let newChangeType = this.content('change-type').value;
        if (newChangeType == 'unconfirmed')
            newChangeType = null;

        const updateRendering = () => {
            this.part('chart-pane').didUpdateAnnotations();
            this.enqueueToRender();
        };
        return this._task.updateChangeType(newChangeType).then(updateRendering, (error) => {
            updateRendering();
            alert('Failed to update the status: ' + error);
        });
    }

    _associateCommit(kind, repository, revision)
    {
        const updateRendering = () => { this.enqueueToRender(); };
        return this._task.associateCommit(kind, repository, revision).then(updateRendering, (error) => {
            updateRendering();
            if (error == 'AmbiguousRevision')
                alert('There are multiple revisions that match the specified string: ' + revision);
            else if (error == 'CommitNotFound')
                alert('There are no revisions that match the specified string:' + revision);
            else
                alert('Failed to associate the commit: ' + error);
        });
    }

    _dissociateCommit(commit)
    {
        const updateRendering = () => { this.enqueueToRender(); };
        return this._task.dissociateCommit(commit).then(updateRendering, (error) => {
            updateRendering();
            alert('Failed to dissociate the commit: ' + error);
        });
    }

    async _retryCurrentTestGroup(testGroup, repetitionCount, repetitionType, notifyOnCompletion)
    {
        const existingNames = (this._testGroups || []).map((group) => group.name());
        const newName = CommitSet.createNameWithoutCollision(testGroup.name(), new Set(existingNames));
        const commitSetList = testGroup.requestedCommitSets();
        const platform = this._task.platform() || testGroup.platform();
        try {
            const testGroups = await TestGroup.createWithCustomConfiguration(this._task, platform, testGroup.test(), newName,
                repetitionCount, repetitionType, commitSetList, notifyOnCompletion);
            this._didFetchTestGroups(testGroups);
        } catch (error) {
            alert('Failed to create a new test group: ' + error);
        }
    }

    async _bisectCurrentTestGroup(testGroup, commitSets, repetitionCount, repetitionType, notifyOnCompletion)
    {
        console.assert(testGroup.task());
        const existingTestGroupNames = new Set((this._testGroups || []).map((testGroup) => testGroup.name()));

        for (let i = 1; i < commitSets.length; i++) {
            const previousCommitSet = commitSets[i - 1];
            const currentCommitSet = commitSets[i];
            const testGroupName = CommitSet.createNameWithoutCollision(CommitSet.diff(previousCommitSet, currentCommitSet), existingTestGroupNames);
            try {
                const testGroups = await TestGroup.createAndRefetchTestGroups(testGroup.task(), testGroupName,
                    repetitionCount, repetitionType, [previousCommitSet, currentCommitSet], notifyOnCompletion);
                this._didFetchTestGroups(testGroups);
            } catch(error) {
                alert('Failed to create a new test group: ' + error);
                break;
            }
        }
    }

    async _createTestGroupAfterVerifyingCommitSetList(testGroupName, repetitionCount, repetitionType, commitSetMap, notifyOnCompletion)
    {
        if (this._hasDuplicateTestGroupName(testGroupName)) {
            alert(`There is already a test group named "${testGroupName}"`);
            return;
        }

        const firstLabel = Object.keys(commitSetMap)[0];
        const firstCommitSet = commitSetMap[firstLabel];

        for (let currentLabel in commitSetMap) {
            const commitSet = commitSetMap[currentLabel];
            for (let repository of commitSet.repositories()) {
                if (!firstCommitSet.revisionForRepository(repository))
                    return alert(`Set ${currentLabel} specifies ${repository.label()} but set ${firstLabel} does not.`);
            }
            for (let repository of firstCommitSet.repositories()) {
                if (!commitSet.revisionForRepository(repository))
                    return alert(`Set ${firstLabel} specifies ${repository.label()} but set ${currentLabel} does not.`);
            }
        }

        const commitSets = [];
        for (let label in commitSetMap)
            commitSets.push(commitSetMap[label]);

        try {
            const testGroups = await TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, repetitionType, commitSets, notifyOnCompletion);
            return this._didFetchTestGroups(testGroups);
        } catch (error) {
            alert('Failed to create a new test group: ' + error);
        }
    }

    async _createCustomTestGroup(testGroupName, repetitionCount, repetitionType, commitSets, platform, test, notifyOnCompletion)
    {
        console.assert(this._task.isCustom());
        if (this._hasDuplicateTestGroupName(testGroupName)) {
            alert(`There is already a test group named "${testGroupName}"`);
            return;
        }

        try {
            const testGroups = await TestGroup.createWithCustomConfiguration(this._task, platform, test, testGroupName,
                repetitionCount, repetitionType, commitSets, notifyOnCompletion);
            this._didFetchTestGroups(testGroups);
        } catch (error) {
            alert('Failed to create a new test group: ' + error);
        }
    }

    _hasDuplicateTestGroupName(name)
    {
        console.assert(this._testGroups);
        for (var group of this._testGroups) {
            if (group.name() == name)
                return true;
        }
        return false;
    }

    static htmlTemplate()
    {
        return `
            <div class="analysis-task-page">
                <h2 class="analysis-task-name"><editable-text id="analysis-task-name"></editable-text></h2>
                <h3 id="platform-metric-names"></h3>
                <p class="error-message"></p>
                <div class="analysis-task-status">
                    <section>
                        <h3>Status</h3>
                        <form id="change-type-form">
                            <select id="change-type">
                                <option value="unconfirmed">Unconfirmed</option>
                                <option value="regression">Definite regression</option>
                                <option value="progression">Definite progression</option>
                                <option value="inconclusive">Inconclusive (Closed)</option>
                                <option value="unchanged">No change (Closed)</option>
                            </select>
                            <button type="submit">Save</button>
                        </form>
                    </section>
                    <section class="associated-bugs">
                        <h3>Associated Bugs</h3>
                        <analysis-task-bug-list id="bug-list"></analysis-task-bug-list>
                    </section>
                    <section id="cause-fix">
                        <h3>Caused by</h3>
                        <mutable-list-view id="cause-list"></mutable-list-view>
                        <h3>Fixed by</h3>
                        <mutable-list-view id="fix-list"></mutable-list-view>
                    </section>
                    <section class="related-tasks">
                        <h3>Related Tasks</h3>
                        <ul id="related-tasks-list"></ul>
                    </section>
                </div>
                <analysis-task-chart-pane id="chart-pane"></analysis-task-chart-pane>
                <analysis-task-results-pane id="results-pane"></analysis-task-results-pane>
                <analysis-task-configurator-pane id="configurator-pane"></analysis-task-configurator-pane>
                <analysis-task-test-group-pane id="group-pane"></analysis-task-test-group-pane>
            </div>
`;
    }

    static cssTemplate()
    {
        return `
            .analysis-task-page {
            }

            .analysis-task-name {
                font-size: 1.2rem;
                font-weight: inherit;
                color: #c93;
                margin: 0 1rem;
                padding: 0;
            }

            #platform-metric-names {
                font-size: 1rem;
                font-weight: inherit;
                color: #c93;
                margin: 0 1rem;
                padding: 0;
            }

            #platform-metric-names a {
                text-decoration: none;
                color: inherit;
            }

            #platform-metric-names:empty {
                visibility: hidden;
                height: 0;
                width: 0;
                /* FIXME: Use display: none instead once r214290 is shipped everywhere */
            }

            .error-message:empty {
                visibility: hidden;
                height: 0;
                width: 0;
                /* FIXME: Use display: none instead once r214290 is shipped everywhere */
            }

            .error-message:not(:empty) {
                margin: 1rem;
                padding: 0;
            }

            #chart-pane,
            #results-pane {
                display: block;
                padding: 0 1rem;
                border-bottom: solid 1px #ccc;
            }

            #results-pane {
                margin-top: 1rem;
            }

            #group-pane {
                margin: 1rem;
                margin-bottom: 2rem;
            }

            .analysis-task-status {
                margin: 0;
                display: flex;
                padding-bottom: 1rem;
                margin-bottom: 1rem;
                border-bottom: solid 1px #ccc;
            }

            .analysis-task-status > section {
                flex-grow: 1;
                flex-shrink: 0;
                border-left: solid 1px #eee;
                padding-left: 1rem;
                padding-right: 1rem;
            }

            .analysis-task-status > section.related-tasks {
                flex-shrink: 1;
            }

            .analysis-task-status > section:first-child {
                border-left: none;
            }

            .analysis-task-status h3 {
                font-size: 1rem;
                font-weight: inherit;
                color: #c93;
            }

            .analysis-task-status ul,
            .analysis-task-status li {
                list-style: none;
                padding: 0;
                margin: 0;
            }

            .related-tasks-list {
                max-height: 10rem;
                overflow-y: scroll;
            }

            .test-configuration h3 {
                font-size: 1rem;
                font-weight: inherit;
                color: inherit;
                margin: 0 1rem;
                padding: 0;
            }`;
    }
}
