<?php

/**
 * @file classes/plugins/PKPPubIdPlugin.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 PKPPubIdPlugin
 *
 * @ingroup plugins
 *
 * @brief Abstract class for public identifiers plugins
 */

namespace PKP\plugins;

use APP\core\Application;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use PKP\core\EntityDAO;
use PKP\core\JSONMessage;
use PKP\core\PKPRequest;
use PKP\db\DAO;
use PKP\db\SchemaDAO;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\AjaxModal;
use PKP\template\PKPTemplateManager;

abstract class PKPPubIdPlugin extends LazyLoadPlugin
{
    //
    // Implement template methods from Plugin
    //
    /**
     * @copydoc Plugin::register()
     *
     * @param null|mixed $mainContextId
     */
    public function register($category, $path, $mainContextId = null)
    {
        if (!parent::register($category, $path, $mainContextId)) {
            return false;
        }
        if ($this->getEnabled($mainContextId)) {
            // Enable storage of additional fields.
            foreach ($this->getDAOs() as $dao) {
                // Augment the object with the additional properties required by the pub ID plugin.
                if ($dao instanceof SchemaDAO) {
                    // Schema-backed DAOs need the schema extended.
                    Hook::add('Schema::get::' . $dao->schemaName, [$this, 'addToSchema']);
                } elseif ($dao instanceof EntityDAO) {
                    // Schema-backed DAOs need the schema extended.
                    Hook::add('Schema::get::' . $dao->schema, [$this, 'addToSchema']);
                } else {
                    // For non-schema-backed DAOs, DAOName::getAdditionalFieldNames can be used.
                    $classNameParts = explode('\\', get_class($dao)); // Separate namespace info from class name
                    Hook::add(strtolower_codesafe(end($classNameParts)) . '::getAdditionalFieldNames', [$this, 'getAdditionalFieldNames']);
                }
            }
        }
        $this->addLocaleData();
        return true;
    }

    /**
     * @copydoc Plugin::getActions()
     */
    public function getActions($request, $actionArgs)
    {
        $router = $request->getRouter();
        return array_merge(
            $this->getEnabled() ? [
                new LinkAction(
                    'settings',
                    new AjaxModal(
                        $router->url($request, null, null, 'manage', null, $actionArgs),
                        $this->getDisplayName()
                    ),
                    __('manager.plugins.settings'),
                    null
                ),
            ] : [],
            parent::getActions($request, $actionArgs)
        );
    }

    /**
     * @copydoc Plugin::manage()
     */
    public function manage($args, $request)
    {
        $user = $request->getUser();
        $router = $request->getRouter();
        $context = $router->getContext($request);

        $form = $this->instantiateSettingsForm($context->getId());
        $notificationManager = new NotificationManager();
        switch ($request->getUserVar('verb')) {
            case 'save':
                $form->readInputData();
                if ($form->validate()) {
                    $form->execute();
                    $notificationManager->createTrivialNotification($user->getId(), Notification::NOTIFICATION_TYPE_SUCCESS);
                    return new JSONMessage(true);
                }
                return new JSONMessage(true, $form->fetch($request));
            case 'clearPubIds':
                if (!$request->checkCSRF()) {
                    return new JSONMessage(false);
                }
                $contextDao = Application::getContextDAO();
                $contextDao->deleteAllPubIds($context->getId(), $this->getPubIdType());
                return new JSONMessage(true);
            default:
                $form->initData();
                return new JSONMessage(true, $form->fetch($request));
        }
    }


    //
    // Protected template methods to be implemented by sub-classes.
    //
    /**
     * Get the public identifier.
     *
     * @param object $pubObject
     * 	Publication, Representation, SubmissionFile
     *
     * @return string
     */
    abstract public function getPubId($pubObject);

    /**
     * Construct the public identifier from its prefix and suffix.
     *
     * @param string $pubIdPrefix
     * @param string $pubIdSuffix
     * @param int $contextId
     *
     * @return string
     */
    abstract public function constructPubId($pubIdPrefix, $pubIdSuffix, $contextId);

    /**
     * Public identifier type, see
     * http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html
     *
     * @return string
     */
    abstract public function getPubIdType();

    /**
     * Public identifier type that will be displayed to the reader.
     *
     * @return string
     */
    abstract public function getPubIdDisplayType();

    /**
     * Full name of the public identifier.
     *
     * @return string
     */
    abstract public function getPubIdFullName();

