<?php

/**
 * @file classes/oai/ojs/OAIDAO.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 OAIDAO
 *
 * @ingroup oai_ojs
 *
 * @see OAI
 *
 * @brief DAO operations for the OJS OAI interface.
 */

namespace APP\oai\ojs;

use APP\core\Application;
use APP\facades\Repo;
use APP\journal\JournalDAO;
use Illuminate\Support\Facades\DB;
use PKP\db\DAORegistry;
use PKP\galley\DAO;
use PKP\oai\OAISet;
use PKP\oai\OAIUtils;
use PKP\oai\PKPOAIDAO;
use PKP\plugins\Hook;
use PKP\submission\PKPSubmission;
use PKP\tombstone\DataObjectTombstoneDAO;

class OAIDAO extends PKPOAIDAO
{
    // Helper DAOs
    /** @var JournalDAO */
    public $journalDao;
    /** @var DAO */
    public $galleyDao;

    public $journalCache;
    public $sectionCache;
    public $issueCache;

    /**
     * Constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->journalDao = DAORegistry::getDAO('JournalDAO');
        $this->galleyDao = Repo::galley()->dao;

        $this->journalCache = [];
        $this->sectionCache = [];
    }

    /**
     * @copydoc PKPOAIDAO::getEarliestDatestampQuery()
     */
    public function getEarliestDatestampQuery()
    {
    }

    /**
     * Cached function to get a journal
     *
     * @param int $journalId
     *
     * @return object
     */
    public function &getJournal($journalId)
    {
        if (!isset($this->journalCache[$journalId])) {
            $this->journalCache[$journalId] = $this->journalDao->getById($journalId);
        }
        return $this->journalCache[$journalId];
    }

    /**
     * Cached function to get an issue
     *
     * @param int $issueId
     *
     * @return object
     */
    public function &getIssue($issueId)
    {
        if (!isset($this->issueCache[$issueId])) {
            $this->issueCache[$issueId] = Repo::issue()->get($issueId);
        }
        return $this->issueCache[$issueId];
    }

    /**
     * Cached function to get a journal section
     *
     * @param int $sectionId
     *
     * @return object
     */
    public function &getSection($sectionId)
    {
        if (!isset($this->sectionCache[$sectionId])) {
            $this->sectionCache[$sectionId] = Repo::section()->get($sectionId);
        }
        return $this->sectionCache[$sectionId];
    }


    //
    // Sets
    //
    /**
     * Return hierarchy of OAI sets (journals plus journal sections).
     *
     * @param int $journalId
     * @param int $offset
     * @param int $total
     *
     * @return array OAISet
     */
    public function &getJournalSets($journalId, $offset, $limit, &$total)
    {
        if (isset($journalId)) {
            $journals = [$this->journalDao->getById($journalId)];
        } else {
            $journals = $this->journalDao->getAll(true);
            $journals = $journals->toArray();
        }

        // FIXME Set descriptions
        $sets = [];
        foreach ($journals as $journal) {
            $title = $journal->getLocalizedName();
            array_push($sets, new OAISet(self::setSpec($journal), $title, ''));

            $tombstoneDao = DAORegistry::getDAO('DataObjectTombstoneDAO'); /** @var DataObjectTombstoneDAO $tombstoneDao */
            $articleTombstoneSets = $tombstoneDao->getSets(Application::ASSOC_TYPE_JOURNAL, $journal->getId());

            $sections = Repo::section()->getCollector()->filterByContextIds([$journal->getId()])->getMany();
            foreach ($sections as $section) {
                $setSpec = self::setSpec($journal, $section);
                if (array_key_exists($setSpec, $articleTombstoneSets)) {
                    unset($articleTombstoneSets[$setSpec]);
                }
                array_push($sets, new OAISet($setSpec, $section->getLocalizedTitle(), ''));
            }
            foreach ($articleTombstoneSets as $articleTombstoneSetSpec => $articleTombstoneSetName) {
                array_push($sets, new OAISet($articleTombstoneSetSpec, $articleTombstoneSetName, ''));
            }
        }

        Hook::call('OAIDAO::getJournalSets', [$this, $journalId, $offset, $limit, $total, &$sets]);

        $total = count($sets);
        $sets = array_slice($sets, $offset, $limit);

        return $sets;
    }

