<?php
/**
 * @file classes/submission/maps/Schema.php
 *
 * Copyright (c) 2014-2020 Simon Fraser University
 * Copyright (c) 2000-2020 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class Schema
 *
 * @brief Map submissions to the properties defined in the submission schema
 */

namespace PKP\submission\maps;

use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use PKP\db\DAORegistry;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\query\QueryDAO;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\Genre;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
use PKP\userGroup\UserGroup;
use PKP\workflow\WorkflowStageDAO;

class Schema extends \PKP\core\maps\Schema
{
    /** @copydoc \PKP\core\maps\Schema::$collection */
    public Enumerable $collection;

    /** @copydoc \PKP\core\maps\Schema::$schema */
    public string $schema = PKPSchemaService::SCHEMA_SUBMISSION;

    /** @var LazyCollection<int,UserGroup> The user groups for this context. */
    public LazyCollection $userGroups;

    /** @var Genre[] The file genres in this context. */
    public array $genres;

    /**
     * Get extra property names used in the submissions list
     */
    protected function getSubmissionsListProps(): array
    {
        PluginRegistry::loadCategory('pubIds', true);

        $props = [
            '_href',
            'contextId',
            'currentPublicationId',
            'dateLastActivity',
            'dateSubmitted',
            'id',
            'lastModified',
            'publications',
            'reviewAssignments',
            'reviewRounds',
            'stageId',
            'stages',
            'status',
            'statusLabel',
            'submissionProgress',
            'urlAuthorWorkflow',
            'urlEditorialWorkflow',
            'urlWorkflow',
            'urlPublished',
        ];

        Hook::call('Submission::getSubmissionsListProps', [&$props]);

        return $props;
    }

    /**
     * Map a submission
     *
     * Includes all properties in the submission schema.
     *
     * @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
     * @param Genre[] $genres The file genres in this context
     */
    public function map(Submission $item, LazyCollection $userGroups, array $genres): array
    {
        $this->userGroups = $userGroups;
        $this->genres = $genres;
        return $this->mapByProperties($this->getProps(), $item);
    }

    /**
     * Summarize a submission
     *
     * Includes properties with the apiSummary flag in the submission schema.
     *
     * @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
     * @param Genre[] $genres The file genres in this context
     */
    public function summarize(Submission $item, LazyCollection $userGroups, array $genres): array
    {
        $this->userGroups = $userGroups;
        $this->genres = $genres;
        return $this->mapByProperties($this->getSummaryProps(), $item);
    }

    /**
     * Map a collection of Submissions
     *
     * @see self::map
     *
     * @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
     * @param Genre[] $genres The file genres in this context
     */
    public function mapMany(Enumerable $collection, LazyCollection $userGroups, array $genres): Enumerable
    {
        $this->collection = $collection;
        $this->userGroups = $userGroups;
        $this->genres = $genres;
        return $collection->map(function ($item) {
            return $this->map($item, $this->userGroups, $this->genres);
        });
    }

    /**
     * Summarize a collection of Submissions
     *
     * @see self::summarize
     *
     * @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
     * @param Genre[] $genres The file genres in this context
     */
    public function summarizeMany(Enumerable $collection, LazyCollection $userGroups, array $genres): Enumerable
    {
        $this->collection = $collection;
        $this->userGroups = $userGroups;
        $this->genres = $genres;
        return $collection->map(function ($item) {
            return $this->summarize($item, $this->userGroups, $this->genres);
        });
    }

    /**
     * Map a submission with extra properties for the submissions list
     *
     * @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
     * @param Genre[] $genres The file genres in this context
     */
    public function mapToSubmissionsList(Submission $item, LazyCollection $userGroups, array $genres): array
    {
        $this->userGroups = $userGroups;
        $this->genres = $genres;
        return $this->mapByProperties($this->getSubmissionsListProps(), $item);
    }

    /**
     * Map a collection of submissions with extra properties for the submissions list
     *
     * @see self::map
     *
     * @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
     * @param Genre[] $genres The file genres in this context
     */
    public function mapManyToSubmissionsList(Enumerable $collection, LazyCollection $userGroups, array $genres): Enumerable
    {
        $this->collection = $collection;
        $this->userGroups = $userGroups;
        $this->genres = $genres;
        return $collection->map(function ($item) {
            return $this->mapToSubmissionsList($item, $this->userGroups, $this->genres);
        });
    }

    /**
     * Map a submission with only the title, authors, and URLs for the stats list
     */
    public function mapToStats(Submission $submission): array
    {
        $props = $this->mapByProperties([
            '_href',
            'id',
            'urlWorkflow',
            'urlPublished',
        ], $submission);

        $currentPublication = $submission->getCurrentPublication();
        if ($currentPublication) {
            $props['authorsStringShort'] = $currentPublication->getShortAuthorString();
            $props['fullTitle'] = $currentPublication->getFullTitles('html');
        }

        return $props;
    }

    /**
     * Summarize a submission without publication details
     */
    public function summarizeWithoutPublication(Submission $item): array
    {
        $props = array_filter($this->getSummaryProps(), function ($prop) {
            return $prop !== 'publications';
        });
        return $this->mapByProperties($props, $item);
    }

