<?php

/**
 * @file classes/oai/PKPOAIDAO.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 PKPOAIDAO
 *
 * @ingroup oai
 *
 * @see OAI
 *
 * @brief Base class for DAO operations for the OAI interface.
 */

namespace PKP\oai;

use PKP\plugins\Hook;

abstract class PKPOAIDAO extends \PKP\db\DAO
{
    /** @var OAI parent OAI object */
    public $oai;


    /**
     * Set parent OAI object.
     *
     * @param OAI $oai
     */
    public function setOAI($oai)
    {
        $this->oai = $oai;
    }

    //
    // Resumption tokens
    //
    /**
     * Clear stale resumption tokens.
     */
    public function clearTokens()
    {
        $this->update(
            'DELETE FROM oai_resumption_tokens WHERE expire < ?',
            [time()]
        );
    }

    /**
     * Retrieve a resumption token.
     *
     * @param string $tokenId OAI resumption token
     *
     * @return OAIResumptionToken
     */
    public function getToken($tokenId)
    {
        $result = $this->retrieve('SELECT * FROM oai_resumption_tokens WHERE token = ?', [$tokenId]);
        $row = $result->current();
        return $row ? new OAIResumptionToken($row->token, $row->record_offset, unserialize($row->params), $row->expire) : null;
    }

    /**
     * Insert an OAI resumption token, generating a new ID.
     *
     * @param OAIResumptionToken $token
     *
     * @return OAIResumptionToken
     */
    public function insertToken($token)
    {
        do {
            // Generate unique token ID
            $token->id = md5(uniqid(random_int(0, PHP_INT_MAX), true));
            $result = $this->retrieve(
                'SELECT COUNT(*) AS row_count FROM oai_resumption_tokens WHERE token = ?',
                [$token->id]
            );
            $row = $result->current();
            $val = $row->row_count;
        } while ($val != 0);

        $this->update(
            'INSERT INTO oai_resumption_tokens (token, record_offset, params, expire)
			VALUES
			(?, ?, ?, ?)',
            [$token->id, $token->offset, serialize($token->params), $token->expire]
        );

        return $token;
    }


    /**
     * Check if a data object ID specifies a data object.
     *
     * @param int $dataObjectId
     * @param array $setIds optional Objects ids that specify an OAI set,
     * in hierarchical order. If passed, will check for the data object id
     * only inside the specified set.
     *
     * @return bool
     */
    public function recordExists($dataObjectId, $setIds = [])
    {
        return $this->getRecord($dataObjectId, $setIds) ? true : false;
    }

    /**
     * Return OAI record for specified data object.
     *
     * @param int $dataObjectId
     * @param array $setIds optional Objects ids that specify an OAI set,
     * in hierarchical order. If passed, will check for the data object id
     * only inside the specified set.
     *
     * @return OAIRecord
     */
    public function getRecord($dataObjectId, $setIds = [])
    {
        $result = $this->_getRecordsRecordSetQuery($setIds, null, null, null, $dataObjectId);
        $row = $result->first();
        return $row ? $this->_returnRecordFromRow((array) $row) : null;
    }

    /**
     * Return set of OAI records matching specified parameters.
     *
     * @param array $setIds Objects ids that specify an OAI set,
     * in hierarchical order. The returned records will be part
     * of this set.
     * @param int $from timestamp
     * @param int $until timestamp
     * @param string $set setSpec
     * @param int $offset
     * @param int $limit
     * @param int $total
     *
     * @return array OAIRecord
     */
    public function getRecords($setIds, $from, $until, $set, $offset, $limit, &$total)
    {
        $query = $this->_getRecordsRecordSetQuery($setIds, $from, $until, $set);
        $total = $query->getCountForPagination();
        $results = $query->offset($offset)->limit($limit)->get();

        $records = [];
        foreach ($results as $row) {
            $records[] = $this->_returnRecordFromRow((array) $row);
        }
        return $records;
    }

    /**
     * Return set of OAI identifiers matching specified parameters.
     *
     * @param array $setIds Objects ids that specify an OAI set,
     * in hierarchical order. The returned records will be part
     * of this set.
     * @param int $from timestamp
     * @param int $until timestamp
     * @param string $set setSpec
     * @param int $offset
     * @param int $limit
     * @param int $total
     *
     * @return array OAIIdentifier
     */
    public function getIdentifiers($setIds, $from, $until, $set, $offset, $limit, &$total)
    {
        $query = $this->_getRecordsRecordSetQuery($setIds, $from, $until, $set);
        $total = $query->getCountForPagination();
        $results = $query->offset($offset)->limit($limit)->get();

        $records = [];
        foreach ($results as $row) {
            $records[] = $this->_returnIdentifierFromRow((array) $row);
        }
        return $records;
    }

    /**
     * Return the *nix timestamp of the earliest published submission.
     *
     * @param array $setIds optional Objects ids that specify an OAI set,
     * in hierarchical order. If empty, all records from
     * all sets will be included.
     *
     * @return int
     */
    public function getEarliestDatestamp($setIds = [])
    {
        $query = $this->_getRecordsRecordSetQuery($setIds, null, null, null, null, 'last_modified');
        if ($row = $query->first()) {
            $record = $this->_returnRecordFromRow((array) $row);
            return OAIUtils::UTCtoTimestamp($record->datestamp);
        }
        return 0;
    }


    //
    // Private helper methods.
    //
    /**
     * Return OAIRecord object from database row.
     *
     * @param array $row
     *
     * @return OAIRecord
     */
    public function _returnRecordFromRow($row)
    {
        $record = new OAIRecord();
        $record = $this->_doCommonOAIFromRowOperations($record, $row);

        Hook::call('OAIDAO::_returnRecordFromRow', [&$record, &$row]);

        return $record;
    }

    /**
     * Return OAIIdentifier object from database row.
     *
     * @param array $row
     *
     * @return OAIIdentifier
     */
    public function _returnIdentifierFromRow($row)
    {
        $record = new OAIIdentifier();
        $record = $this->_doCommonOAIFromRowOperations($record, $row);

        Hook::call('OAIDAO::_returnIdentifierFromRow', [&$record, &$row]);

        return $record;
    }

    /**
     * Common operations for OAIRecord and OAIIdentifier object data set.
     *
     * @param OAIRecord|OAIIdentifier $record
     * @param array $row
     *
     * @return OAIRecord|OAIIdentifier
     */
    public function _doCommonOAIFromRowOperations($record, $row)
    {
        $record->datestamp = OAIUtils::UTCDate(strtotime($this->datetimeFromDB($row['last_modified'])));

        if (isset($row['tombstone_id'])) {
            $record->identifier = $row['oai_identifier'];
            $record->sets = [$row['set_spec']];
            $record->status = OAI::OAIRECORD_STATUS_DELETED;
        } else {
            $record->status = OAI::OAIRECORD_STATUS_ALIVE;
            $record = $this->setOAIData($record, $row, $record instanceof \PKP\oai\OAIRecord);
        }

        return $record;
    }

    /**
     * Get a OAI records record set.
     *
     * @param array $setIds Objects ids that specify an OAI set,
     * in hierarchical order.
     * @param int|string $from *nix timestamp or ISO datetime string
     * @param int|string $until *nix timestamp or ISO datetime string
     * @param string $set
     * @param int $submissionId optional
     * @param string $orderBy UNFILTERED
     *
     * @return \Illuminate\Database\Query\Builder
     */
    abstract public function _getRecordsRecordSetQuery($setIds, $from, $until, $set, $submissionId = null, $orderBy = 'journal_id, submission_id');
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\oai\PKPOAIDAO', '\PKPOAIDAO');
}