    /**
     * Return the journal ID and section ID corresponding to a journal/section pairing.
     *
     * @param string $journalSpec
     * @param string $sectionSpec
     * @param int $restrictJournalId
     *
     * @return array (int, int)
     */
    public function getSetJournalSectionId($journalSpec, $sectionSpec, $restrictJournalId = null)
    {
        $journal = $this->journalDao->getByPath($journalSpec);
        if (!isset($journal) || (isset($restrictJournalId) && $journal->getId() != $restrictJournalId)) {
            return [0, 0];
        }

        $journalId = $journal->getId();
        $sectionId = null;

        if (isset($sectionSpec)) {
            $sectionId = 0;
            $sections = Repo::section()->getCollector()->filterByContextIds([$journalId])->getMany();
            foreach ($sections as $section) {
                if ($sectionSpec == OAIUtils::toValidSetSpec($section->getLocalizedAbbrev())) {
                    $sectionId = $section->getId();
                    break;
                }
            }
        }

        return [$journalId, $sectionId];
    }

    public static function setSpec($journal, $section = null): string
    {
        // journal path is already restricted to ascii alphanumeric, '-' and '_'
        return isset($section)
            ? $journal->getPath() . ':' . OAIUtils::toValidSetSpec($section->getLocalizedAbbrev())
            : $journal->getPath();
    }

    //
    // Protected methods.
    //
    /**
     * @see lib/pkp/classes/oai/PKPOAIDAO::setOAIData()
     */
    public function setOAIData($record, $row, $isRecord = true)
    {
        $journal = $this->getJournal($row['journal_id']);
        $section = $this->getSection($row['section_id']);
        $articleId = $row['submission_id'];

        /** @var JournalOAI */
        $oai = $this->oai;
        $record->identifier = $oai->articleIdToIdentifier($articleId);
        $record->sets = [self::setSpec($journal, $section)];

        if ($isRecord) {
            $submission = Repo::submission()->get($articleId);
            $issue = $this->getIssue($row['issue_id']);
            $galleys = Repo::galley()->getCollector()
                ->filterByPublicationIds([$submission->getCurrentPublication()->getId()])
                ->getMany();

            $record->setData('article', $submission);
            $record->setData('journal', $journal);
            $record->setData('section', $section);
            $record->setData('issue', $issue);
            $record->setData('galleys', $galleys);
        }

        return $record;
    }

