<?php

/**
 * @file classes/metadata/MetadataDataObjectAdapter.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 MetadataDataObjectAdapter
 *
 * @ingroup metadata
 *
 * @see DataObject
 * @see MetadataSchema
 * @see MetadataDescription
 *
 * @brief Class that injects/extracts a meta-data description
 *  into/from an application entity object (DataObject).
 *
 *  These adapters have to be persistable as they'll be provided
 *  by plug-ins via the filter registry.
 */

namespace PKP\metadata;

use PKP\core\DataObject;
use PKP\filter\ClassTypeDescription;
use PKP\filter\FilterGroup;
use PKP\filter\PersistableFilter;

class MetadataDataObjectAdapter extends PersistableFilter
{
    public const METADATA_DOA_INJECTION_MODE = 1;
    public const METADATA_DOA_EXTRACTION_MODE = 2;

    /** @var int */
    public $_mode;

    /** @var MetadataSchema */
    public $_metadataSchema;

    /** @var string */
    public $_dataObjectClass;

    /** @var array */
    public $_metadataFieldNames;

    /** @var string */
    public $_metadataSchemaName;

    /** @var int */
    public $_assocType;

    /** @var string */
    public $_dataObjectName;

    /** @var DataObject */
    public $_targetDataObject;

    /**
     * Constructor
     *
     * @param FilterGroup $filterGroup
     * @param null|mixed $mode
     */
    public function __construct($filterGroup, $mode = null)
    {
        // Initialize the adapter.
        parent::__construct($filterGroup);

        // Extract information from the input/output types.

        // Find out whether this filter is injecting or
        // extracting meta-data.
        $metadataTypeDescription = null; /** @var MetadataTypeDescription $metadataTypeDescription */
        $dataObjectTypeDescription = null; /** @var ClassTypeDescription $dataObjectTypeDescription */
        $inputType = $this->getInputType();
        $outputType = $this->getOutputType();
        if (is_null($mode)) {
            if ($inputType instanceof \PKP\metadata\MetadataTypeDescription) {
                $mode = self::METADATA_DOA_INJECTION_MODE;
            } else {
                $mode = self::METADATA_DOA_EXTRACTION_MODE;
            }
        }
        $this->_mode = $mode;

        if ($mode == self::METADATA_DOA_INJECTION_MODE) {
            // We are in meta-data injection mode (or both input and output are meta-data descriptions).
            $metadataTypeDescription = & $inputType; /** @var MetadataTypeDescription $metadataTypeDescription */
            assert($outputType instanceof ClassTypeDescription);
            $dataObjectTypeDescription = & $outputType; /** @var ClassTypeDescription $dataObjectTypeDescription */
        } else {
            // We are in meta-data extraction mode.
            assert($outputType instanceof \PKP\metadata\MetadataTypeDescription);
            $metadataTypeDescription = & $outputType;
            assert($inputType instanceof ClassTypeDescription);
            $dataObjectTypeDescription = & $inputType;
        }

        // Extract information from the input/output types.
        $this->_metadataSchemaName = $metadataTypeDescription->getMetadataSchemaClass();
        $this->_assocType = $metadataTypeDescription->getAssocType();
        $this->_dataObjectName = $dataObjectTypeDescription->getTypeName();

        // Set the display name.
        if ($mode == self::METADATA_DOA_INJECTION_MODE) {
            $this->setDisplayName('Inject metadata into a(n) ' . $this->getDataObjectClass());
        } else {
            $this->setDisplayName('Extract metadata from a(n) ' . $this->getDataObjectClass());
        }
    }

    //
    // Getters and setters
    //
    /**
     * One of the METADATA_DOA_*_MODE constants.
     *
     * @return int
     */
    public function getMode()
    {
        return $this->_mode;
    }

    /**
     * Get the fully qualified class name of
     * the supported meta-data schema.
     *
     * @return string
     */
    public function getMetadataSchemaName()
    {
        return $this->_metadataSchemaName;
    }

    /**
     * Get the supported meta-data schema (lazy load)
     *
     * @return MetadataSchema
     */
    public function &getMetadataSchema()
    {
        // Lazy-load the meta-data schema if this has
        // not been done before.
        if (is_null($this->_metadataSchema)) {
            $metadataSchemaName = $this->getMetadataSchemaName();
            assert(!is_null($metadataSchemaName));
            if (preg_match('/^[a-zA-Z0-9_.]+$/', $metadataSchemaName)) {
                // DEPRECATED as of 3.4.0: Schema class names should be fully qualified pkp/pkp-lib#8186
                $this->_metadataSchema = & instantiate($metadataSchemaName, (string) \PKP\metadata\MetadataSchema::class);
            } elseif (class_exists($metadataSchemaName)) {
                $this->_metadataSchema = new $metadataSchemaName();
            }
            if (!$this->_metadataSchema instanceof \PKP\metadata\MetadataSchema) {
                throw new \Exception('Metadata schema is unexpected class!');
            }
            assert(is_object($this->_metadataSchema));
        }
        return $this->_metadataSchema;
    }