    /**
     * Get the whole resolving URL.
     *
     * @param int $contextId
     * @param string $pubId
     *
     * @return string resolving URL
     */
    abstract public function getResolvingURL($contextId, $pubId);

    /**
     * Get the file (path + filename)
     * to be included into the object's
     * identifiers tab, e.g. for suffix editing.
     *
     * @return string
     */
    abstract public function getPubIdMetadataFile();

    /**
     * Add JavaScript files to be loaded in the metadata file.
     *
     * @param PKPRequest $request
     * @param PKPTemplateManager $templateMgr
     */
    public function addJavaScript($request, $templateMgr)
    {
    }

    /**
     * Get the file (path + filename)
     * for the pub id assignment
     * to be included into other pages.
     *
     * @return string
     */
    abstract public function getPubIdAssignFile();

    /**
     * Get the settings form.
     *
     * @param int $contextId
     *
     * @return object Settings form
     */
    abstract public function instantiateSettingsForm($contextId);

    /**
     * Get the additional form field names,
     * for metadata, e.g. suffix field name.
     *
     * @return array
     */
    abstract public function getFormFieldNames();

    /**
     * Get the assign option form field name.
     *
     * @return string
     */
    abstract public function getAssignFormFieldName();

    /**
     * Get the the prefix form field name.
     *
     * @return string
     */
    abstract public function getPrefixFieldName();

    /**
     * Get the the suffix form field name.
     *
     * @return string
     */
    abstract public function getSuffixFieldName();

    /**
     * Get the link actions used in the pub id forms,
     * e.g. clear pub id.
     *
     * @return array
     */
    abstract public function getLinkActions($pubObject);

    /**
     * Get the suffix patterns form field names for all objects.
     *
     * @return array (pub object type => suffix pattern field name)
     */
    abstract public function getSuffixPatternsFieldNames();

    /**
     * Get additional field names to be considered for storage.
     *
     * @return array
     */
    abstract public function getDAOFieldNames();

    /**
     * Get the possible publication object types.
     *
     * @return array
     */
    public function getPubObjectTypes()
    {
        return [
            'Publication' => '\APP\publication\Publication',
            'Representation' => '\PKP\submission\Representation',
            'SubmissionFile' => '\PKP\submissionFile\SubmissionFile',
        ];
    }

    /**
     * Is this object type enabled in plugin settings
     *
     * @param string $pubObjectType
     * @param int $contextId
     *
     * @return bool
     */
    abstract public function isObjectTypeEnabled($pubObjectType, $contextId);

    /**
     * Get the error message for not unique pub id
     *
     * @return string
     */
    abstract public function getNotUniqueErrorMsg();

    /**
     * Verify form data.
     *
     * @param string $fieldName The form field to be checked.
     * @param string $fieldValue The value of the form field.
     * @param object $pubObject
     * @param int $contextId
     * @param string $errorMsg Return validation error messages here.
     *
     * @return bool
     */
    public function verifyData($fieldName, $fieldValue, $pubObject, $contextId, &$errorMsg)
    {
        // Verify pub id uniqueness.
        if ($fieldName == $this->getSuffixFieldName()) {
            if (empty($fieldValue)) {
                return true;
            }

            // Construct the potential new pub id with the posted suffix.
            $pubIdPrefix = $this->getSetting($contextId, $this->getPrefixFieldName());
            if (empty($pubIdPrefix)) {
                return true;
            }
            $newPubId = $this->constructPubId($pubIdPrefix, $fieldValue, $contextId);

            if (!$this->checkDuplicate($newPubId, get_class($pubObject), $pubObject->getId(), $contextId)) {
                $errorMsg = $this->getNotUniqueErrorMsg();
                return false;
            }
        }
        return true;
    }

    /**
     * Check whether the given pubId is valid.
     *
     * @param string $pubId
     *
     * @return bool
     */
    public function validatePubId($pubId)
    {
        return true; // Assume a valid ID by default;
    }

    /**
     * Return an array of publication object types and
     * the corresponding DAOs.
     *
     * @return array
     */
    public function getDAOs()
    {
        return  [
            Repo::publication()->dao,
            Application::getRepresentationDAO(),
            Repo::submissionFile()->dao,
        ];
    }

