<?php
/**
 * @file classes/publication/DAO.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 DAO
 *
 * @brief Read and write publications to the database.
 */

namespace PKP\publication;

use APP\facades\Repo;
use APP\publication\Publication;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\citation\CitationDAO;
use PKP\core\EntityDAO;
use PKP\core\traits\EntityWithParent;
use PKP\services\PKPSchemaService;
use PKP\submission\SubmissionAgencyDAO;
use PKP\submission\SubmissionDisciplineDAO;
use PKP\submission\SubmissionKeywordDAO;
use PKP\submission\SubmissionLanguageDAO;
use PKP\submission\SubmissionSubjectDAO;

/**
 * @template T of Publication
 * @extends EntityDAO<T>
 */
class DAO extends EntityDAO
{
    use EntityWithParent;

    /** @copydoc EntityDAO::$schema */
    public $schema = PKPSchemaService::SCHEMA_PUBLICATION;

    /** @copydoc EntityDAO::$table */
    public $table = 'publications';

    /** @copydoc EntityDAO::$settingsTable */
    public $settingsTable = 'publication_settings';

    /** @copydoc EntityDAO::$primaryKeyColumn */
    public $primaryKeyColumn = 'publication_id';

    /** @var SubmissionKeywordDAO */
    public $submissionKeywordDao;

    /** @var SubmissionSubjectDAO */
    public $submissionSubjectDao;

    /** @var SubmissionDisciplineDAO */
    public $submissionDisciplineDao;

    /** @var SubmissionLanguageDAO */
    public $submissionLanguageDao;

    /** @var SubmissionAgencyDAO */
    public $submissionAgencyDao;

    /** @var CitationDAO */
    public $citationDao;

    /**
     * Constructor
     */
    public function __construct(
        SubmissionKeywordDAO $submissionKeywordDao,
        SubmissionSubjectDAO $submissionSubjectDao,
        SubmissionDisciplineDAO $submissionDisciplineDao,
        SubmissionLanguageDAO $submissionLanguageDao,
        SubmissionAgencyDAO $submissionAgencyDao,
        CitationDAO $citationDao,
        PKPSchemaService $schemaService
    ) {
        parent::__construct($schemaService);

        $this->submissionKeywordDao = $submissionKeywordDao;
        $this->submissionSubjectDao = $submissionSubjectDao;
        $this->submissionDisciplineDao = $submissionDisciplineDao;
        $this->submissionLanguageDao = $submissionLanguageDao;
        $this->submissionAgencyDao = $submissionAgencyDao;
        $this->citationDao = $citationDao;
    }

    /**
     * Get the parent object ID column name
     */
    public function getParentColumn(): string
    {
        return 'submission_id';
    }

    /**
     * Instantiate a new DataObject
     */
    public function newDataObject(): Publication
    {
        return app(Publication::class);
    }

    /**
     * Get the total count of rows matching the configured query
     */
    public function getCount(Collector $query): int
    {
        return $query
            ->getQueryBuilder()
            ->getCountForPagination();
    }

    /**
     * Get a list of ids matching the configured query
     *
     * @return Collection<int,int>
     */
    public function getIds(Collector $query): Collection
    {
        return $query
            ->getQueryBuilder()
            ->select('p.' . $this->primaryKeyColumn)
            ->pluck('p.' . $this->primaryKeyColumn);
    }

    /**
     * Get a collection of publications matching the configured query
     * @return LazyCollection<int,T>
     */
    public function getMany(Collector $query): LazyCollection
    {
        $rows = $query
            ->getQueryBuilder()
            ->get();

        return LazyCollection::make(function () use ($rows) {
            foreach ($rows as $row) {
                yield $row->publication_id => $this->fromRow($row);
            }
        });
    }

