<?php

/**
 * @defgroup submission_reviewRound Review Round
 */
/**
 * @file classes/submission/reviewRound/ReviewRound.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2003-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class ReviewRound
 *
 * @ingroup submission_reviewRound
 *
 * @see ReviewRoundDAO
 *
 * @brief Basic class describing a review round.
 */

namespace PKP\submission\reviewRound;

use APP\decision\Decision;
use APP\facades\Repo;
use PKP\db\DAORegistry;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;

class ReviewRound extends \PKP\core\DataObject
{
    // The first four statuses are set explicitly by Decisions, which override
    // the current status.
    public const REVIEW_ROUND_STATUS_REVISIONS_REQUESTED = 1;
    public const REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW = 2;
    public const REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL = 3;
    public const REVIEW_ROUND_STATUS_ACCEPTED = 4;
    public const REVIEW_ROUND_STATUS_DECLINED = 5;

    // The following statuses are calculated based on the statuses of ReviewAssignments
    // in this round.
    public const REVIEW_ROUND_STATUS_PENDING_REVIEWERS = 6; // No reviewers have been assigned
    public const REVIEW_ROUND_STATUS_PENDING_REVIEWS = 7; // Waiting for reviews to be submitted by reviewers
    public const REVIEW_ROUND_STATUS_REVIEWS_READY = 8; // One or more reviews is ready for an editor to view
    public const REVIEW_ROUND_STATUS_REVIEWS_COMPLETED = 9; // All assigned reviews have been confirmed by an editor
    public const REVIEW_ROUND_STATUS_REVIEWS_OVERDUE = 10; // One or more reviews is overdue
    // The following status is calculated when the round is in ReviewRound::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED and
    // at least one revision file has been uploaded.
    public const REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED = 11;

    // The following statuses are calculated based on the statuses of recommendOnly EditorAssignments
    // and their decisions in this round.
    public const REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS = 12; // Waiting for recommendations to be submitted by recommendOnly editors
    public const REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY = 13; // One or more recommendations are ready for an editor to view
    public const REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED = 14; // All assigned recommendOnly editors have made a recommendation

    // The following status is calculated when the round is in ReviewRound::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW and
    // at least one revision file has been uploaded.
    public const REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED = 15;

    // The following status is set when a submission return back from copyediting stage to last review round again
    public const REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW = 16;

    //
    // Get/set methods
    //

    /**
     * get submission id
     *
     * @return int
     */
    public function getSubmissionId()
    {
        return $this->getData('submissionId');
    }

    /**
     * set submission id
     *
     * @param int $submissionId
     */
    public function setSubmissionId($submissionId)
    {
        $this->setData('submissionId', $submissionId);
    }

    /**
     * Get review stage id (internal or external review).
     *
     * @return int
     */
    public function getStageId()
    {
        return $this->getData('stageId');
    }

    /**
     * Set review stage id
     *
     * @param int $stageId
     */
    public function setStageId($stageId)
    {
        $this->setData('stageId', $stageId);
    }

    /**
     * Get review round
     *
     * @return int
     */
    public function getRound()
    {
        return $this->getData('round');
    }

    /**
     * Set review round
     */
    public function setRound($round)
    {
        $this->setData('round', $round);
    }

    /**
     * Get current round status
     *
     * @return int
     */
    public function getStatus()
    {
        return $this->getData('status');
    }

    /**
     * Set current round status
     *
     * @param int $status
     */
    public function setStatus($status)
    {
        $this->setData('status', $status);
    }

