<?php
/**
 * @file classes/issue/Repository.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class Repository
 *
 * @brief A repository to find and manage issues.
 */

namespace APP\issue;

use APP\core\Request;
use APP\facades\Repo;
use APP\file\IssueFileManager;
use APP\file\PublicFileManager;
use APP\journal\JournalDAO;
use Illuminate\Support\Collection;
use PKP\db\DAORegistry;
use PKP\doi\exceptions\DoiException;
use PKP\plugins\Hook;
use PKP\services\PKPSchemaService;
use PKP\validation\ValidatorFactory;

class Repository
{
    /** @var DAO $dao */
    public $dao;

    /** @var string $schemaMap The name of the class to map this entity to its schema */
    public $schemaMap = maps\Schema::class;

    /** @var Request $request */
    protected $request;

    /** @var PKPSchemaService $schemaService */
    protected $schemaService;

    // TODO: Explicitly excluding caching for now as it wasn't actually setting data to cache

    public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
    {
        $this->dao = $dao;
        $this->request = $request;
        $this->schemaService = $schemaService;
    }

    /** @copydoc  DAO::newDataObject() */
    public function newDataObject(array $params = []): Issue
    {
        $object = $this->dao->newDataObject();
        if (!empty($params)) {
            $object->setAllData($params);
        }
        return $object;
    }

    /** @copydoc DAO::exists() */
    public function exists(int $id, ?int $journalId = null): bool
    {
        return $this->dao->exists($id, $journalId);
    }

    /**
     * @copydoc DAO::get()
     */
    public function get(int $id, ?int $journalId = null): ?Issue
    {
        return $this->dao->get($id, $journalId);
    }

    /** @copydoc DAO::getCollector() */
    public function getCollector(): Collector
    {
        return app(Collector::class);
    }

    /**
     * Get an instance of the map class for mapping
     * announcements to their schema
     */
    public function getSchemaMap(): maps\Schema
    {
        return app('maps')->withExtensions($this->schemaMap);
    }

    /**
     * Validate properties for an issue
     *
     * Perform validation checks on data used to add or edit an issue.
     *
     * @param array $props A key/value array with the new data to validate
     * @param array $allowedLocales The context's supported locales
     * @param string $primaryLocale The context's primary locale
     *
     * @throws \Exception
     *
     * @return array A key/value array with validation errors. Empty if no errors
     *
     * @hook Issue::validate [[&$errors, $object, $props, $allowedLocales, $primaryLocale]]
     */
    public function validate(?Issue $object, array $props, array $allowedLocales, string $primaryLocale): array
    {
        $errors = [];

        $validator = ValidatorFactory::make(
            $props,
            $this->schemaService->getValidationRules($this->dao->schema, $allowedLocales),
        );

        // Check required fields
        ValidatorFactory::required(
            $validator,
            $object,
            $this->schemaService->getRequiredProps($this->dao->schema),
            $this->schemaService->getMultilingualProps($this->dao->schema),
            $allowedLocales,
            $primaryLocale
        );

        // Check for input from disallowed locales
        ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales);

        if ($validator->fails()) {
            $errors = $this->schemaService->formatValidationErrors($validator->errors());
        }

        Hook::call('Issue::validate', [&$errors, $object, $props, $allowedLocales, $primaryLocale]);

