<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */
declare (strict_types=1);
namespace Packetery\Nette\ComponentModel;

use Packetery\Nette;
/**
 * ComponentContainer is default implementation of IContainer.
 *
 * @property-read \Iterator $components
 */
class Container extends Component implements IContainer
{
    private const NameRegexp = '#^[a-zA-Z0-9_]+$#D';
    /** @var IComponent[] */
    private $components = [];
    /** @var Container|null */
    private $cloning;
    /********************* interface IContainer ****************d*g**/
    /**
     * Adds the component to the container.
     * @return static
     * @throws \Packetery\Nette\InvalidStateException
     */
    public function addComponent(IComponent $component, ?string $name, ?string $insertBefore = null)
    {
        if ($name === null) {
            $name = $component->getName();
            if ($name === null) {
                throw new \Packetery\Nette\InvalidStateException("Missing component's name.");
            }
        }
        if (!\preg_match(self::NameRegexp, $name)) {
            throw new \Packetery\Nette\InvalidArgumentException("Component name must be non-empty alphanumeric string, '{$name}' given.");
        }
        if (isset($this->components[$name])) {
            throw new \Packetery\Nette\InvalidStateException("Component with name '{$name}' already exists.");
        }
        // check circular reference
        $obj = $this;
        do {
            if ($obj === $component) {
                throw new \Packetery\Nette\InvalidStateException("Circular reference detected while adding component '{$name}'.");
            }
            $obj = $obj->getParent();
        } while ($obj !== null);
        // user checking
        $this->validateChildComponent($component);
        if (isset($this->components[$insertBefore])) {
            $tmp = [];
            foreach ($this->components as $k => $v) {
                if ((string) $k === $insertBefore) {
                    $tmp[$name] = $component;
                }
                $tmp[$k] = $v;
            }
            $this->components = $tmp;
        } else {
            $this->components[$name] = $component;
        }
        try {
            $component->setParent($this, $name);
        } catch (\Throwable $e) {
            unset($this->components[$name]);
            // undo
            throw $e;
        }
        return $this;
    }
    /**
     * Removes the component from the container.
     */
    public function removeComponent(IComponent $component) : void
    {
        $name = $component->getName();
        if (($this->components[$name] ?? null) !== $component) {
            throw new \Packetery\Nette\InvalidArgumentException("Component named '{$name}' is not located in this container.");
        }
        unset($this->components[$name]);
        $component->setParent(null);
    }
    /**
     * Returns component specified by name or path.
     * @param  bool  $throw  throw exception if component doesn't exist?
     */
    public final function getComponent(string $name, bool $throw = \true) : ?IComponent
    {
        [$name] = $parts = \explode(self::NameSeparator, $name, 2);
        if (!isset($this->components[$name])) {
            if (!\preg_match(self::NameRegexp, $name)) {
                if ($throw) {
                    throw new \Packetery\Nette\InvalidArgumentException("Component name must be non-empty alphanumeric string, '{$name}' given.");
                }
                return null;
            }
            $component = $this->createComponent($name);
            if ($component && !isset($this->components[$name])) {
                $this->addComponent($component, $name);
            }
        }
        $component = $this->components[$name] ?? null;
        if ($component !== null) {
            if (!isset($parts[1])) {
                return $component;
            } elseif ($component instanceof IContainer) {
                return $component->getComponent($parts[1], $throw);
            } elseif ($throw) {
                throw new \Packetery\Nette\InvalidArgumentException("Component with name '{$name}' is not container and cannot have '{$parts[1]}' component.");
            }
        } elseif ($throw) {
            $hint = \Packetery\Nette\Utils\ObjectHelpers::getSuggestion(\array_merge(\array_map('strval', \array_keys($this->components)), \array_map('lcfirst', \preg_filter('#^createComponent([A-Z0-9].*)#', '$1', \get_class_methods($this)))), $name);
            throw new \Packetery\Nette\InvalidArgumentException("Component with name '{$name}' does not exist" . ($hint ? ", did you mean '{$hint}'?" : '.'));
        }
        return null;
    }
    /**
     * Component factory. Delegates the creation of components to a createComponent<Name> method.
     */
    protected function createComponent(string $name) : ?IComponent
    {
        $ucname = \ucfirst($name);
        $method = 'createComponent' . $ucname;
        if ($ucname !== $name && \method_exists($this, $method) && (new \ReflectionMethod($this, $method))->getName() === $method) {
            $component = $this->{$method}($name);
            if (!$component instanceof IComponent && !isset($this->components[$name])) {
                $class = static::class;
                throw new \Packetery\Nette\UnexpectedValueException("Method {$class}::{$method}() did not return or create the desired component.");
            }
            return $component;
        }
        return null;
    }
    /**
     * Iterates over descendants components.
     * @return \Iterator<int|string,IComponent>
     */
    public final function getComponents(bool $deep = \false, ?string $filterType = null) : \Iterator
    {
        $iterator = new RecursiveComponentIterator($this->components);
        if ($deep) {
            $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);
        }
        if ($filterType) {
            $iterator = new \CallbackFilterIterator($iterator, function ($item) use($filterType) {
                return $item instanceof $filterType;
            });
        }
        return $iterator;
    }
    /**
     * Descendant can override this method to disallow insert a child by throwing an \Packetery\Nette\InvalidStateException.
     * @throws \Packetery\Nette\InvalidStateException
     */
    protected function validateChildComponent(IComponent $child) : void
    {
    }
    /********************* cloneable, serializable ****************d*g**/
    /**
     * Object cloning.
     */
    public function __clone()
    {
        if ($this->components) {
            $oldMyself = \reset($this->components)->getParent();
            \assert($oldMyself instanceof self);
            $oldMyself->cloning = $this;
            foreach ($this->components as $name => $component) {
                $this->components[$name] = clone $component;
            }
            $oldMyself->cloning = null;
        }
        parent::__clone();
    }
    /**
     * Is container cloning now?
     * @internal
     */
    public final function _isCloning() : ?self
    {
        return $this->cloning;
    }
}
