// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT

const store = require('store');

const PluginRegistry = require('./plugins');
const Issue = require('./issue');
const User = require('./user');
const { ArgumentError, DataError } = require('./exceptions');
const { ReviewStatus } = require('./enums');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy');

/**
 * Class representing a single review
 * @memberof module:API.cvat.classes
 * @hideconstructor
 */
class Review {
    constructor(initialData) {
        const data = {
            id: undefined,
            job: undefined,
            issue_set: [],
            estimated_quality: undefined,
            status: undefined,
            reviewer: undefined,
            assignee: undefined,
            reviewed_frames: undefined,
            reviewed_states: undefined,
        };

        for (const property in data) {
            if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
                data[property] = initialData[property];
            }
        }

        if (data.reviewer && !(data.reviewer instanceof User)) data.reviewer = new User(data.reviewer);
        if (data.assignee && !(data.assignee instanceof User)) data.assignee = new User(data.assignee);

        data.reviewed_frames = Array.isArray(data.reviewed_frames) ? new Set(data.reviewed_frames) : new Set();
        data.reviewed_states = Array.isArray(data.reviewed_states) ? new Set(data.reviewed_states) : new Set();
        if (data.issue_set) {
            data.issue_set = data.issue_set.map((issue) => new Issue(issue));
        }

        if (typeof data.id === 'undefined') {
            data.id = negativeIDGenerator();
        }

        Object.defineProperties(
            this,
            Object.freeze({
                /**
                 * @name id
                 * @type {integer}
                 * @memberof module:API.cvat.classes.Review
                 * @readonly
                 * @instance
                 */
                id: {
                    get: () => data.id,
                },
                /**
                 * An identifier of a job the review is attached to
                 * @name job
                 * @type {integer}
                 * @memberof module:API.cvat.classes.Review
                 * @readonly
                 * @instance
                 */
                job: {
                    get: () => data.job,
                },
                /**
                 * List of attached issues
                 * @name issues
                 * @type {number[]}
                 * @memberof module:API.cvat.classes.Review
                 * @instance
                 * @readonly
                 */
                issues: {
                    get: () => data.issue_set.filter((issue) => !issue.removed),
                },
                /**
                 * Estimated quality of the review
                 * @name estimatedQuality
                 * @type {number}
                 * @memberof module:API.cvat.classes.Review
                 * @instance
                 * @readonly
                 * @throws {module:API.cvat.exceptions.ArgumentError}
                 */
                estimatedQuality: {
                    get: () => data.estimated_quality,
                    set: (value) => {
                        if (typeof value !== 'number' || value < 0 || value > 5) {
                            throw new ArgumentError(`Value must be a number in range [0, 5]. Got ${value}`);
                        }
                        data.estimated_quality = value;
                    },
                },
                /**
                 * @name status
                 * @type {module:API.cvat.enums.ReviewStatus}
                 * @memberof module:API.cvat.classes.Review
                 * @instance
                 * @readonly
                 * @throws {module:API.cvat.exceptions.ArgumentError}
                 */
                status: {
                    get: () => data.status,
                    set: (status) => {
                        const type = ReviewStatus;
                        let valueInEnum = false;
                        for (const value in type) {
                            if (type[value] === status) {
                                valueInEnum = true;
                                break;
                            }
                        }

                        if (!valueInEnum) {
                            throw new ArgumentError(
                                'Value must be a value from the enumeration cvat.enums.ReviewStatus',
                            );
                        }

                        data.status = status;
                    },
                },
                /**
                 * An instance of a user who has done the review
                 * @name reviewer
                 * @type {module:API.cvat.classes.User}
                 * @memberof module:API.cvat.classes.Review
                 * @readonly
                 * @instance
                 * @throws {module:API.cvat.exceptions.ArgumentError}
                 */
                reviewer: {
                    get: () => data.reviewer,
                    set: (reviewer) => {
                        if (!(reviewer instanceof User)) {
                            throw new ArgumentError(`Reviewer must be an instance of the User class. Got ${reviewer}`);
                        }

                        data.reviewer = reviewer;
                    },
                },
                /**
                 * An instance of a user who was assigned for annotation before the review
                 * @name assignee
                 * @type {module:API.cvat.classes.User}
                 * @memberof module:API.cvat.classes.Review
                 * @readonly
                 * @instance
                 */
                assignee: {
                    get: () => data.assignee,
                },
                /**
                 * A set of frames that have been visited during review
                 * @name reviewedFrames
                 * @type {number[]}
                 * @memberof module:API.cvat.classes.Review
                 * @readonly
                 * @instance
                 */
                reviewedFrames: {
                    get: () => Array.from(data.reviewed_frames),
                },
                /**
                 * A set of reviewed states (server IDs combined with frames)
                 * @name reviewedFrames
                 * @type {string[]}
                 * @memberof module:API.cvat.classes.Review
                 * @readonly
                 * @instance
                 */
                reviewedStates: {
                    get: () => Array.from(data.reviewed_states),
                },
                __internal: {
                    get: () => data,
                },
            }),
        );
    }

    /**
     * Method appends a frame to a set of reviewed frames
     * Reviewed frames are saved only in local storage
     * @method reviewFrame
     * @memberof module:API.cvat.classes.Review
     * @param {number} frame
     * @readonly
     * @instance
     * @async
     * @throws {module:API.cvat.exceptions.ArgumentError}
     * @throws {module:API.cvat.exceptions.PluginError}
     */
    async reviewFrame(frame) {
        const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewFrame, frame);
        return result;
    }

    /**
     * Method appends a frame to a set of reviewed frames
     * Reviewed states are saved only in local storage. They are used to automatic annotations quality assessment
     * @method reviewStates
     * @memberof module:API.cvat.classes.Review
     * @param {string[]} stateIDs
     * @readonly
     * @instance
     * @async
     * @throws {module:API.cvat.exceptions.ArgumentError}
     * @throws {module:API.cvat.exceptions.PluginError}
     */
    async reviewStates(stateIDs) {
        const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewStates, stateIDs);
        return result;
    }

    /**
     * @typedef {Object} IssueData
     * @property {number} frame
     * @property {number[]} position
     * @property {number} owner
     * @property {CommentData[]} comment_set
     * @global
     */
    /**
     * Method adds a new issue to the review
     * @method openIssue
     * @memberof module:API.cvat.classes.Review
     * @param {IssueData} data
     * @returns {module:API.cvat.classes.Issue}
     * @readonly
     * @instance
     * @async
     * @throws {module:API.cvat.exceptions.ArgumentError}
     * @throws {module:API.cvat.exceptions.PluginError}
     */
    async openIssue(data) {
        const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.openIssue, data);
        return result;
    }

    /**
     * Method submits local review to the server
     * @method submit
     * @memberof module:API.cvat.classes.Review
     * @readonly
     * @instance
     * @async
     * @throws {module:API.cvat.exceptions.DataError}
     * @throws {module:API.cvat.exceptions.PluginError}
     */
    async submit() {
        const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.submit);
        return result;
    }

    serialize() {
        const { issues, reviewedFrames, reviewedStates } = this;
        const data = {
            job: this.job,
            issue_set: issues.map((issue) => issue.serialize()),
            reviewed_frames: Array.from(reviewedFrames),
            reviewed_states: Array.from(reviewedStates),
        };

        if (this.id > 0) {
            data.id = this.id;
        }
        if (typeof this.estimatedQuality !== 'undefined') {
            data.estimated_quality = this.estimatedQuality;
        }
        if (typeof this.status !== 'undefined') {
            data.status = this.status;
        }
        if (this.reviewer) {
            data.reviewer = this.reviewer.toJSON();
        }
        if (this.assignee) {
            data.reviewer = this.assignee.toJSON();
        }

        return data;
    }

    toJSON() {
        const data = this.serialize();
        const {
            reviewer,
            assignee,
            reviewed_frames: reviewedFrames,
            reviewed_states: reviewedStates,
            ...updated
        } = data;

        return {
            ...updated,
            issue_set: this.issues.map((issue) => issue.toJSON()),
            reviewer_id: reviewer ? reviewer.id : undefined,
            assignee_id: assignee ? assignee.id : undefined,
        };
    }

    async toLocalStorage() {
        const data = this.serialize();
        store.set(`job-${this.job}-review`, JSON.stringify(data));
    }
}