        return $errors;
    }

    /** @copydoc DAO::insert() */
    public function add(Issue $issue): int
    {
        return $this->dao->insert($issue);
    }

    /** @copydoc DAO::update() */
    public function edit(Issue $issue, array $params)
    {
        $newIssue = $this->newDataObject(array_merge($issue->_data, $params));

        Hook::call('Issue::edit', [&$newIssue, $issue, $params]);

        $this->dao->update($newIssue);
    }

    /** @copydoc DAO::delete() */
    public function delete(Issue $issue)
    {
        $publicFileManager = new PublicFileManager();

        if (is_array($issue->getCoverImage(null))) {
            foreach ($issue->getCoverImage(null) as $coverImage) {
                if ($coverImage != '') {
                    $publicFileManager->removeContextFile($issue->getJournalId(), $coverImage);
                }
            }
        }

        $issueId = $issue->getId();

        // Delete issue-specific ordering if it exists.
        Repo::section()->deleteCustomSectionOrdering($issueId);

        // Delete published issue galleys and issue files
        $issueGalleyDao = DAORegistry::getDAO('IssueGalleyDAO'); /** @var IssueGalleyDAO $issueGalleyDao */
        $issueGalleyDao->deleteByIssueId($issueId);

        $issueFileDao = DAORegistry::getDAO('IssueFileDAO'); /** @var IssueFileDAO $issueFileDao */
        $issueFileDao->deleteByIssueId($issueId);

        $issueFileManager = new IssueFileManager($issueId);
        $issueFileManager->deleteIssueTree();

        $this->dao->deleteCustomIssueOrdering($issueId);

        $this->dao->delete($issue);
    }

    public function deleteMany(Collector $collector)
    {
        foreach ($collector->getIds() as $issueId) {
            $this->dao->deleteById($issueId);
        }
    }

    /**
     * Retrieve current issue
     */
    public function getCurrent(int $contextId): ?Issue
    {
        /** @var JournalDAO $journalDao */
        $journalDao = DAORegistry::getDAO('JournalDAO');

        $journal = $journalDao->getById($contextId);
        $issueId = $journal->getData('currentIssueId');

        return $issueId != null ? $this->get($issueId) : null;
    }

    /**
     * Update the current issue for the journal.
     *
     */
    public function updateCurrent(int $contextId, ?Issue $newCurrentIssue = null)
    {
        /** @var JournalDAO $journalDao */
        $journalDao = DAORegistry::getDAO('JournalDAO');
        $journal = $journalDao->getById($contextId);

        if ($newCurrentIssue) {
            $journal->setData('currentIssueId', $newCurrentIssue->getId());
            $this->edit($newCurrentIssue, []);
            $journalDao->updateObject($journal);
        } else {
            $journalDao->removeCurrentIssue($journal->getId());
        }
    }

    /**
     * Get issue by a submission id
     *
     */
    public function getBySubmissionId(int $submissionId): ?Issue
    {
        $issueId = Repo::submission()
            ->get($submissionId)
            ?->getCurrentPublication()
            ?->getData('issueId');
        return $issueId ? $this->get($issueId) : null;
    }

    /**
     * Retrieve Issue by "best" issue id -- url path if it exists,
     * falling back on the internal issue ID otherwise.
     */
    public function getByBestId(string $idOrUrlPath, ?int $contextId = null): ?Issue
    {
        // Get the issue that matches the requested urlPath (if $idOrUrlPath is one)
        return ctype_digit((string) $idOrUrlPath)
            ? $this->get((int) $idOrUrlPath, $contextId)
            : $this->getByUrlPath($idOrUrlPath, $contextId);
    }

    /**
     * Get an issue by its urlPath
     *
     */
    public function getByUrlPath(string $urlPath, int $contextId): ?Issue
    {
        $issueId = $this->dao->getIdByUrlPath($urlPath, $contextId);
        return $issueId ? $this->get($issueId) : null;
    }

    /**
     * Gets a list of all the years in which issues have been published
     *
     */
    public function getYearsIssuesPublished(int $contextId): Collection
    {
        return $this->dao->getYearsIssuesPublished($contextId);
    }

    /**
     * Deletes all Issues for a given context
     *
     */
    public function deleteByContextId(int $contextId)
    {
        $collector = $this->getCollector()->filterByContextIds([$contextId]);
        $this->deleteMany($collector);
    }

    /**
     * Creates a DOI for the given issue
     *
     * @return DoiException[]
     */
    public function createDoi(Issue $issue): array
    {
        /** @var JournalDAO $contextDao */
        $contextDao = DAORegistry::getDAO('JournalDAO');
        $context = $contextDao->getById($issue->getData('journalId'));

        $doiCreationFailures = [];

        if ($context->isDoiTypeEnabled(Repo::doi()::TYPE_ISSUE) && empty($issue->getData('doiId'))) {
            try {
                $doiId = Repo::doi()->mintIssueDoi($issue, $context);
                $issue->setData('doiId', $doiId);
                $this->dao->update($issue);
            } catch (DoiException $exception) {
                $doiCreationFailures[] = $exception;
            }
        }

        return $doiCreationFailures;
    }
}
