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

use Magento\Framework\ObjectManagerInterface;

/**
 * @internal
 */
class TMap implements \IteratorAggregate, \Countable, \ArrayAccess
{
    /**
     * @var string
     */
    private $type;

    /**
     * @var array
     */
    private $array = [];

    /**
     * @var array
     */
    private $objectsArray = [];

    /**
     * @var int
     */
    private $counter = 0;

    /**
     * @var ObjectManagerInterface
     */
    private $objectManager;

    /**
     * @var ConfigInterface
     */
    private $configInterface;

    /**
     * @var \Closure
     */
    private $objectCreationStrategy;

    /**
     * @param string $type
     * @param ObjectManagerInterface $objectManager
     * @param ConfigInterface $configInterface
     * @param array $array
     * @param \Closure $objectCreationStrategy
     */
    public function __construct(
        $type,
        ObjectManagerInterface $objectManager,
        ConfigInterface $configInterface,
        array $array = [],
        \Closure $objectCreationStrategy = null
    ) {
        if ((empty($this->type) || !class_exists($this->type)) && !interface_exists($type)) {
            throw new \InvalidArgumentException(sprintf('Unknown type %s', $type));
        }

        $this->type = $type;

        $this->objectManager = $objectManager;
        $this->configInterface = $configInterface;

        array_walk(
            $array,
            function ($item, $index) {
                $this->assertValidTypeLazy($item, $index);
            }
        );

        $this->array = $array;
        $this->counter = count($array);
        $this->objectCreationStrategy = $objectCreationStrategy;
    }

    /**
     * Asserts that item type is collection defined type
     *
     * @param string $instanceName
     * @param string|int|null $index
     * @return void
     * @throws \InvalidArgumentException
     */
    private function assertValidTypeLazy($instanceName, $index = null)
    {
        $realType = $this->configInterface->getInstanceType(
            $this->configInterface->getPreference($instanceName)
        );

        if (!in_array(
            $this->type,
            array_unique(array_merge(class_parents($realType), class_implements($realType))),
            true
        )) {
            $this->throwTypeException($realType, $index);
        }
    }

    /**
     * Throws exception according type mismatch
     *
     * @param string $inputType
     * @param string $index
     * @return void
     * @throws \InvalidArgumentException
     */
    private function throwTypeException($inputType, $index)
    {
        $message = 'Type mismatch. Expected type: %s. Actual: %s, Code: %s';

        throw new \InvalidArgumentException(
            sprintf($message, $this->type, $inputType, $index)
        );
    }

    /**
     * Returns object for requested index
     *
     * @param string|int $index
     * @return object
     */
    private function initObject($index)
    {
        if (isset($this->objectsArray[$index])) {
            return $this->objectsArray[$index];
        }

        $objectCreationStrategy = $this->objectCreationStrategy;
        return $this->objectsArray[$index] = $objectCreationStrategy === null
            ? $this->objectManager->create($this->array[$index])
            : $objectCreationStrategy($this->objectManager, $this->array[$index]);
    }

    /**
     * @inheritdoc
     */
    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        if (array_keys($this->array) != array_keys($this->objectsArray)) {
            foreach (array_keys($this->array) as $index) {
                $this->initObject($index);
            }
        }

        return new \ArrayIterator($this->objectsArray);
    }

    /**
     * @inheritdoc
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return array_key_exists($offset, $this->array);
    }

    /**
     * @inheritdoc
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return isset($this->array[$offset]) ? $this->initObject($offset) : null;
    }

    /**
     * @inheritdoc
     */
    #[\ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        $this->assertValidTypeLazy($value, $offset);
        if ($offset === null) {
            $this->array[] = $value;
        } else {
            $this->array[$offset] = $value;
        }

        $this->counter++;
    }

    /**
     * @inheritdoc
     */
    #[\ReturnTypeWillChange]
    public function offsetUnset($offset)
    {
        if ($this->counter && isset($this->array[$offset])) {
            $this->counter--;
            unset($this->array[$offset]);

            if (isset($this->objectsArray[$offset])) {
                unset($this->objectsArray[$offset]);
            }
        }
    }

    /**
     * @inheritdoc
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return $this->counter;
    }
}