    /**
     * Convenience method that returns the
     * meta-data name space.
     *
     * @return string
     */
    public function getMetadataNamespace()
    {
        $metadataSchema = & $this->getMetadataSchema();
        return $metadataSchema->getNamespace();
    }

    /**
     * Get the supported application entity (class) name
     *
     * @return string
     */
    public function getDataObjectName()
    {
        return $this->_dataObjectName;
    }

    /**
     * Return the data object class name
     * (without the package prefix)
     *
     * @return string
     */
    public function getDataObjectClass()
    {
        if (is_null($this->_dataObjectClass)) {
            $dataObjectName = $this->getDataObjectName();
            assert(!is_null($dataObjectName));
            $dataObjectNameParts = explode('.', $dataObjectName);
            $this->_dataObjectClass = array_pop($dataObjectNameParts);
        }
        return $this->_dataObjectClass;
    }

    /**
     * Get the association type corresponding to the data
     * object type.
     *
     * @return int
     */
    public function getAssocType()
    {
        return $this->_assocType;
    }

    /**
     * Set the target data object for meta-data injection.
     *
     * @param DataObject $targetDataObject
     */
    public function setTargetDataObject(&$targetDataObject)
    {
        $this->_targetDataObject = & $targetDataObject;
    }

    /**
     * Get the target data object for meta-data injection.
     */
    public function &getTargetDataObject()
    {
        return $this->_targetDataObject;
    }


    //
    // Abstract template methods
    //
    /**
     * Inject a MetadataDescription into the target DataObject
     *
     * @param MetadataDescription $metadataDescription
     * @param DataObject $targetDataObject
     *
     * @return DataObject
     */
    public function &injectMetadataIntoDataObject(&$metadataDescription, &$targetDataObject)
    {
        // Must be implemented by sub-classes
        assert(false);
    }

    /**
     * Extract a MetadataDescription from a source DataObject.
     *
     * @param DataObject $sourceDataObject
     *
     * @return MetadataDescription
     */
    public function extractMetadataFromDataObject(&$sourceDataObject)
    {
        // Must be implemented by sub-classes
        assert(false);
    }

    /**
     * Return the additional field names introduced by the
     * meta-data schema that need to be persisted in the
     * ..._settings table corresponding to the DataObject
     * which is supported by this adapter.
     * NB: The field names must be prefixed with the meta-data
     * schema namespace identifier.
     *
     * @param bool $translated if true, return localized field
     *  names, otherwise return additional field names.
     *
     * @return array an array of field names to be persisted.
     */
    public function getDataObjectMetadataFieldNames($translated = true)
    {
        // By default return all field names
        return $this->getMetadataFieldNames($translated);
    }


    //
    // Implement template methods from Filter
    //
    /**
     * Convert a MetadataDescription to an application
     * object or vice versa.
     *
     * @see Filter::process()
     *
     * @param mixed $input either a MetadataDescription or an application object
     *
     * @return mixed either a MetadataDescription or an application object
     */
    public function &process(&$input)
    {
        // Do we inject or extract metadata?
        switch ($this->getMode()) {
            case self::METADATA_DOA_INJECTION_MODE:
                $targetDataObject = & $this->getTargetDataObject();

                // Instantiate a new data object if none was given.
                if (is_null($targetDataObject)) {
                    $targetDataObject = & $this->instantiateDataObject();
                    assert(is_a($targetDataObject, $this->getDataObjectName()));
                }

                // Inject meta-data into the data object.
                $output = & $this->injectMetadataIntoDataObject($input, $targetDataObject);
                break;

            case self::METADATA_DOA_EXTRACTION_MODE:
                $output = $this->extractMetadataFromDataObject($input);
                break;

            default:
                // Input should be validated by now.
                assert(false);
        }

        return $output;
    }


    //
    // Protected helper methods
    //
    /**
     * Instantiate a new data object of the
     * correct type.
     *
     * NB: This can be overridden by sub-classes for more complex
     * data objects. The standard implementation assumes there are
     * no constructor args to be set or configurations to be made.
     *
     * @return DataObject
     */
    public function &instantiateDataObject()
    {
        $dataObjectName = $this->getDataObjectName();
        assert(!is_null($dataObjectName));
        $dataObject = & instantiate($dataObjectName, $this->getDataObjectClass());
        return $dataObject;
    }

    /**
     * Instantiate a meta-data description that conforms to the
     * settings of this adapter.
     *
     * @return MetadataDescription
     */
    public function &instantiateMetadataDescription()
    {
        $metadataDescription = new MetadataDescription($this->getMetadataSchemaName(), $this->getAssocType());
        return $metadataDescription;
    }