    /**
     * Get the publication dates of the first and last publications
     * matching the passed query
     *
     * @return object self::$min_date_published, self::$max_date_published
     */
    public function getDateBoundaries(Collector $query): object
    {
        return $query
            ->getQueryBuilder()
            ->reorder()
            ->select([
                DB::raw('MIN(p.date_published) AS min_date_published, MAX(p.date_published) AS max_date_published')
            ])
            ->first();
    }

    /**
     * Is the urlPath a duplicate?
     *
     * Checks if the urlPath is used in any other submission than the one
     * passed
     *
     * A urlPath may be duplicated across more than one publication of the
     * same submission. But two publications in two different submissions
     * can not share the same urlPath.
     *
     * This is only applied within a single context.
     */
    public function isDuplicateUrlPath(string $urlPath, int $submissionId, int $contextId): bool
    {
        if (!strlen($urlPath)) {
            return false;
        }
        return (bool) DB::table('publications as p')
            ->leftJoin('submissions as s', 's.submission_id', '=', 'p.submission_id')
            ->where('url_path', '=', $urlPath)
            ->where('p.submission_id', '!=', $submissionId)
            ->where('s.context_id', '=', $contextId)
            ->count();
    }

    /**
     * @copydoc EntityDAO::fromRow()
     */
    public function fromRow(object $row): Publication
    {
        $publication = parent::fromRow($row);

        $this->setDoiObject($publication);

        // Set the primary locale from the submission
        $locale = DB::table('submissions as s')
            ->where('s.submission_id', '=', $publication->getData('submissionId'))
            ->value('locale');
        $publication->setData('locale', $locale);

        $this->setAuthors($publication);
        $this->setCategories($publication);
        $this->setControlledVocab($publication);

        return $publication;
    }

    /**
     * @copydoc EntityDAO::insert()
     */
    public function insert(Publication $publication): int
    {
        $vocabs = $this->extractControlledVocab($publication);

        $id = parent::_insert($publication);

        $this->saveControlledVocab($vocabs, $id);
        $this->saveCategories($publication);

        // Parse the citations
        if ($publication->getData('citationsRaw')) {
            $this->saveCitations($publication);
        }

        return $id;
    }

    /**
     * @copydoc EntityDAO::update()
     */
    public function update(Publication $publication, Publication $oldPublication = null)
    {
        $vocabs = $this->extractControlledVocab($publication);

        parent::_update($publication);

        $this->saveControlledVocab($vocabs, $publication->getId());
        $this->saveCategories($publication);

        if ($oldPublication && $oldPublication->getData('citationsRaw') != $publication->getData('citationsRaw')) {
            $this->saveCitations($publication);
        }
    }

    /**
     * @copydoc EntityDAO::delete()
     */
    public function delete(Publication $publication)
    {
        parent::_delete($publication);
    }

    /**
     * @copydoc EntityDAO::deleteById()
     */
    public function deleteById(int $publicationId)
    {
        parent::deleteById($publicationId);

        $this->deleteAuthors($publicationId);
        $this->deleteCategories($publicationId);
        $this->deleteControlledVocab($publicationId);
        $this->deleteCitations($publicationId);
    }

    /**
     * Get publication ids that have a matching setting
     */
    public function getIdsBySetting(string $settingName, $settingValue, int $contextId): Enumerable
    {
        $q = DB::table($this->table . ' as p')
            ->join($this->settingsTable . ' as ps', 'p.publication_id', '=', 'ps.publication_id')
            ->join('submissions as s', 'p.submission_id', '=', 's.submission_id')
            ->where('ps.setting_name', '=', $settingName)
            ->where('ps.setting_value', '=', $settingValue)
            ->where('s.context_id', '=', (int) $contextId);

        return $q->select('p.publication_id')
            ->pluck('p.publication_id');
    }

