'use strict';

class AnalysisTask extends LabeledObject {
    constructor(id, object)
    {
        super(id, object);
        this._author = object.author;
        this._createdAt = object.createdAt;

        console.assert(!object.platform || object.platform instanceof Platform);
        this._platform = object.platform;

        console.assert(!object.metric || object.metric instanceof Metric);
        this._metric = object.metric;

        this._startMeasurementId = object.startRun;
        this._startTime = object.startRunTime;
        this._endMeasurementId = object.endRun;
        this._endTime = object.endRunTime;
        this._category = object.category;
        this._changeType = object.result; // Can't change due to v2 compatibility.
        this._needed = object.needed;
        this._bugs = object.bugs || [];
        this._causes = object.causes || [];
        this._fixes = object.fixes || [];
        this._buildRequestCount = +object.buildRequestCount;
        this._finishedBuildRequestCount = +object.finishedBuildRequestCount;
    }

    static findByPlatformAndMetric(platformId, metricId)
    {
        return this.all().filter((task) => {
            const platform = task._platform;
            const metric = task._metric;
            return platform && metric && platform.id() == platformId && metric.id() == metricId;
        });
    }

    updateSingleton(object)
    {
        super.updateSingleton(object);

        console.assert(this._author == object.author);
        console.assert(+this._createdAt == +object.createdAt);
        console.assert(this._platform == object.platform);
        console.assert(this._metric == object.metric);
        console.assert(this._startMeasurementId == object.startRun);
        console.assert(this._startTime == object.startRunTime);
        console.assert(this._endMeasurementId == object.endRun);
        console.assert(this._endTime == object.endRunTime);

        this._category = object.category;
        this._changeType = object.result; // Can't change due to v2 compatibility.
        this._needed = object.needed;
        this._bugs = object.bugs || [];
        this._causes = object.causes || [];
        this._fixes = object.fixes || [];
        this._buildRequestCount = +object.buildRequestCount;
        this._finishedBuildRequestCount = +object.finishedBuildRequestCount;
    }

    isCustom() { return !this._platform; }
    hasResults() { return this._finishedBuildRequestCount; }
    hasPendingRequests() { return this._finishedBuildRequestCount < this._buildRequestCount; }
    requestLabel() { return `${this._finishedBuildRequestCount} of ${this._buildRequestCount}`; }

    startMeasurementId() { return this._startMeasurementId; }
    startTime() { return this._startTime; }
    endMeasurementId() { return this._endMeasurementId; }
    endTime() { return this._endTime; }

    author() { return this._author || ''; }
    createdAt() { return this._createdAt; }
    bugs() { return this._bugs; }
    causes() { return this._causes; }
    fixes() { return this._fixes; }
    platform() { return this._platform; }
    metric() { return this._metric; }

    changeType() { return this._changeType; }

    updateName(newName) { return this._updateRemoteState({name: newName}); }
    updateChangeType(changeType) { return this._updateRemoteState({result: changeType}); }