    /**
     * Map schema properties of a Submission to an assoc array
     */
    protected function mapByProperties(array $props, Submission $submission): array
    {
        $output = [];

        if (in_array('publications', $props)) {
            $reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
            $currentUserReviewAssignment = $reviewAssignmentDao->getLastReviewRoundReviewAssignmentByReviewer(
                $submission->getId(),
                $this->request->getUser()->getId()
            );
            $anonymize = $currentUserReviewAssignment && $currentUserReviewAssignment->getReviewMethod() === ReviewAssignment::SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS;
        }

        foreach ($props as $prop) {
            switch ($prop) {
                case '_href':
                    $output[$prop] = Repo::submission()->getUrlApi($this->context, $submission->getId());
                    break;
                case 'publications':
                    $output[$prop] = Repo::publication()->getSchemaMap($submission, $this->userGroups, $this->genres)
                        ->summarizeMany($submission->getData('publications'), $anonymize)->values();
                    break;
                case 'reviewAssignments':
                    $output[$prop] = $this->getPropertyReviewAssignments($submission);
                    break;
                case 'reviewRounds':
                    $output[$prop] = $this->getPropertyReviewRounds($submission);
                    break;
                case 'stages':
                    $output[$prop] = $this->getPropertyStages($submission);
                    break;
                case 'statusLabel':
                    $output[$prop] = __($submission->getStatusKey());
                    break;
                case 'urlAuthorWorkflow':
                    $output[$prop] = Repo::submission()->getUrlAuthorWorkflow($this->context, $submission->getId());
                    break;
                case 'urlEditorialWorkflow':
                    $output[$prop] = Repo::submission()->getUrlEditorialWorkflow($this->context, $submission->getId());
                    break;
                case 'urlSubmissionWizard':
                    $output[$prop] = Repo::submission()->getUrlSubmissionWizard($this->context, $submission->getId());
                    break;
                case 'urlWorkflow':
                    $output[$prop] = Repo::submission()->getWorkflowUrlByUserRoles($submission);
                    break;
                default:
                    $output[$prop] = $submission->getData($prop);
                    break;
            }
        }

        return $output;
    }

    /**
     * Get details about the review assignments for a submission
     */
    protected function getPropertyReviewAssignments(Submission $submission): array
    {
        $reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
        $reviewAssignments = $reviewAssignmentDao->getBySubmissionId($submission->getId());

        $reviews = [];
        foreach ($reviewAssignments as $reviewAssignment) {
            // @todo for now, only show reviews that haven't been
            // declined or cancelled
            if ($reviewAssignment->getDeclined() || $reviewAssignment->getCancelled()) {
                continue;
            }

            $request = Application::get()->getRequest();
            $currentUser = $request->getUser();
            $context = $request->getContext();
            $due = is_null($reviewAssignment->getDateDue()) ? null : date('Y-m-d', strtotime($reviewAssignment->getDateDue()));
            $responseDue = is_null($reviewAssignment->getDateResponseDue()) ? null : date('Y-m-d', strtotime($reviewAssignment->getDateResponseDue()));

            $reviews[] = [
                'id' => (int) $reviewAssignment->getId(),
                'isCurrentUserAssigned' => $currentUser->getId() == (int) $reviewAssignment->getReviewerId(),
                'statusId' => (int) $reviewAssignment->getStatus(),
                'status' => __($reviewAssignment->getStatusKey()),
                'due' => $due,
                'responseDue' => $responseDue,
                'round' => (int) $reviewAssignment->getRound(),
                'roundId' => (int) $reviewAssignment->getReviewRoundId(),
            ];
        }

        return $reviews;
    }

    /**
     * Get details about the review rounds for a submission
     */
    protected function getPropertyReviewRounds(Submission $submission): array
    {
        $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
        $reviewRounds = $reviewRoundDao->getBySubmissionId($submission->getId())->toIterator();

        $rounds = [];
        foreach ($reviewRounds as $reviewRound) {
            $rounds[] = [
                'id' => $reviewRound->getId(),
                'round' => $reviewRound->getRound(),
                'stageId' => $reviewRound->getStageId(),
                'statusId' => $reviewRound->determineStatus(),
                'status' => __($reviewRound->getStatusKey()),
            ];
        }

        return $rounds;
    }