    /**
     * @copydoc PKPPubIdPluginDAO::pubIdExists()
     */
    public function pubIdExists($pubIdType, $pubId, $excludePubObjectId, int $contextId)
    {
        $result = $this->deprecatedDao->retrieve(
            'SELECT COUNT(*) AS row_count
			FROM publication_settings ps
			LEFT JOIN publications p ON p.publication_id = ps.publication_id
			LEFT JOIN submissions s ON p.submission_id = s.submission_id
			WHERE ps.setting_name = ? and ps.setting_value = ? and s.submission_id <> ? AND s.context_id = ?',
            [
                'pub-id::' . $pubIdType,
                $pubId,
                // The excludePubObjectId refers to the submission id
                // because multiple versions of the same submission
                // are allowed to share a DOI.
                (int) $excludePubObjectId,
                (int) $contextId
            ]
        );
        $row = $result->current();
        return $row ? (bool) $row->row_count : false;
    }

    /**
     * @copydoc PKPPubIdPluginDAO::changePubId()
     */
    public function changePubId($pubObjectId, $pubIdType, $pubId)
    {
        DB::table($this->settingsTable)
            ->update([
                'publication_id' => (int) $pubObjectId,
                'locale' => '',
                'setting_name' => 'pub-id::' . $pubIdType,
                'setting_value' => (string) $pubId
            ]);
    }

    /**
     * @copydoc PKPPubIdPluginDAO::deletePubId()
     */
    public function deletePubId($pubObjectId, $pubIdType)
    {
        DB::table($this->settingsTable)
            ->where('publication_id', (int) $pubObjectId)
            ->where('setting_name', '=', 'pub-id::' . $pubIdType)
            ->delete();
    }

    /**
     * @copydoc PKPPubIdPluginDAO::deleteAllPubIds()
     */
    public function deleteAllPubIds($contextId, $pubIdType)
    {
        switch (DB::getDriverName()) {
            case 'mysql':
                $this->deprecatedDao->update(
                    'DELETE ps FROM publication_settings ps
						LEFT JOIN publications p ON p.publication_id = ps.publication_id
						LEFT JOIN submissions s ON s.submission_id = p.submission_id
						WHERE ps.setting_name = ?
						AND s.context_id = ?',
                    [
                        'pub-id::' . $pubIdType,
                        $contextId,
                    ]
                );
                break;
            case 'pgsql':
                $this->deprecatedDao->update(
                    'DELETE FROM publication_settings
					USING publication_settings ps
						LEFT JOIN publications p ON p.publication_id = ps.publication_id
						LEFT JOIN submissions s ON s.submission_id = p.submission_id
					WHERE	ps.setting_name = ?
						AND s.context_id = ?
						AND ps.publication_id = publication_settings.publication_id
						AND ps.locale = publication_settings.locale
						AND ps.setting_name = publication_settings.setting_name',
                    [
                        'pub-id::' . $pubIdType,
                        $contextId,
                    ]
                );
                break;
            default: fatalError('Unknown database type!');
        }
        $this->deprecatedDao->flushCache();
    }

    /**
     * Set a publication's author properties
     */
    protected function setAuthors(Publication $publication)
    {
        $publication->setData(
            'authors',
            Repo::author()
                ->getCollector()
                ->filterByPublicationIds([$publication->getId()])
                ->orderBy(\PKP\author\Collector::ORDERBY_SEQUENCE)
                ->getMany()
                ->remember()
        );
    }

    /**
     * Delete a publication's authors
     */
    protected function deleteAuthors(int $publicationId)
    {
        $authors = Repo::author()
            ->getCollector()
            ->filterByPublicationIds([$publicationId])
            ->getMany();

        foreach ($authors as $author) {
            Repo::author()->delete($author);
        }
    }

    /**
     * Set a publication's controlled vocabulary properties
     */
    protected function setControlledVocab(Publication $publication)
    {
        $publication->setData('keywords', $this->submissionKeywordDao->getKeywords($publication->getId()));
        $publication->setData('subjects', $this->submissionSubjectDao->getSubjects($publication->getId()));
        $publication->setData('disciplines', $this->submissionDisciplineDao->getDisciplines($publication->getId()));
        $publication->setData('languages', $this->submissionLanguageDao->getLanguages($publication->getId()));
        $publication->setData('supportingAgencies', $this->submissionAgencyDao->getAgencies($publication->getId()));
    }

