<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\Framework\Api\ExtensionAttribute;

use Magento\Framework\Api\ExtensionAttribute\Config;
use Magento\Framework\Api\ExtensionAttribute\Config\Converter;
use Magento\Framework\Data\Collection\AbstractDb as DbCollection;
use Magento\Framework\Reflection\TypeProcessor;
use Magento\Framework\Api\ExtensibleDataInterface;
use Magento\Framework\Api\ExtensionAttributesFactory;

/**
 * Join processor allows to join extension attributes during collections loading.
 */
class JoinProcessor implements \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface
{
    /**
     * Object Manager instance
     *
     * @var \Magento\Framework\ObjectManagerInterface
     */
    protected $objectManager;

    /**
     * @var \Magento\Framework\Reflection\TypeProcessor
     */
    private $typeProcessor;

    /**
     * @var \Magento\Framework\Api\ExtensionAttributesFactory
     */
    private $extensionAttributesFactory;

    /**
     * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorHelper
     */
    private $joinProcessorHelper;

    /**
     * Initialize dependencies.
     *
     * @param \Magento\Framework\ObjectManagerInterface $objectManager
     * @param TypeProcessor $typeProcessor
     * @param ExtensionAttributesFactory $extensionAttributesFactory
     * @param JoinProcessorHelper $joinProcessorHelper
     */
    public function __construct(
        \Magento\Framework\ObjectManagerInterface $objectManager,
        TypeProcessor $typeProcessor,
        ExtensionAttributesFactory $extensionAttributesFactory,
        JoinProcessorHelper $joinProcessorHelper
    ) {
        $this->objectManager = $objectManager;
        $this->typeProcessor = $typeProcessor;
        $this->extensionAttributesFactory = $extensionAttributesFactory;
        $this->joinProcessorHelper = $joinProcessorHelper;
    }

    /**
     * @inheritdoc
     */
    public function process(DbCollection $collection, $extensibleEntityClass = null)
    {
        $extensibleEntityClass = $extensibleEntityClass ?: $collection->getItemObjectClass();
        $joinDirectives = $this->getJoinDirectivesForType($extensibleEntityClass);

        foreach ($joinDirectives as $attributeCode => $directive) {
            /** @var JoinDataInterface $joinData */
            $joinData = $this->joinProcessorHelper->getJoinDataInterface();
            $joinData->setAttributeCode($attributeCode)
                ->setReferenceTable($directive[Converter::JOIN_REFERENCE_TABLE])
                ->setReferenceTableAlias($this->getReferenceTableAlias($attributeCode))
                ->setReferenceField($directive[Converter::JOIN_REFERENCE_FIELD])
                ->setJoinField($directive[Converter::JOIN_ON_FIELD]);
            $joinData->setSelectFields(
                $this->joinProcessorHelper->getSelectFieldsMap($attributeCode, $directive[Converter::JOIN_FIELDS])
            );
            $collection->joinExtensionAttribute($joinData, $this);
        }
    }

    /**
     * Generate reference table alias.
     *
     * @param string $attributeCode
     * @return string
     */
    private function getReferenceTableAlias($attributeCode)
    {
        return 'extension_attribute_' . $attributeCode;
    }

    /**
     * @inheritdoc
     */
    public function extractExtensionAttributes($extensibleEntityClass, array $data)
    {
        if (!$this->isExtensibleAttributesImplemented($extensibleEntityClass)) {
            /* do nothing as there are no extension attributes */
            return $data;
        }

        $joinDirectives = $this->getJoinDirectivesForType($extensibleEntityClass);
        $extensionData = [];
        foreach ($joinDirectives as $attributeCode => $directive) {
            $this->populateAttributeCodeWithDirective(
                $attributeCode,
                $directive,
                $data,
                $extensionData,
                $extensibleEntityClass
            );
        }
        if (!empty($extensionData)) {
            $extensionAttributes = $this->extensionAttributesFactory->create($extensibleEntityClass, $extensionData);
            $data[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY] = $extensionAttributes;
        }
        return $data;
    }

    /**
     * Populate a specific attribute code with join directive instructions.
     *
     * @param string $attributeCode
     * @param array $directive
     * @param array $data
     * @param array $extensionData
     * @param string $extensibleEntityClass
     */
    private function populateAttributeCodeWithDirective(
        $attributeCode,
        $directive,
        &$data,
        &$extensionData,
        $extensibleEntityClass
    ) {
        $attributeType = $directive[Converter::DATA_TYPE];
        $selectFields = $this->joinProcessorHelper
            ->getSelectFieldsMap($attributeCode, $directive[Converter::JOIN_FIELDS]);

        foreach ($selectFields as $selectField) {
            $internalAlias = $selectField[JoinDataInterface::SELECT_FIELD_INTERNAL_ALIAS];
            if (isset($data[$internalAlias])) {
                if ($this->typeProcessor->isArrayType($attributeType)) {
                    throw new \LogicException(
                        sprintf(
                            'Join directives cannot be processed for attribute (%s) of extensible entity (%s),'
                            . ' which has an Array type (%s).',
                            $attributeCode,
                            $this->extensionAttributesFactory->getExtensibleInterfaceName($extensibleEntityClass),
                            $attributeType
                        )
                    );
                } elseif ($this->typeProcessor->isTypeSimple($attributeType)) {
                    $extensionData['data'][$attributeCode] = $data[$internalAlias];
                    unset($data[$internalAlias]);
                    break;
                } else {
                    if (!isset($extensionData['data'][$attributeCode])) {
                        $extensionData['data'][$attributeCode] = $this->objectManager->create($attributeType);
                    }
                    $setterName = $selectField[JoinDataInterface::SELECT_FIELD_SETTER];
                    $extensionData['data'][$attributeCode]->$setterName($data[$internalAlias]);
                    unset($data[$internalAlias]);
                }
            }
        }
    }

    /**
     * Returns the internal join directive config for a given type.
     *
     * Array returned has all of the \Magento\Framework\Api\ExtensionAttribute\Config\Converter JOIN* fields set.
     *
     * @param string $extensibleEntityClass
     * @return array
     */
    private function getJoinDirectivesForType($extensibleEntityClass)
    {
        $extensibleInterfaceName = $this->extensionAttributesFactory
            ->getExtensibleInterfaceName($extensibleEntityClass);
        $extensibleInterfaceName = ltrim($extensibleInterfaceName, '\\');
        $config = $this->joinProcessorHelper->getConfigData();
        if (!isset($config[$extensibleInterfaceName])) {
            return [];
        }

        $typeAttributesConfig = $config[$extensibleInterfaceName];
        $joinDirectives = [];
        foreach ($typeAttributesConfig as $attributeCode => $attributeConfig) {
            if (isset($attributeConfig[Converter::JOIN_DIRECTIVE])) {
                $joinDirectives[$attributeCode] = $attributeConfig[Converter::JOIN_DIRECTIVE];
                $joinDirectives[$attributeCode][Converter::DATA_TYPE] = $attributeConfig[Converter::DATA_TYPE];
            }
        }

        return $joinDirectives;
    }

    /**
     * Determine if the type is an actual extensible data interface.
     *
     * @param string $typeName
     * @return bool
     */
    private function isExtensibleAttributesImplemented($typeName)
    {
        try {
            $this->extensionAttributesFactory->getExtensibleInterfaceName($typeName);
            return true;
        } catch (\LogicException $e) {
            return false;
        }
    }
}