    /**
     * Get details about a submission's stage(s)
     *
     * @return array
     * [
     *  {
     *  `id` int stage id
     *  `label` string translated stage name
     *  `queries` array [{
     *    `id` int query id
     *    `assocType` int
     *    `assocId` int
     *    `stageId` int
     *    `seq` int
     *    `closed` bool
     *   }]
     *  `statusId` int stage status. note: on review stage, this refers to the
     *    status of the latest round.
     *  `status` string translated stage status name
     *  `files` array {
     *    `count` int number of files attached to stage. note: this only counts
     *      revision files.
     *   }
     *  ]
     */
    public function getPropertyStages(Submission $submission): array
    {
        $stageIds = Application::get()->getApplicationStages();
        $request = Application::get()->getRequest();
        $currentUser = $request->getUser();
        $context = $request->getContext();

        $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
        $stageAssignments = $stageAssignmentDao->getBySubmissionAndUserIdAndStageId($submission->getId(), $currentUser->getId() ?? 0)->toArray();

        $queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
        $openPerStage = $queryDao->countOpenPerStage($submission->getId(), [$request->getUser()->getId()]);

        $stages = [];
        foreach ($stageIds as $stageId) {
            $workflowStageDao = DAORegistry::getDAO('WorkflowStageDAO'); /** @var WorkflowStageDAO $workflowStageDao */
            $stage = [
                'id' => (int) $stageId,
                'label' => __($workflowStageDao->getTranslationKeyFromId($stageId)),
                'isActiveStage' => $submission->getData('stageId') == $stageId,
                'openQueryCount' => $openPerStage[$stageId],
            ];

            $currentUserAssignedRoles = [];
            if ($currentUser) {
                /** @var StageAssignment $stageAssignment */
                foreach ($stageAssignments as $stageAssignment) {
                    $userGroup = $this->getUserGroup($stageAssignment->getUserGroupId());
                    if ($userGroup) {
                        $currentUserAssignedRoles[] = $userGroup->getRoleId();
                    }
                }
                $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
                $stageAssignmentsResult = $stageAssignmentDao->getBySubmissionAndUserIdAndStageId($submission->getId(), $currentUser->getId(), $stageId);
                while ($stageAssignment = $stageAssignmentsResult->next()) {
                    $userGroup = Repo::userGroup()->get($stageAssignment->getUserGroupId());
                    $currentUserAssignedRoles[] = (int) $userGroup->getRoleId();
                }
            }
            $stage['currentUserAssignedRoles'] = array_values(array_unique($currentUserAssignedRoles));

            // Stage-specific statuses
            switch ($stageId) {
                case WORKFLOW_STAGE_ID_SUBMISSION:
                    $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
                    $assignedEditors = $stageAssignmentDao->editorAssignedToStage($submission->getId(), $stageId);
                    if (!$assignedEditors) {
                        $stage['statusId'] = Repo::submission()::STAGE_STATUS_SUBMISSION_UNASSIGNED;
                        $stage['status'] = __('submissions.queuedUnassigned');
                    }

                    // Submission stage never has revisions
                    $stage['files'] = [
                        'count' => 0,
                    ];
                    break;

                case WORKFLOW_STAGE_ID_INTERNAL_REVIEW:
                case WORKFLOW_STAGE_ID_EXTERNAL_REVIEW:
                    $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
                    $reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $stageId);
                    if ($reviewRound) {
                        $stage['statusId'] = $reviewRound->determineStatus();
                        $stage['status'] = __($reviewRound->getStatusKey());

                        // Revision files in this round.
                        $stage['files'] = [
                            'count' => Repo::submissionFile()->getCollector()
                                ->filterBySubmissionIds([$submission->getId()])
                                ->filterByFileStages([SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION])
                                ->filterByReviewRoundIds([$reviewRound->getId()])
                                ->getCount()
                        ];

                        // See if the  current user can only recommend:
                        $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
                        $user = $request->getUser();
                        $editorsStageAssignments = $stageAssignmentDao->getEditorsAssignedToStage($submission->getId(), $stageId);
                        // if the user is assigned several times in the editorial role, and
                        // one of the assignments have recommendOnly option set, consider it here
                        $stage['currentUserCanRecommendOnly'] = false;
                        foreach ($editorsStageAssignments as $editorsStageAssignment) {
                            if ($editorsStageAssignment->getUserId() == $user->getId() && $editorsStageAssignment->getRecommendOnly()) {
                                $stage['currentUserCanRecommendOnly'] = true;
                                break;
                            }
                        }
                    } else {
                        // workaround for pkp/pkp-lib#4231, pending formal data model
                        $stage['files'] = [
                            'count' => 0
                        ];
                    }
                    break;

                    // Get revision files for editing and production stages.
                    // Review rounds are handled separately in the review stage below.
                case WORKFLOW_STAGE_ID_EDITING:
                case WORKFLOW_STAGE_ID_PRODUCTION:
                    $fileStages = [WORKFLOW_STAGE_ID_EDITING ? SubmissionFile::SUBMISSION_FILE_COPYEDIT : SubmissionFile::SUBMISSION_FILE_PROOF];
                    // Revision files in this round.
                    $stage['files'] = [
                        'count' => Repo::submissionFile()->getCollector()
                            ->filterBySubmissionIds([$submission->getId()])
                            ->filterByFileStages($fileStages)
                            ->getCount()
                    ];
                    break;
            }

            $stages[] = $stage;
        }

        return $stages;
    }

    protected function getUserGroup(int $userGroupId): ?UserGroup
    {
        /** @var UserGroup $userGroup */
        foreach ($this->userGroups as $userGroup) {
            if ($userGroup->getId() === $userGroupId) {
                return $userGroup;
            }
        }
        return null;
    }
}