    /**
     * Remove controlled vocabulary from a publication's data
     *
     * Controlled vocabulary includes keywords, subjects, and similar
     * metadata that shouldn't be saved in the publications table.
     *
     * @see self::saveControlledVocab()
     *
     * @return array Key/value of controlled vocabulary properties
     */
    protected function extractControlledVocab(Publication $publication): array
    {
        $controlledVocabKeyedArray = array_flip([
            'disciplines',
            'keywords',
            'languages',
            'subjects',
            'supportingAgencies',
        ]);

        $values = array_intersect_key($publication->_data, $controlledVocabKeyedArray);
        $publication->setAllData(array_diff_key($publication->_data, $controlledVocabKeyedArray));

        return $values;
    }

    /**
     * Save controlled vocabulary properties
     *
     * @see self::extractControlledVocab()
     */
    protected function saveControlledVocab(array $values, int $publicationId)
    {
        // Update controlled vocabularly for which we have props
        foreach ($values as $prop => $value) {
            switch ($prop) {
                case 'keywords':
                    $this->submissionKeywordDao->insertKeywords($value, $publicationId);
                    break;
                case 'subjects':
                    $this->submissionSubjectDao->insertSubjects($value, $publicationId);
                    break;
                case 'disciplines':
                    $this->submissionDisciplineDao->insertDisciplines($value, $publicationId);
                    break;
                case 'languages':
                    $this->submissionLanguageDao->insertLanguages($value, $publicationId);
                    break;
                case 'supportingAgencies':
                    $this->submissionAgencyDao->insertAgencies($value, $publicationId);
                    break;
            }
        }
    }

    /**
     * Delete controlled vocab entries for a publication
     */
    protected function deleteControlledVocab(int $publicationId)
    {
        $this->submissionKeywordDao->insertKeywords([], $publicationId);
        $this->submissionSubjectDao->insertSubjects([], $publicationId);
        $this->submissionDisciplineDao->insertDisciplines([], $publicationId);
        $this->submissionLanguageDao->insertLanguages([], $publicationId);
        $this->submissionAgencyDao->insertAgencies([], $publicationId);
    }

    /**
     * Set a publication's category property
     */
    protected function setCategories(Publication $publication)
    {
        $publication->setData(
            'categoryIds',
            Repo::category()->getCollector()
                ->filterByPublicationIds([$publication->getId()])
                ->getIds()
                ->toArray()
        );
    }

    /**
     * Save the assigned categories
     */
    protected function saveCategories(Publication $publication)
    {
        Repo::category()->dao->deletePublicationAssignments($publication->getId());
        if (!empty($publication->getData('categoryIds'))) {
            foreach ($publication->getData('categoryIds') as $categoryId) {
                Repo::category()->dao->insertPublicationAssignment($categoryId, $publication->getId());
            }
        }
    }

    /**
     * Delete the category assignments
     */
    protected function deleteCategories(int $publicationId)
    {
        Repo::category()->dao->deletePublicationAssignments($publicationId);
    }

    /**
     * Save the citations
     */
    protected function saveCitations(Publication $publication)
    {
        $this->citationDao->importCitations($publication->getId(), $publication->getData('citationsRaw'));
    }

    /**
     * Delete the citations
     */
    protected function deleteCitations(int $publicationId)
    {
        $this->citationDao->deleteByPublicationId($publicationId);
    }

    /**
     * Set the DOI object
     *
     */
    protected function setDoiObject(Publication $publication)
    {
        if (!empty($publication->getData('doiId'))) {
            $publication->setData('doiObject', Repo::doi()->get($publication->getData('doiId')));
        }
    }
}