    /**
     * Can a pub id be assigned to the object.
     *
     * @param object $pubObject
     * 	Publication, Representation, SubmissionFile, OJS Issue, OMP Chapter
     *
     * @return bool
     * 	false, if the pub id contains an unresolved pattern i.e. '%' or
     * 	if the custom suffix is empty i.e. the pub id null.
     */
    public function canBeAssigned($pubObject)
    {
        // Has the pub id already been assigned.
        $pubIdType = $this->getPubIdType();
        $storedPubId = $pubObject->getStoredPubId($pubIdType);
        if ($storedPubId) {
            return false;
        }
        // Get the pub id.
        $pubId = $this->getPubId($pubObject);
        // Is the custom suffix empty i.e. the pub id null.
        if (!$pubId) {
            return false;
        }
        // Does the suffix contain unresolved pattern.
        $containPatterns = strpos($pubId, '%') !== false;
        return !$containPatterns;
    }

    /**
     * Add properties for this type of public identifier to the entity's list for
     * storage in the database.
     * This is used for SchemaDAO-backed entities only.
     *
     * @see PKPPubIdPlugin::getAdditionalFieldNames()
     *
     * @param string $hookName `Schema::get::publication`
     * @param array $params
     */
    public function addToSchema($hookName, $params)
    {
        $schema = & $params[0];
        foreach (array_merge($this->getFormFieldNames(), $this->getDAOFieldNames()) as $fieldName) {
            $schema->properties->{$fieldName} = (object) [
                'type' => 'string',
                'apiSummary' => true,
                'validation' => ['nullable'],
            ];
        }
        return false;
    }

    /**
     * Add properties for this type of public identifier to the entity's list for
     * storage in the database.
     * This is used for non-SchemaDAO-backed entities only.
     *
     * @see PKPPubIdPlugin::addToSchema()
     *
     * @param string $hookName
     * @param DAO $dao
     * @param array $additionalFields
     *
     * @return bool
     */
    public function getAdditionalFieldNames($hookName, $dao, &$additionalFields)
    {
        foreach (array_merge($this->getFormFieldNames(), $this->getDAOFieldNames()) as $fieldName) {
            $additionalFields[] = $fieldName;
        }
        return false;
    }

    /**
     * Return the object type.
     *
     * @param object $pubObject
     *
     * @return ?string
     */
    public function getPubObjectType($pubObject)
    {
        $allowedTypes = $this->getPubObjectTypes();
        $pubObjectType = null;
        foreach ($allowedTypes as $type => $fqcn) {
            if ($pubObject instanceof $fqcn) {
                $pubObjectType = $type;
                break;
            }
        }
        if (is_null($pubObjectType)) {
            // This must be a dev error, so bail with an assertion.
            assert(false);
            return null;
        }
        return $pubObjectType;
    }

    /**
     * Set and store a public identifier.
     *
     * @param object $pubObject
     * @param string $pubId
     */
    public function setStoredPubId(&$pubObject, $pubId)
    {
        $dao = $pubObject->getDAO();
        $dao->changePubId($pubObject->getId(), $this->getPubIdType(), $pubId);
        $pubObject->setStoredPubId($this->getPubIdType(), $pubId);
    }


    //
    // Public API
    //
    /**
     * Check for duplicate public identifiers.
     *
     * Checks to see if a pubId has already been assigned to any object
     * in the context.
     *
     * @param string $pubId
     * @param string $pubObjectType Class name of the pub object being checked
     * @param int $excludeId This object id will not be checked for duplicates
     * @param int $contextId
     *
     * @return bool
     */
    public function checkDuplicate($pubId, $pubObjectType, $excludeId, $contextId)
    {
        foreach ($this->getPubObjectTypes() as $type => $fqcn) {
            if ($type === 'Publication') {
                $typeDao = Repo::publication()->dao;
            } elseif ($type === 'Representation') {
                $typeDao = Application::getRepresentationDAO();
            } elseif ($type === 'SubmissionFile') {
                $typeDao = Repo::submissionFile()->dao;
            }
            $excludeTypeId = $type === $pubObjectType ? $excludeId : null;
            if (isset($typeDao) && $typeDao->pubIdExists($this->getPubIdType(), $pubId, $excludeTypeId, $contextId)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get the context object.
     *
     * @param int $contextId
     *
     * @return object Context
     */
    public function getContext($contextId)
    {
        assert(is_numeric($contextId));

        // Get the context object from the context (optimized).
        $request = Application::get()->getRequest();
        $router = $request->getRouter();
        $context = $router->getContext($request);
        if ($context && $context->getId() == $contextId) {
            return $context;
        }

        // Fall back the database.
        $contextDao = Application::getContextDAO();
        return $contextDao->getById($contextId);
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\plugins\PKPPubIdPlugin', '\PKPPubIdPlugin');
}