    /**
     * @copydoc PKPOAIDAO::_getRecordsRecordSet
     *
     * @param null|mixed $submissionId
     */
    public function _getRecordsRecordSetQuery($setIds, $from, $until, $set, $submissionId = null, $orderBy = 'journal_id, submission_id')
    {
        $journalId = array_shift($setIds);
        $sectionId = array_shift($setIds);

        // Exclude all journals that do not have OAI-PMH specifically turned on, see #pkp/pkp-lib#6503
        $excludeJournals = DB::table('journals')
            ->whereNotIn('journal_id', function ($query) {
                $query->select('journal_id')
                    ->from('journal_settings')
                    ->where('setting_name', 'enableOai')
                    ->where('setting_value', 1);
            })
            ->groupBy('journal_id')
            ->pluck('journal_id')
            ->all();

        return DB::table('submissions AS a')
            ->select([
                DB::raw('GREATEST(a.last_modified, i.last_modified, p.last_modified) AS last_modified'),
                'a.submission_id AS submission_id',
                'i.issue_id',
                DB::raw('NULL AS tombstone_id'),
                DB::raw('NULL AS set_spec'),
                DB::raw('NULL AS oai_identifier'),
                'j.journal_id AS journal_id',
                's.section_id AS section_id',
            ])
            ->join('publications AS p', 'a.current_publication_id', '=', 'p.publication_id')
            ->join('publication_settings AS psissue', function ($join) {
                $join->on('psissue.publication_id', '=', 'p.publication_id');
                $join->where('psissue.setting_name', '=', DB::raw('\'issueId\''));
                $join->where('psissue.locale', '=', DB::raw('\'\''));
            })
            ->join('issues AS i', DB::raw('CAST(i.issue_id AS CHAR(20))'), '=', 'psissue.setting_value')
            ->join('sections AS s', 's.section_id', '=', 'p.section_id')
            ->join('journals AS j', 'j.journal_id', '=', 'a.context_id')
            ->where('i.published', '=', 1)
            ->where('j.enabled', '=', 1)
            ->where('a.status', '=', PKPSubmission::STATUS_PUBLISHED)
            ->when($excludeJournals, function ($query, $excludeJournals) {
                return $query->whereNotIn('j.journal_id', $excludeJournals);
            })
            ->when(isset($journalId), function ($query) use ($journalId) {
                return $query->where('j.journal_id', '=', (int) $journalId);
            })
            ->when(isset($sectionId), function ($query) use ($sectionId) {
                return $query->where('p.section_id', '=', (int) $sectionId);
            })
            ->when($from, function ($query, $from) {
                return $query->where('GREATEST(a.last_modified, i.last_modified, p.last_modified)', '>=', $from);
            })
            ->when($until, function ($query, $until) {
                return $query->where('GREATEST(a.last_modified, i.last_modified, p.last_modified)', '<=', $until);
            })
            ->when($submissionId, function ($query, $submissionId) {
                return $query->where('a.submission_id', '=', (int) $submissionId);
            })
            ->union(
                DB::table('data_object_tombstones AS dot')
                    ->select([
                        'dot.date_deleted AS last_modified',
                        'dot.data_object_id AS submission_id',
                        DB::raw('NULL AS issue_id'),
                        'dot.tombstone_id',
                        'dot.set_spec',
                        'dot.oai_identifier'
                    ])
                    ->when(isset($journalId), function ($query, $journalId) {
                        return $query->join('data_object_tombstone_oai_set_objects AS tsoj', function ($join) use ($journalId) {
                            $join->on('tsoj.tombstone_id', '=', 'dot.tombstone_id');
                            $join->where('tsoj.assoc_type', '=', Application::ASSOC_TYPE_JOURNAL);
                            $join->where('tsoj.assoc_id', '=', (int) $journalId);
                        })->addSelect(['tsoj.assoc_id']);
                    }, function ($query) {
                        return $query->addSelect([DB::raw('NULL AS assoc_id')]);
                    })
                    ->when(isset($sectionId), function ($query) use ($sectionId) {
                        return $query->join('data_object_tombstone_oai_set_objects AS tsos', function ($join) use ($sectionId) {
                            $join->on('tsos.tombstone_id', '=', 'dot.tombstone_id');
                            $join->where('tsos.assoc_type', '=', Application::ASSOC_TYPE_SECTION);
                            $join->where('tsos.assoc_id', '=', (int) $sectionId);
                        })->addSelect(['tsos.assoc_id']);
                    }, function ($query) {
                        return $query->addSelect([DB::raw('NULL AS assoc_id')]);
                    })
                    ->when(isset($set), function ($query) use ($set) {
                        return $query->where('dot.set_spec', '=', $set)
                            ->orWhere('dot.set_spec', 'like', $set . ':%');
                    })
                    ->when($from, function ($query, $from) {
                        return $query->where('dot.date_deleted', '>=', $from);
                    })
                    ->when($until, function ($query, $until) {
                        return $query->where('dot.date_deleted', '<=', $until);
                    })
                    ->when($submissionId, function ($query, $submissionId) {
                        return $query->where('dot.data_object_id', '=', (int) $submissionId);
                    })
            )
            ->orderBy(DB::raw($orderBy));
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\APP\oai\ojs\OAIDAO', '\OAIDAO');
}