    /**
     * Calculate the status of this review round.
     *
     * If the round is in revisions, it will search for revision files and set
     * the status accordingly. If the round has not reached a revision status
     * yet, it will determine the status based on the statuses of the round's
     * ReviewAssignments.
     *
     * @return int
     */
    public function determineStatus()
    {
        // If revisions have been requested, check to see if any have been
        // submitted
        if ($this->getStatus() == self::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED || $this->getStatus() == self::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED) {
            // get editor decisions
            $decisionToCheck = $this->getStageId() === WORKFLOW_STAGE_ID_EXTERNAL_REVIEW ? Decision::PENDING_REVISIONS : Decision::PENDING_REVISIONS_INTERNAL;
            $pendingRevisionDecision = Repo::decision()->getActivePendingRevisionsDecision($this->getSubmissionId(), $this->getStageId(), $decisionToCheck);

            if ($pendingRevisionDecision) {
                if (Repo::decision()->revisionsUploadedSinceDecision($pendingRevisionDecision, $this->getSubmissionId())) {
                    return self::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED;
                }
            }
            return self::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED;
        }

        // If revisions have been requested for re-submission, check to see if any have been
        // submitted
        if ($this->getStatus() == self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW || $this->getStatus() == self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED) {
            // get editor decisions
            $decisionToCheck = $this->getStageId() === WORKFLOW_STAGE_ID_EXTERNAL_REVIEW ? Decision::RESUBMIT : Decision::RESUBMIT_INTERNAL;

            $pendingRevisionDecision = Repo::decision()->getActivePendingRevisionsDecision($this->getSubmissionId(), $this->getStageId(), $decisionToCheck);

            if ($pendingRevisionDecision) {
                if (Repo::decision()->revisionsUploadedSinceDecision($pendingRevisionDecision, $this->getSubmissionId())) {
                    return self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED;
                }
            }
            return self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW;
        }

        $statusFinished = in_array(
            $this->getStatus(),
            [
                self::REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL,
                self::REVIEW_ROUND_STATUS_ACCEPTED,
                self::REVIEW_ROUND_STATUS_DECLINED
            ]
        );
        if ($statusFinished) {
            return $this->getStatus();
        }

        // Determine the round status by looking at the recommendOnly editor assignment statuses
        $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
        $pendingRecommendations = false;
        $recommendationsFinished = true;
        $recommendationsReady = false;
        $editorsStageAssignments = $stageAssignmentDao->getEditorsAssignedToStage($this->getSubmissionId(), $this->getStageId());
        foreach ($editorsStageAssignments as $editorsStageAssignment) {
            if ($editorsStageAssignment->getRecommendOnly()) {
                $pendingRecommendations = true;
                // Get recommendation from the assigned recommendOnly editor
                $decisions = Repo::decision()->getCollector()
                    ->filterBySubmissionIds([$this->getSubmissionId()])
                    ->filterByStageIds([$this->getStageId()])
                    ->filterByReviewRoundIds([$this->getId()])
                    ->filterByEditorIds([$editorsStageAssignment->getUserId()])
                    ->getCount();

                if (!$decisions) {
                    $recommendationsFinished = false;
                } else {
                    $recommendationsReady = true;
                }
            }
        }
        if ($pendingRecommendations) {
            if ($recommendationsFinished) {
                return self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED;
            } elseif ($recommendationsReady) {
                return self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY;
            }
        }

        // Determine the round status by looking at the assignment statuses
        $anyOverdueReview = false;
        $anyIncompletedReview = false;
        $anyUnreadReview = false;
        $reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
        $reviewAssignments = $reviewAssignmentDao->getByReviewRoundId($this->getId());
        foreach ($reviewAssignments as $reviewAssignment) {
            assert($reviewAssignment instanceof ReviewAssignment);

            $assignmentStatus = $reviewAssignment->getStatus();

            switch ($assignmentStatus) {
                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_DECLINED:
                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_CANCELLED:
                    break;

                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE:
                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE:
                    $anyOverdueReview = true;
                    break;

                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE:
                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND:
                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_ACCEPTED:
                    $anyIncompletedReview = true;
                    break;

                case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_RECEIVED:
                    $anyUnreadReview = true;
                    break;
            }
        }

        // Find the correct review round status based on the state of
        // the current review assignments. The check order matters: the
        // first conditions override the others.
        if (empty($reviewAssignments)) {
            return self::REVIEW_ROUND_STATUS_PENDING_REVIEWERS;
        } elseif ($anyOverdueReview) {
            return self::REVIEW_ROUND_STATUS_REVIEWS_OVERDUE;
        } elseif ($anyUnreadReview) {
            return self::REVIEW_ROUND_STATUS_REVIEWS_READY;
        } elseif ($anyIncompletedReview) {
            return self::REVIEW_ROUND_STATUS_PENDING_REVIEWS;
        } elseif ($pendingRecommendations) {
            return self::REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS;
        }

        // The submission back form copy editing stage to last review round
        if ($this->getStatus() == self::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW) {
            return self::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW;
        }
        
        return self::REVIEW_ROUND_STATUS_REVIEWS_COMPLETED;
    }