Review.prototype.reviewFrame.implementation = function (frame) {
    if (!Number.isInteger(frame)) {
        throw new ArgumentError(`The argument "frame" is expected to be an integer. Got ${frame}`);
    }
    this.__internal.reviewed_frames.add(frame);
};

Review.prototype.reviewStates.implementation = function (stateIDs) {
    if (!Array.isArray(stateIDs) || stateIDs.some((stateID) => typeof stateID !== 'string')) {
        throw new ArgumentError(`The argument "stateIDs" is expected to be an array of string. Got ${stateIDs}`);
    }

    stateIDs.forEach((stateID) => this.__internal.reviewed_states.add(stateID));
};

Review.prototype.openIssue.implementation = async function (data) {
    if (typeof data !== 'object' || data === null) {
        throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
    }

    if (typeof data.frame !== 'number') {
        throw new ArgumentError(`Issue frame must be a number. Got ${data.frame}`);
    }

    if (!(data.owner instanceof User)) {
        throw new ArgumentError(`Issue owner must be a User instance. Got ${data.owner}`);
    }

    if (!Array.isArray(data.position) || data.position.some((coord) => typeof coord !== 'number')) {
        throw new ArgumentError(`Issue position must be an array of numbers. Got ${data.position}`);
    }

    if (!Array.isArray(data.comment_set)) {
        throw new ArgumentError(`Issue comment set must be an array. Got ${data.comment_set}`);
    }

    const copied = {
        frame: data.frame,
        position: Issue.hull(data.position),
        owner: data.owner,
        comment_set: [],
    };

    const issue = new Issue(copied);

    for (const comment of data.comment_set) {
        await issue.comment.implementation.call(issue, comment);
    }

    this.__internal.issue_set.push(issue);
    return issue;
};

Review.prototype.submit.implementation = async function () {
    if (typeof this.estimatedQuality === 'undefined') {
        throw new DataError('Estimated quality is expected to be a number. Got "undefined"');
    }

    if (typeof this.status === 'undefined') {
        throw new DataError('Review status is expected to be a string. Got "undefined"');
    }

    if (this.id < 0) {
        const data = this.toJSON();

        const response = await serverProxy.jobs.reviews.create(data);
        store.remove(`job-${this.job}-review`);
        this.__internal.id = response.id;
        this.__internal.issue_set = response.issue_set.map((issue) => new Issue(issue));
        this.__internal.estimated_quality = response.estimated_quality;
        this.__internal.status = response.status;

        if (response.reviewer) this.__internal.reviewer = new User(response.reviewer);
        if (response.assignee) this.__internal.assignee = new User(response.assignee);
    }
};

module.exports = Review;
