<?php

/**
 * Part of the Joomla Framework DI Package
 *
 * @copyright  Copyright (C) 2013 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace _JchOptimizeVendor\V91\Joomla\DI;

use _JchOptimizeVendor\V91\Joomla\DI\Exception\DependencyResolutionException;
use _JchOptimizeVendor\V91\Joomla\DI\Exception\KeyNotFoundException;
use _JchOptimizeVendor\V91\Joomla\DI\Exception\ProtectedKeyException;
use _JchOptimizeVendor\V91\Psr\Container\ContainerInterface;

/**
 * The Container class.
 *
 * @since  1.0
 */
class Container implements ContainerInterface
{
    /**
     * Holds the key aliases.
     *
     * Format:
     * 'alias' => 'key'
     *
     * @var    array
     * @since  1.0
     */
    protected $aliases = [];
    /**
     * Holds the resources.
     *
     * @var    ContainerResource[]
     * @since  2.0.0
     */
    protected $resources = [];
    /**
     * Parent for hierarchical containers.
     *
     * In fact, this can be any PSR-11 compatible container, which gets decorated by this
     *
     * @var    Container|ContainerInterface|null
     * @since  1.0
     */
    protected $parent;
    /**
     * Holds the service tag mapping.
     *
     * @var    array
     * @since  1.5.0
     */
    protected $tags = [];
    /**
     * Constructor for the DI Container
     *
     * @param   ContainerInterface|null  $parent  Parent for hierarchical containers.
     *
     * @since   1.0
     */
    public function __construct(?ContainerInterface $parent = null)
    {
        $this->parent = $parent;
    }
    /**
     * Retrieve a resource
     *
     * @param   string  $resourceName  Name of the resource to get.
     *
     * @return  mixed  The requested resource
     *
     * @since   1.0
     * @throws  KeyNotFoundException
     */
    public function get($resourceName)
    {
        $key = $this->resolveAlias($resourceName);
        if (!isset($this->resources[$key])) {
            if ($this->parent instanceof ContainerInterface && $this->parent->has($key)) {
                return $this->parent->get($key);
            }
            throw new KeyNotFoundException(sprintf("Resource '%s' has not been registered with the container.", $resourceName));
        }
        return $this->resources[$key]->getInstance();
    }
    /**
     * Check if specified resource exists.
     *
     * @param   string  $resourceName  Name of the resource to check.
     *
     * @return  boolean  true if key is defined, false otherwise
     *
     * @since   1.5.0
     */
    public function has($resourceName)
    {
        $key = $this->resolveAlias($resourceName);
        if (!isset($this->resources[$key])) {
            if ($this->parent instanceof ContainerInterface) {
                return $this->parent->has($key);
            }
            return \false;
        }
        return \true;
    }
    /**
     * Method to check if specified dataStore key exists.
     *
     * @param   string  $key  Name of the dataStore key to check.
     *
     * @return  boolean  True for success
     *
     * @since   1.0
     * @deprecated  3.0  Use ContainerInterface::has() instead
     */
    public function exists($key)
    {
        trigger_deprecation('joomla/di', '1.5.0', '%s() is deprecated and will be removed in 3.0, use %s::has() instead.', __METHOD__, ContainerInterface::class);
        return $this->has($key);
    }
    /**
     * Create an alias for a given key for easy access.
     *
     * @param   string  $alias  The alias name
     * @param   string  $key    The key to alias
     *
     * @return  $this
     *
     * @since   1.0
     */
    public function alias($alias, $key)
    {
        $this->aliases[$alias] = $key;
        return $this;
    }
    /**
     * Resolve a resource name.
     *
     * If the resource name is an alias, the corresponding key is returned.
     * If the resource name is not an alias, the resource name is returned unchanged.
     *
     * @param   string  $resourceName  The key to search for.
     *
     * @return  string
     *
     * @since   1.0
     */
    protected function resolveAlias($resourceName)
    {
        return $this->aliases[$resourceName] ?? $resourceName;
    }
    /**
     * Check whether a resource is shared
     *
     * @param   string  $resourceName  Name of the resource to check.
     *
     * @return  boolean
     *
     * @since   2.0.0
     */
    public function isShared(string $resourceName): bool
    {
        return $this->hasFlag($resourceName, 'isShared', \true);
    }
    /**
     * Check whether a resource is protected
     *
     * @param   string  $resourceName  Name of the resource to check.
     *
     * @return  boolean
     *
     * @since   2.0.0
     */
    public function isProtected(string $resourceName): bool
    {
        return $this->hasFlag($resourceName, 'isProtected', \true);
    }
    /**
     * Check whether a flag (i.e., one of 'shared' or 'protected') is set
     *
     * @param   string   $resourceName  Name of the resource to check.
     * @param   string   $method        Method to delegate to
     * @param   boolean  $default       Default return value
     *
     * @return  boolean
     *
     * @since   2.0.0
     * @throws  KeyNotFoundException
     */
    private function hasFlag(string $resourceName, string $method, bool $default = \true): bool
    {
        $key = $this->resolveAlias($resourceName);
        if (isset($this->resources[$key])) {
            return \call_user_func([$this->resources[$key], $method]);
        }
        if ($this->parent instanceof self) {
            return \call_user_func([$this->parent, $method], $key);
        }
        if ($this->parent instanceof ContainerInterface && $this->parent->has($key)) {
            // We don't know if the parent supports the 'shared' or 'protected' concept, so we assume the default
            return $default;
        }
        throw new KeyNotFoundException(sprintf("Resource '%s' has not been registered with the container.", $resourceName));
    }
    /**
     * Assign a tag to services.
     *
     * @param   string  $tag   The tag name
     * @param   array   $keys  The service keys to tag
     *
     * @return  $this
     *
     * @since   1.5.0
     */
    public function tag($tag, array $keys)
    {
        foreach ($keys as $key) {
            $resolvedKey = $this->resolveAlias($key);
            if (!isset($this->tags[$tag])) {
                $this->tags[$tag] = [];
            }
            $this->tags[$tag][] = $resolvedKey;
        }
        // Prune duplicates
        $this->tags[$tag] = array_unique($this->tags[$tag]);
        return $this;
    }
    /**
     * Fetch all services registered to the given tag.
     *
     * @param   string  $tag  The tag name
     *
     * @return  array  The resolved services for the given tag
     *
     * @since   1.5.0
     */
    public function getTagged($tag)
    {
        $services = [];
        if (isset($this->tags[$tag])) {
            foreach ($this->tags[$tag] as $service) {
                $services[] = $this->get($service);
            }
        }
        return $services;
    }
    /**
     * Build an object of the requested class
     *
     * Creates an instance of the class specified by $resourceName with all dependencies injected.
     * If the dependencies cannot be completely resolved, a DependencyResolutionException is thrown.
     *
     * @param   string   $resourceName  The class name to build.
     * @param   boolean  $shared        True to create a shared resource.
     *
     * @return  object|false  Instance of class specified by $resourceName with all dependencies injected.
     *                        Returns an object if the class exists and false otherwise
     *
     * @since   1.0
     * @throws  DependencyResolutionException if the object could not be built (due to missing information)
     */
    public function buildObject($resourceName, $shared = \false)
    {
        static $buildStack = [];
        $key = $this->resolveAlias($resourceName);
        if (\in_array($key, $buildStack, \true)) {
            $buildStack = [];
            throw new DependencyResolutionException(sprintf('Cannot resolve circular dependency for "%s"', $key));
        }
        $buildStack[] = $key;
        if ($this->has($key)) {
            $resource = $this->get($key);
            array_pop($buildStack);
            return $resource;
        }
        try {
            $reflection = new \ReflectionClass($key);
        } catch (\ReflectionException $e) {
            array_pop($buildStack);
            return \false;
        }
        if (!$reflection->isInstantiable()) {
            $buildStack = [];
            if ($reflection->isInterface()) {
                throw new DependencyResolutionException(sprintf('There is no service for "%s" defined, cannot autowire a class service for an interface.', $key));
            }
            if ($reflection->isAbstract()) {
                throw new DependencyResolutionException(sprintf('There is no service for "%s" defined, cannot autowire an abstract class.', $key));
            }
            throw new DependencyResolutionException(sprintf('"%s" cannot be instantiated.', $key));
        }
        $constructor = $reflection->getConstructor();
        // If there are no parameters, just return a new object.
        if ($constructor === null) {
            // There is no constructor, just return a new object.
            $callback = function () use ($key) {
                return new $key();
            };
        } else {
            $newInstanceArgs = $this->getMethodArgs($constructor);
            $callback = function () use ($reflection, $newInstanceArgs) {
                return $reflection->newInstanceArgs($newInstanceArgs);
            };
        }
        $this->set($key, $callback, $shared);
        $resource = $this->get($key);
        array_pop($buildStack);
        return $resource;
    }
    /**
     * Convenience method for building a shared object.
     *
     * @param   string  $resourceName  The class name to build.
     *
     * @return  object|false  Instance of class specified by $resourceName with all dependencies injected.
     *                        Returns an object if the class exists and false otherwise
     *
     * @since   1.0
     */
    public function buildSharedObject($resourceName)
    {
        return $this->buildObject($resourceName, \true);
    }
    /**
     * Create a child Container with a new property scope that has the ability to access the parent scope when resolving.
     *
     * @return  Container  A new container with the current as a parent
     *
     * @since   1.0
     */
    public function createChild()
    {
        return new static($this);
    }
    /**
     * Extend a defined service Closure by wrapping the existing one with a new callable function.
     *
     * This works very similar to a decorator pattern.  Note that this only works on service Closures
     * that have been defined in the current container, not parent containers.
     *
     * @param   string    $resourceName  The unique identifier for the Closure or property.
     * @param   callable  $callable      A callable to wrap the original service Closure.
     *
     * @return  void
     *
     * @since   1.0
     * @throws  KeyNotFoundException
     */
    public function extend($resourceName, callable $callable)
    {
        $key = $this->resolveAlias($resourceName);
        $resource = $this->getResource($key, \true);
        $closure = function ($c) use ($callable, $resource) {
            return $callable($resource->getInstance(), $c);
        };
        $this->set($key, $closure, $resource->isShared());
    }
    /**
     * Build an array of method arguments.
     *
     * @param   \ReflectionMethod  $method  Method for which to build the argument array.
     *
     * @return  array  Array of arguments to pass to the method.
     *
     * @since   1.0
     * @throws  DependencyResolutionException
     */
    private function getMethodArgs(\ReflectionMethod $method): array
    {
        $methodArgs = [];
        foreach ($method->getParameters() as $param) {
            // Check for a typehinted dependency
            if ($param->hasType()) {
                $dependency = $param->getType();
                // Don't support PHP 8 union types
                if ($dependency instanceof \ReflectionUnionType) {
                    // If this is a nullable parameter, then don't error out
                    if ($param->allowsNull()) {
                        $methodArgs[] = null;
                        continue;
                    }
                    throw new DependencyResolutionException(sprintf('Could not resolve the parameter "$%s" of "%s::%s()": Union typehints are not supported.', $param->name, $method->class, $method->name));
                }
                // Check for a class, if it doesn't have one then it is a scalar type, which we cannot handle if a mandatory argument
                if ($dependency->isBuiltin()) {
                    // If the param is optional, then fall through to the optional param handling later in this method
                    if (!$param->isOptional()) {
                        $message = 'Could not resolve the parameter "$%s" of "%s::%s()":';
                        $message .= ' Scalar parameters cannot be autowired and the parameter does not have a default value.';
                        throw new DependencyResolutionException(sprintf($message, $param->name, $method->class, $method->name));
                    }
                } else {
                    $dependencyClassName = $dependency->getName();
                    // Check that class or interface exists
                    if (!interface_exists($dependencyClassName) && !class_exists($dependencyClassName)) {
                        // If this is a nullable parameter, then don't error out
                        if ($param->allowsNull()) {
                            $methodArgs[] = null;
                            continue;
                        }
                        throw new DependencyResolutionException(sprintf('Could not resolve the parameter "$%s" of "%s::%s()": The "%s" class does not exist.', $param->name, $method->class, $method->name, $dependencyClassName));
                    }
                    // If the dependency class name is registered with this container or a parent, use it.
                    if ($this->getResource($dependencyClassName) !== null) {
                        $depObject = $this->get($dependencyClassName);
                    } else {
                        try {
                            $depObject = $this->buildObject($dependencyClassName);
                        } catch (DependencyResolutionException $exception) {
                            // If this is a nullable parameter, then don't error out
                            if ($param->allowsNull()) {
                                $methodArgs[] = null;
                                continue;
                            }
                            $message = 'Could not resolve the parameter "$%s" of "%s::%s()":';
                            $message .= ' No service for "%s" exists and the dependency could not be autowired.';
                            throw new DependencyResolutionException(sprintf($message, $param->name, $method->class, $method->name, $dependencyClassName), 0, $exception);
                        }
                    }
                    if ($depObject instanceof $dependencyClassName) {
                        $methodArgs[] = $depObject;
                        continue;
                    }
                }
            }
            // If there is a default parameter and it can be read, use it.
            if ($param->isOptional() && $param->isDefaultValueAvailable()) {
                try {
                    $methodArgs[] = $param->getDefaultValue();
                    continue;
                } catch (\ReflectionException $exception) {
                    throw new DependencyResolutionException(sprintf('Could not resolve the parameter "$%s" of "%s::%s()": Unable to read the default parameter value.', $param->name, $method->class, $method->name), 0, $exception);
                }
            }
            // If an untyped variadic argument, skip it
            if (!$param->hasType() && $param->isVariadic()) {
                continue;
            }
            // At this point the argument cannot be resolved, most likely cause is an untyped required argument
            throw new DependencyResolutionException(sprintf('Could not resolve the parameter "$%s" of "%s::%s()": The argument is untyped and has no default value.', $param->name, $method->class, $method->name));
        }
        return $methodArgs;
    }
    /**
     * Set a resource to the container. If the value is null the resource is removed.
     *
     * @param   string   $key        Name of resources key to set.
     * @param   mixed    $value      Callable function to run or string to retrive when requesting the specified $key.
     * @param   boolean  $shared     True to create and store a shared instance.
     * @param   boolean  $protected  True to protect this item from being overwritten. Useful for services.
     *
     * @return  $this
     *
     * @since   1.0
     * @throws  ProtectedKeyException  Thrown if the provided key is already set and is protected.
     */
    public function set($key, $value, $shared = \false, $protected = \false)
    {
        $key = $this->resolveAlias($key);
        $hasKey = $this->has($key);
        if ($hasKey && $this->isProtected($key)) {
            throw new ProtectedKeyException(sprintf("Key %s is protected and can't be overwritten.", $key));
        }
        if ($value === null && $hasKey) {
            unset($this->resources[$key]);
            return $this;
        }
        $mode = $shared ? ContainerResource::SHARE : ContainerResource::NO_SHARE;
        $mode |= $protected ? ContainerResource::PROTECT : ContainerResource::NO_PROTECT;
        $this->resources[$key] = new ContainerResource($this, $value, $mode);
        return $this;
    }
    /**
     * Shortcut method for creating protected keys.
     *
     * @param   string   $key     Name of dataStore key to set.
     * @param   mixed    $value   Callable function to run or string to retrive when requesting the specified $key.
     * @param   boolean  $shared  True to create and store a shared instance.
     *
     * @return  $this
     *
     * @since   1.0
     */
    public function protect($key, $value, $shared = \false)
    {
        return $this->set($key, $value, $shared, \true);
    }
    /**
     * Shortcut method for creating shared keys.
     *
     * @param   string   $key        Name of dataStore key to set.
     * @param   mixed    $value      Callable function to run or string to retrive when requesting the specified $key.
     * @param   boolean  $protected  True to protect this item from being overwritten. Useful for services.
     *
     * @return  $this
     *
     * @since   1.0
     */
    public function share($key, $value, $protected = \false)
    {
        return $this->set($key, $value, \true, $protected);
    }
    /**
     * Get the raw data assigned to a key.
     *
     * @param   string   $key   The key for which to get the stored item.
     * @param   boolean  $bail  Throw an exception, if the key is not found
     *
     * @return  ContainerResource|null  The resource if present, or null if instructed to not bail
     *
     * @since   2.0.0
     * @throws  KeyNotFoundException
     */
    public function getResource(string $key, bool $bail = \false): ?ContainerResource
    {
        if (isset($this->resources[$key])) {
            return $this->resources[$key];
        }
        if ($this->parent instanceof self) {
            return $this->parent->getResource($key);
        }
        if ($this->parent instanceof ContainerInterface && $this->parent->has($key)) {
            return new ContainerResource($this, $this->parent->get($key), ContainerResource::SHARE | ContainerResource::PROTECT);
        }
        if ($bail) {
            throw new KeyNotFoundException(sprintf('Key %s has not been registered with the container.', $key));
        }
        return null;
    }
    /**
     * Method to force the container to return a new instance of the results of the callback for requested $key.
     *
     * @param   string  $key  Name of the resources key to get.
     *
     * @return  mixed   Results of running the callback for the specified key.
     *
     * @since   1.0
     */
    public function getNewInstance($key)
    {
        $key = $this->resolveAlias($key);
        $this->getResource($key, \true)->reset();
        return $this->get($key);
    }
    /**
     * Register a service provider to the container.
     *
     * @param   ServiceProviderInterface  $provider  The service provider to register.
     *
     * @return  $this
     *
     * @since   1.0
     */
    public function registerServiceProvider(ServiceProviderInterface $provider)
    {
        $provider->register($this);
        return $this;
    }
    /**
     * Retrieve the keys for services assigned to this container.
     *
     * @return  array
     *
     * @since   1.5.0
     */
    public function getKeys()
    {
        return array_unique(array_merge(array_keys($this->aliases), array_keys($this->resources)));
    }
}