    /**
     * Return all field names introduced by the
     * meta-data schema that might have to be persisted.
     *
     * @param $translated if true, return localized field
     *  names, otherwise return additional field names.
     *
     * @return An array of field names to be persisted.
     */
    public function getMetadataFieldNames(bool $translated = true): array
    {
        // Do we need to build the field name cache first?
        if (is_null($this->_metadataFieldNames)) {
            // Initialize the cache array
            $this->_metadataFieldNames = [];

            // Retrieve all properties and add
            // their names to the cache
            $metadataSchema = & $this->getMetadataSchema();
            $metadataSchemaNamespace = $metadataSchema->getNamespace();
            $properties = & $metadataSchema->getProperties();
            foreach ($properties as $property) {
                $propertyAssocTypes = $property->getAssocTypes();
                if (in_array($this->_assocType, $propertyAssocTypes)) {
                    // Separate translated and non-translated property names
                    // and add the name space so that field names are unique
                    // across various meta-data schemas.
                    $this->_metadataFieldNames[$property->getTranslated()][] = $metadataSchemaNamespace . ':' . $property->getName();
                }
            }
        }

        // Return the field names
        return $this->_metadataFieldNames[$translated];
    }

    /**
     * Set several localized statements in a meta-data schema.
     *
     * @param MetadataDescription $metadataDescription
     * @param string $propertyName
     * @param array $localizedValues (keys: locale, values: localized values)
     */
    public function addLocalizedStatements(&$metadataDescription, $propertyName, $localizedValues)
    {
        if (is_array($localizedValues)) {
            foreach ($localizedValues as $locale => $values) {
                // Handle cardinality "many" and "one" in the same way.
                if (is_scalar($values)) {
                    $values = [$values];
                }
                foreach ($values as $value) {
                    $metadataDescription->addStatement($propertyName, $value, $locale);
                    unset($value);
                }
            }
        }
    }

    /**
     * Directly inject all fields that are not mapped to the
     * data object into the data object's data array for
     * automatic persistence by the meta-data framework.
     *
     * @param MetadataDescription $metadataDescription
     * @param DataObject $dataObject
     */
    public function injectUnmappedDataObjectMetadataFields(&$metadataDescription, &$dataObject)
    {
        // Handle translated and non-translated statements separately.
        foreach ([true, false] as $translated) {
            // Retrieve the unmapped fields.
            foreach ($this->getDataObjectMetadataFieldNames($translated) as $unmappedProperty) {
                // Identify the corresponding property name.
                [$namespace, $propertyName] = explode(':', $unmappedProperty);

                // Find out whether we have a statement for this unmapped property.
                if ($metadataDescription->hasStatement($propertyName)) {
                    // Add the unmapped statement directly to the
                    // data object.
                    if ($translated) {
                        $dataObject->setData($unmappedProperty, $metadataDescription->getStatementTranslations($propertyName));
                    } else {
                        $dataObject->setData($unmappedProperty, $metadataDescription->getStatement($propertyName));
                    }
                }
            }
        }
    }

    /**
     * Directly extract all fields that are not mapped to the
     * data object from the data object's data array.
     *
     * @param DataObject $dataObject
     * @param MetadataDescription $metadataDescription
     */
    public function extractUnmappedDataObjectMetadataFields(&$dataObject, &$metadataDescription)
    {
        $metadataSchema = & $this->getMetadataSchema();
        $handledNamespace = $metadataSchema->getNamespace();

        // Handle translated and non-translated statements separately.
        foreach ([true, false] as $translated) {
            // Retrieve the unmapped fields.
            foreach ($this->getDataObjectMetadataFieldNames($translated) as $unmappedProperty) {
                // Find out whether we have a statement for this unmapped property.
                if ($dataObject->hasData($unmappedProperty)) {
                    // Identify the corresponding property name and namespace.
                    [$namespace, $propertyName] = explode(':', $unmappedProperty);

                    // Only extract data if the namespace of the property
                    // is the same as the one handled by this adapter and the
                    // property is within the current description.
                    if ($namespace == $handledNamespace && $metadataSchema->hasProperty($propertyName)) {
                        // Add the unmapped statement to the metadata description.
                        if ($translated) {
                            $this->addLocalizedStatements($metadataDescription, $propertyName, $dataObject->getData($unmappedProperty));
                        } else {
                            $metadataDescription->addStatement($propertyName, $dataObject->getData($unmappedProperty));
                        }
                    }
                }
            }
        }
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\metadata\MetadataDataObjectAdapter', '\MetadataDataObjectAdapter');
    define('METADATA_DOA_INJECTION_MODE', MetadataDataObjectAdapter::METADATA_DOA_INJECTION_MODE);
    define('METADATA_DOA_EXTRACTION_MODE', MetadataDataObjectAdapter::METADATA_DOA_EXTRACTION_MODE);
}