    _updateRemoteState(param)
    {
        param.task = this.id();
        return PrivilegedAPI.sendRequest('update-analysis-task', param).then(function (data) {
            return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: param.task}, true)
                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
        });
    }

    associateBug(tracker, bugNumber)
    {
        console.assert(tracker instanceof BugTracker);
        console.assert(typeof(bugNumber) == 'number');
        var id = this.id();
        return PrivilegedAPI.sendRequest('associate-bug', {
            task: id,
            bugTracker: tracker.id(),
            number: bugNumber,
        }).then(function (data) {
            return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
        });
    }

    dissociateBug(bug)
    {
        console.assert(bug instanceof Bug);
        console.assert(this.bugs().includes(bug));
        var id = this.id();
        return PrivilegedAPI.sendRequest('associate-bug', {
            task: id,
            bugTracker: bug.bugTracker().id(),
            number: bug.bugNumber(),
            shouldDelete: true,
        }).then(function (data) {
            return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
        });
    }

    associateCommit(kind, repository, revision)
    {
        console.assert(kind == 'cause' || kind == 'fix');
        console.assert(repository instanceof Repository);
        if (revision.startsWith('r'))
            revision = revision.substring(1);
        const id = this.id();
        return PrivilegedAPI.sendRequest('associate-commit', {
            task: id,
            repository: repository.id(),
            revision: revision,
            kind: kind,
        }).then((data) => {
            return AnalysisTask.cachedFetch('/api/analysis-tasks', {id}, true)
                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
        });
    }

    dissociateCommit(commit)
    {
        console.assert(commit instanceof CommitLog);
        var id = this.id();
        return PrivilegedAPI.sendRequest('associate-commit', {
            task: id,
            commit: commit.id(),
        }).then(function (data) {
            return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
                .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
        });
    }

    category()
    {
        var category = 'unconfirmed';

        if (this._changeType == 'unchanged' || this._changeType == 'inconclusive'
            || (this._changeType == 'regression' && this._fixes.length)
            || (this._changeType == 'progression' && (this._causes.length || this._fixes.length)))
            category = 'closed';
        else if (this._causes.length || this._fixes.length || this._changeType == 'regression' || this._changeType == 'progression')
            category = 'investigated';

        return category;
    }

    async commitSetsFromTestGroupsAndMeasurementSet()
    {
        const platform = this.platform();
        const metric = this.metric();
        if (!platform || !metric)
            return [];

        const lastModified = platform.lastModified(metric);
        const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
        const fetchingMeasurementSetPromise = measurementSet.fetchBetween(this.startTime(), this.endTime());
        const commitSetsInSamePlatformGroupPromise = this._commitSetsInSamePlatformGroup();

        const allTestGroupsInTask = await TestGroup.fetchForTask(this.id());
        const allCommitSetsInTask = new Set;
        for (const group of allTestGroupsInTask)
            group.requestedCommitSets().forEach((commitSet) => allCommitSetsInTask.add(commitSet));

        await fetchingMeasurementSetPromise;

        const series = measurementSet.fetchedTimeSeries('current', false, false);
        const startPoint = series.findById(this.startMeasurementId());
        const endPoint = series.findById(this.endMeasurementId());
        const commitSetList = Array.from(series.viewBetweenPoints(startPoint, endPoint)).map((point) => point.commitSet());
        const arrayOfCommitSetList = await Promise.all(commitSetsInSamePlatformGroupPromise);

        return [...commitSetList.concat(...arrayOfCommitSetList), ...allCommitSetsInTask];
    }

    _commitSetsInSamePlatformGroup()
    {
        const metric = this.metric();
        if (!metric || !this.platform() || !this.platform().group())
            return [];
        const otherPlatforms = this.platform().group().platforms().filter((platform) => platform != this.platform());
        return otherPlatforms.map(async (platform) => {
            if (!platform.hasMetric(metric))
                return [];
            const lastModified = platform.lastModified(metric);
            const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
            await measurementSet.fetchBetween(this.startTime(), this.endTime());
            const series = measurementSet.fetchedTimeSeries('current', false, false);
            const timeSeriesView = series.viewBetweenTime(this.startTime(), this.endTime());

            return timeSeriesView ? Array.from(timeSeriesView).map((point) => point.commitSet()) : [];
        });
    }

    static categories()
    {
        return [
            'unconfirmed',
            'investigated',
            'closed'
        ];
    }

    static fetchById(id, noCache)
    {
        return this._fetchSubset({id: id}, noCache).then((data) => AnalysisTask.findById(id));
    }

    static fetchByBuildRequestId(id)
    {
        return this._fetchSubset({buildRequest: id}).then(function (tasks) { return tasks[0]; });
    }

    static fetchByPlatformAndMetric(platformId, metricId, noCache)
    {
        return this._fetchSubset({platform: platformId, metric: metricId}, noCache).then(function (data) {
            return AnalysisTask.findByPlatformAndMetric(platformId, metricId);
        });
    }

    static fetchRelatedTasks(taskId)
    {
        // FIXME: We should add new sever-side API to just fetch the related tasks.
        return this.fetchAll().then(function () {
            var task = AnalysisTask.findById(taskId);
            if (!task)
                return undefined;
            var relatedTasks = new Set;
            for (var bug of task.bugs()) {
                for (var otherTask of AnalysisTask.all()) {
                    if (otherTask.bugs().includes(bug))
                        relatedTasks.add(otherTask);
                }
            }
            for (var otherTask of AnalysisTask.all()) {
                if (task.isCustom())
                    continue;
                if (task.endTime() < otherTask.startTime()
                    || otherTask.endTime() < task.startTime()
                    || task.metric() != otherTask.metric())
                    continue;
                relatedTasks.add(otherTask);
            }
            return Array.from(relatedTasks);
        });
    }

    static _fetchSubset(params, noCache)
    {
        if (this._fetchAllPromise && !noCache)
            return this._fetchAllPromise;
        return this.cachedFetch('/api/analysis-tasks', params, noCache).then(this._constructAnalysisTasksFromRawData.bind(this));
    }

    static fetchAll()
    {
        if (!this._fetchAllPromise)
            this._fetchAllPromise = RemoteAPI.getJSONWithStatus('/api/analysis-tasks').then(this._constructAnalysisTasksFromRawData.bind(this));
        return this._fetchAllPromise;
    }

    static _constructAnalysisTasksFromRawData(data)
    {
        Instrumentation.startMeasuringTime('AnalysisTask', 'construction');

        // FIXME: The backend shouldn't create a separate bug row per task for the same bug number.
        var taskToBug = {};
        for (var rawData of data.bugs) {
            rawData.bugTracker = BugTracker.findById(rawData.bugTracker);
            if (!rawData.bugTracker)
                continue;

            var bug = Bug.ensureSingleton(rawData);
            if (!taskToBug[rawData.task])
                taskToBug[rawData.task] = [];
            taskToBug[rawData.task].push(bug);
        }

        for (var rawData of data.commits) {
            rawData.repository = Repository.findById(rawData.repository);
            if (!rawData.repository)
                continue;
            CommitLog.ensureSingleton(rawData.id, rawData);
        }

        function resolveCommits(commits) {
            return commits.map(function (id) { return CommitLog.findById(id); }).filter(function (commit) { return !!commit; });
        }

        var results = [];
        for (var rawData of data.analysisTasks) {
            rawData.platform = Platform.findById(rawData.platform);
            rawData.metric = Metric.findById(rawData.metric);
            rawData.bugs = taskToBug[rawData.id];
            rawData.causes = resolveCommits(rawData.causes);
            rawData.fixes = resolveCommits(rawData.fixes);
            results.push(AnalysisTask.ensureSingleton(rawData.id, rawData));
        }

        Instrumentation.endMeasuringTime('AnalysisTask', 'construction');

        return results;
    }

    static async create(name, startPoint, endPoint, testGroupName = null, repetitionCount = 0, repetitionType = 'alternating', notifyOnCompletion = false)
    {
        const parameters = {name, startRun: startPoint.id, endRun: endPoint.id};
        if (testGroupName) {
            console.assert(repetitionCount);
            parameters['revisionSets'] = CommitSet.revisionSetsFromCommitSets([startPoint.commitSet(), endPoint.commitSet()]);
            parameters['repetitionCount'] = repetitionCount;
            parameters['testGroupName'] = testGroupName;
            parameters['needsNotification'] = notifyOnCompletion;
            parameters['repetitionType'] = repetitionType;
        }
        const response = await PrivilegedAPI.sendRequest('create-analysis-task', parameters);
        return AnalysisTask.fetchById(response.taskId, true);
    }
}

if (typeof module != 'undefined')
    module.exports.AnalysisTask = AnalysisTask;