    /**
     * Get locale key associated with current status
     *
     * @param bool $isAuthor True iff the status is to be shown to the author (slightly tweaked phrasing)
     *
     * @return string
     */
    public function getStatusKey($isAuthor = false)
    {
        switch ($this->determineStatus()) {
            case self::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED:
                return 'editor.submission.roundStatus.revisionsRequested';
            case self::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED:
                return 'editor.submission.roundStatus.revisionsSubmitted';
            case self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW:
                return 'editor.submission.roundStatus.resubmitForReview';
            case self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED:
                return 'editor.submission.roundStatus.submissionResubmitted';
            case self::REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL:
                return 'editor.submission.roundStatus.sentToExternal';
            case self::REVIEW_ROUND_STATUS_ACCEPTED:
                return 'editor.submission.roundStatus.accepted';
            case self::REVIEW_ROUND_STATUS_DECLINED:
                return 'editor.submission.roundStatus.declined';
            case self::REVIEW_ROUND_STATUS_PENDING_REVIEWERS:
                return 'editor.submission.roundStatus.pendingReviewers';
            case self::REVIEW_ROUND_STATUS_PENDING_REVIEWS:
                return 'editor.submission.roundStatus.pendingReviews';
            case self::REVIEW_ROUND_STATUS_REVIEWS_READY:
                return $isAuthor ? 'author.submission.roundStatus.reviewsReady' : 'editor.submission.roundStatus.reviewsReady';
            case self::REVIEW_ROUND_STATUS_REVIEWS_COMPLETED:
                return 'editor.submission.roundStatus.reviewsCompleted';
            case self::REVIEW_ROUND_STATUS_REVIEWS_OVERDUE:
                return $isAuthor ? 'author.submission.roundStatus.reviewOverdue' : 'editor.submission.roundStatus.reviewOverdue';
            case self::REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS:
                return 'editor.submission.roundStatus.pendingRecommendations';
            case self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY:
                return 'editor.submission.roundStatus.recommendationsReady';
            case self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED:
                return 'editor.submission.roundStatus.recommendationsCompleted';
            case self::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW:
                return 'editor.submission.roundStatus.returnedToReview';
            default: return null;
        }
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\submission\reviewRound\ReviewRound', '\ReviewRound');
    foreach ([
        'REVIEW_ROUND_STATUS_REVISIONS_REQUESTED',
        'REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW',
        'REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL',
        'REVIEW_ROUND_STATUS_ACCEPTED',
        'REVIEW_ROUND_STATUS_DECLINED',
        'REVIEW_ROUND_STATUS_PENDING_REVIEWERS',
        'REVIEW_ROUND_STATUS_PENDING_REVIEWS',
        'REVIEW_ROUND_STATUS_REVIEWS_READY',
        'REVIEW_ROUND_STATUS_REVIEWS_COMPLETED',
        'REVIEW_ROUND_STATUS_REVIEWS_OVERDUE',
        'REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED',
        'REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS',
        'REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY',
        'REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED',
        'REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED',
    ] as $constantName) {
        define($constantName, constant('\ReviewRound::' . $constantName));
    }
}
