<?php

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

namespace Magento\Eav\Model\Entity;

use Magento\Eav\Model\ReservedAttributeCheckerInterface;
use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator;
use Magento\Framework\Api\AttributeValueFactory;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface;
use Magento\Eav\Model\Config;
use Magento\Eav\Model\Cache\AttributesFormIdentity;

/**
 * EAV Entity attribute model
 *
 * @api
 * @method \Magento\Eav\Model\Entity\Attribute setOption($value)
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 * @since 100.0.2
 */
class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute implements
    \Magento\Framework\DataObject\IdentityInterface
{
    /**
     * Attribute code max length.
     *
     * The value is defined as 60 because in the flat mode attribute code will be transformed into column name.
     * MySQL allows only 64 symbols in column name.
     */
    public const ATTRIBUTE_CODE_MAX_LENGTH = 60;

    /**
     * Min accepted length of an attribute code.
     */
    public const ATTRIBUTE_CODE_MIN_LENGTH = 1;

    /**
     * Tag to use for attributes caching.
     */
    public const CACHE_TAG = 'EAV_ATTRIBUTE';

    /**
     * Prefix of model events names
     *
     * @var string
     */
    protected $_eventPrefix = 'eav_entity_attribute';

    /**
     * Parameter name in event
     *
     * In observe method you can use $observer->getEvent()->getAttribute() in this case
     *
     * @var string
     */
    protected $_eventObject = 'attribute';

    /**
     * @var string
     */
    protected $_cacheTag = self::CACHE_TAG;

    /**
     * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
     */
    protected $_localeDate;

    /**
     * @var \Magento\Catalog\Model\Product\ReservedAttributeList
     *
     * @deprecated Incorrect direct dependency on Product attribute.
     * @see \Magento\Eav\Model\Entity\Attribute::$reservedAttributeChecker
     */
    protected $reservedAttributeList;

    /**
     * @var \Magento\Framework\Locale\ResolverInterface
     */
    protected $_localeResolver;

    /**
     * @var DateTimeFormatterInterface
     */
    protected $dateTimeFormatter;

    /**
     * @var AttributeCodeValidator|null
     */
    private $attributeCodeValidator;

    /**
     * @var ReservedAttributeCheckerInterface|null
     */
    private $reservedAttributeChecker;

    /**
     * @param \Magento\Framework\Model\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory
     * @param AttributeValueFactory $customAttributeFactory
     * @param \Magento\Eav\Model\Config $eavConfig
     * @param TypeFactory $eavTypeFactory
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper
     * @param \Magento\Framework\Validator\UniversalFactory $universalFactory
     * @param \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory $optionDataFactory
     * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor
     * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper
     * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
     * @param \Magento\Catalog\Model\Product\ReservedAttributeList $reservedAttributeList
     * @param \Magento\Framework\Locale\ResolverInterface $localeResolver
     * @param DateTimeFormatterInterface $dateTimeFormatter
     * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
     * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
     * @param array $data
     * @param AttributeCodeValidator|null $attributeCodeValidator
     * @param ReservedAttributeCheckerInterface|null $reservedAttributeChecker
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     * @codeCoverageIgnore
     */
    public function __construct(
        \Magento\Framework\Model\Context $context,
        \Magento\Framework\Registry $registry,
        \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
        AttributeValueFactory $customAttributeFactory,
        \Magento\Eav\Model\Config $eavConfig,
        \Magento\Eav\Model\Entity\TypeFactory $eavTypeFactory,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Eav\Model\ResourceModel\Helper $resourceHelper,
        \Magento\Framework\Validator\UniversalFactory $universalFactory,
        \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory $optionDataFactory,
        \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor,
        \Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
        \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
        \Magento\Catalog\Model\Product\ReservedAttributeList $reservedAttributeList,
        \Magento\Framework\Locale\ResolverInterface $localeResolver,
        DateTimeFormatterInterface $dateTimeFormatter,
        \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
        \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
        array $data = [],
        AttributeCodeValidator $attributeCodeValidator = null,
        ?ReservedAttributeCheckerInterface $reservedAttributeChecker = null
    ) {
        parent::__construct(
            $context,
            $registry,
            $extensionFactory,
            $customAttributeFactory,
            $eavConfig,
            $eavTypeFactory,
            $storeManager,
            $resourceHelper,
            $universalFactory,
            $optionDataFactory,
            $dataObjectProcessor,
            $dataObjectHelper,
            $resource,
            $resourceCollection,
            $data
        );
        $this->_localeDate = $localeDate;
        $this->_localeResolver = $localeResolver;
        $this->reservedAttributeList = $reservedAttributeList;
        $this->dateTimeFormatter = $dateTimeFormatter;
        $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get(
            AttributeCodeValidator::class
        );
        $this->reservedAttributeChecker = $reservedAttributeChecker ?: ObjectManager::getInstance()->get(
            ReservedAttributeCheckerInterface::class
        );
    }

    /**
     * Retrieve default attribute backend model by attribute code
     *
     * @return string
     */
    protected function _getDefaultBackendModel()
    {
        switch ($this->getAttributeCode()) {
            case 'created_at':
                return \Magento\Eav\Model\Entity\Attribute\Backend\Time\Created::class;

            case 'updated_at':
                return \Magento\Eav\Model\Entity\Attribute\Backend\Time\Updated::class;

            case 'store_id':
                return \Magento\Eav\Model\Entity\Attribute\Backend\Store::class;

            case 'increment_id':
                return \Magento\Eav\Model\Entity\Attribute\Backend\Increment::class;

            default:
                break;
        }

        return parent::_getDefaultBackendModel();
    }

    /**
     * Retrieve default attribute source model
     *
     * @return string
     */
    protected function _getDefaultSourceModel()
    {
        if ($this->getAttributeCode() == 'store_id') {
            return \Magento\Eav\Model\Entity\Attribute\Source\Store::class;
        }
        return parent::_getDefaultSourceModel();
    }

    /**
     * Delete entity
     *
     * @return \Magento\Eav\Model\ResourceModel\Entity\Attribute
     * @throws LocalizedException
     * @codeCoverageIgnore
     */
    public function deleteEntity()
    {
        return $this->_getResource()->deleteEntity($this);
    }

    /**
     * Load entity_attribute_id into $this by $this->attribute_set_id
     *
     * @return $this
     */
    public function loadEntityAttributeIdBySet()
    {
        // load attributes collection filtered by attribute_id and attribute_set_id

        $filteredAttributes = $this->getResourceCollection()->setAttributeSetFilter(
            $this->getAttributeSetId()
        )->addFieldToFilter(
            'entity_attribute.attribute_id',
            $this->getId()
        )->load();
        if (count($filteredAttributes) > 0) {
            // getFirstItem() can be used as we can have one or zero records in the collection
            $this->setEntityAttributeId($filteredAttributes->getFirstItem()->getEntityAttributeId());
        }
        return $this;
    }

    /**
     * Prepare data for save
     *
     * @return $this
     * @throws LocalizedException
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function beforeSave()
    {
        if (isset($this->_data['attribute_code'])
            && !$this->attributeCodeValidator->isValid($this->_data['attribute_code'])
        ) {
            $errorMessages = implode("\n", $this->attributeCodeValidator->getMessages());
            throw new LocalizedException(__($errorMessages));
        }

        // prevent overriding product data
        if (isset($this->_data['attribute_code']) && $this->reservedAttributeChecker->isReservedAttribute($this)) {
            throw new LocalizedException(
                __(
                    'The attribute code \'%1\' is reserved by system. Please try another attribute code',
                    $this->_data['attribute_code']
                )
            );
        }

        $this->validateEntityType();

        $defaultValue = $this->getDefaultValue();
        $hasDefaultValue = (string)$defaultValue != '';

        if ($this->getBackendType() == 'decimal' && $hasDefaultValue) {
            $numberFormatter = new \NumberFormatter($this->_localeResolver->getLocale(), \NumberFormatter::DECIMAL);
            $defaultValue = $numberFormatter->parse($defaultValue);
            if ($defaultValue === false) {
                throw new LocalizedException(
                    __('The default decimal value is invalid. Verify the value and try again.')
                );
            }
            $this->setDefaultValue($defaultValue);
        }

        if ($this->getBackendType() == 'datetime') {
            if (!$this->getBackendModel()) {
                $this->setBackendModel(\Magento\Eav\Model\Entity\Attribute\Backend\Datetime::class);
            }

            if (!$this->getFrontendModel()) {
                $this->setFrontendModel(\Magento\Eav\Model\Entity\Attribute\Frontend\Datetime::class);
            }

            // save default date value as timestamp
            if ($hasDefaultValue) {
                $defaultValue = $this->getUtcDateDefaultValue($defaultValue);
                $this->setDefaultValue($defaultValue);
            }
        }

        if ($this->getFrontendInput() == 'media_image') {
            if (!$this->getFrontendModel()) {
                $this->setFrontendModel(\Magento\Catalog\Model\Product\Attribute\Frontend\Image::class);
            }
        }

        if ($this->getBackendType() == 'gallery') {
            if (!$this->getBackendModel()) {
                $this->setBackendModel(\Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend::class);
            }
        }

        return parent::beforeSave();
    }

    /**
     * Convert localized date default value to UTC
     *
     * @param string $defaultValue
     * @return string
     * @throws LocalizedException
     */
    private function getUtcDateDefaultValue(string $defaultValue): string
    {
        $hasTime = $this->getFrontendInput() === 'datetime';
        try {
            $defaultValue = $this->_localeDate->date($defaultValue, null, $hasTime, $hasTime);
            if ($hasTime) {
                $defaultValue->setTimezone(new \DateTimeZone($this->_localeDate->getDefaultTimezone()));
            }
            $utcValue = $defaultValue->format(DateTime::DATETIME_PHP_FORMAT);
        } catch (\Exception $e) {
            throw new LocalizedException(__('The default date is invalid. Verify the date and try again.'));
        }

        return $utcValue;
    }

    /**
     * @inheritdoc
     *
     * @return $this
     * @throws LocalizedException
     */
    public function afterSave()
    {
        $this->_getResource()->saveInSetIncluding($this);
        return parent::afterSave();
    }

    /**
     * Detect backend storage type using frontend input type
     *
     * @param string $type frontend_input field value
     * @return string backend_type field value
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    public function getBackendTypeByInput($type)
    {
        $field = null;
        switch ($type) {
            case 'text':
            case 'gallery':
            case 'media_image':
                $field = 'varchar';
                break;

            case 'image':
            case 'textarea':
            case 'multiselect':
                $field = 'text';
                break;

            case 'date':
            case 'datetime':
                $field = 'datetime';
                break;

            case 'select':
            case 'boolean':
                $field = 'int';
                break;

            case 'price':
            case 'weight':
                $field = 'decimal';
                break;

            default:
                break;
        }

        return $field;
    }

    /**
     * Detect default value using frontend input type
     *
     * @param string $type frontend_input field name
     * @return string default_value field value
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    public function getDefaultValueByInput($type)
    {
        $field = '';
        switch ($type) {
            case 'select':
            case 'gallery':
            case 'media_image':
                break;
            case 'multiselect':
                $field = null;
                break;

            case 'text':
            case 'price':
            case 'image':
            case 'weight':
                $field = 'default_value_text';
                break;

            case 'textarea':
            case 'texteditor':
                $field = 'default_value_textarea';
                break;

            case 'date':
                $field = 'default_value_date';
                break;

            case 'datetime':
                $field = 'default_value_datetime';
                break;

            case 'boolean':
                $field = 'default_value_yesno';
                break;

            default:
                break;
        }

        return $field;
    }

    /**
     * Retrieve attribute codes by frontend type
     *
     * @param string $type
     * @return array
     * @codeCoverageIgnore
     */
    public function getAttributeCodesByFrontendType($type)
    {
        return $this->getResource()->getAttributeCodesByFrontendType($type);
    }

    /**
     * Return array of labels of stores
     *
     * @return string[]
     */
    public function getStoreLabels()
    {
        if (!$this->getData('store_labels')) {
            $storeLabel = $this->getResource()->getStoreLabelsByAttributeId($this->getId());
            $this->setData('store_labels', $storeLabel);
        }
        return $this->getData('store_labels');
    }

    /**
     * Return store label of attribute
     *
     * @param int|null $storeId
     * @return string
     */
    public function getStoreLabel($storeId = null)
    {
        if ($this->hasData('store_label')) {
            return $this->getData('store_label');
        }
        $store = $this->_storeManager->getStore($storeId);
        $labels = $this->getStoreLabels();
        if (isset($labels[$store->getId()])) {
            return $labels[$store->getId()];
        } else {
            return $this->getFrontendLabel();
        }
    }

    /**
     * Get attribute sort weight
     *
     * @param int $setId
     * @return float
     */
    public function getSortWeight($setId)
    {
        $groupSortWeight = isset($this->_data['attribute_set_info'][$setId]['group_sort'])
            ? (float) $this->_data['attribute_set_info'][$setId]['group_sort'] * 1000
            : 0.0;
        $sortWeight = isset($this->_data['attribute_set_info'][$setId]['sort'])
            ? (float) $this->_data['attribute_set_info'][$setId]['sort'] * 0.0001
            : 0.0;
        return $groupSortWeight + $sortWeight;
    }

    /**
     * Get identities
     *
     * @return array
     * @codeCoverageIgnore
     */
    public function getIdentities()
    {
        $identities = [self::CACHE_TAG . '_' . $this->getId()];

        if (($this->hasDataChanges() || $this->isDeleted())) {
            $identities[] = sprintf(
                "%s_%s_ENTITY",
                Config::ENTITIES_CACHE_ID,
                strtoupper($this->getEntityType()->getEntityTypeCode())
            );

            $usedBeforeChange = $this->getOrigData('used_in_forms') ?? [];
            $usedInForms = $this->getUsedInForms() ?? [];

            if (is_array($usedBeforeChange) && is_array($usedInForms) && ($usedBeforeChange != $usedInForms)) {
                $formsToInvalidate = array_merge(
                    array_diff($usedBeforeChange, $usedInForms),
                    array_diff($usedInForms, $usedBeforeChange)
                );

                foreach ($formsToInvalidate as $form) {
                    $identities[] = sprintf(
                        "%s_%s_FORM",
                        AttributesFormIdentity::CACHE_TAG,
                        $form
                    );
                };
            }
        }

        return $identities;
    }

    /**
     * @inheritdoc
     * @since 100.0.7
     */
    public function __sleep()
    {
        $this->unsetData('attribute_set_info');
        return array_diff(
            parent::__sleep(),
            ['_localeDate', '_localeResolver', 'reservedAttributeList', 'dateTimeFormatter']
        );
    }

    /**
     * @inheritdoc
     * @since 100.0.7
     */
    public function __wakeup()
    {
        parent::__wakeup();
        $objectManager = ObjectManager::getInstance();
        $this->_localeDate = $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
        $this->_localeResolver = $objectManager->get(\Magento\Framework\Locale\ResolverInterface::class);
        $this->reservedAttributeList = $objectManager->get(\Magento\Catalog\Model\Product\ReservedAttributeList::class);
        $this->reservedAttributeChecker = $objectManager->get(ReservedAttributeCheckerInterface::class);
        $this->dateTimeFormatter = $objectManager->get(DateTimeFormatterInterface::class);
    }

    /**
     * Entity type for existing attribute shouldn't be changed.
     *
     * @return void
     * @throws LocalizedException
     */
    private function validateEntityType(): void
    {
        if ($this->getId() !== null) {
            $origEntityTypeId = $this->getOrigData('entity_type_id');

            if (($origEntityTypeId !== null) && ((int)$this->getEntityTypeId() !== (int)$origEntityTypeId)) {
                throw new LocalizedException(__('Do not change entity type.'));
            }
        }
    }
}
