<?php
/**
 * The Dependency Injection container.
 *
 * @package lucatume\DI52
 *
 * @license GPL-3.0
 * Modified using {@see https://github.com/BrianHenryIE/strauss}.
 */

namespace KadenceWP\KadenceBlocks\lucatume\DI52;

use ArrayAccess;
use Closure;
use Exception;
use KadenceWP\KadenceBlocks\lucatume\DI52\Builders\BuilderInterface;
use KadenceWP\KadenceBlocks\lucatume\DI52\Builders\ValueBuilder;
use KadenceWP\KadenceBlocks\Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReturnTypeWillChange;
use Throwable;
use function spl_object_hash;

/**
 * Class Container
 *
 * @package lucatume\DI52
 * @implements ArrayAccess<string,object>
 */
class Container implements ArrayAccess, ContainerInterface
{
    const EXCEPTION_MASK_NONE = 0;
    const EXCEPTION_MASK_MESSAGE = 1;
    const EXCEPTION_MASK_FILE_LINE = 2;

    /**
     * An array cache to store the results of the class exists checks.
     *
     * @var array<string,bool>
     */
    protected $classIsInstantiatableCache = [];
    /**
     * A cache of what methods are static and what are not.
     *
     * @var array<string,bool>
     */
    protected $isStaticMethodCache = [];
    /**
     * A list of bound and resolved singletons.
     *
     * @var array<string|class-string,bool>
     */
    protected $singletons = [];
    /**
     * @var array<ServiceProvider>
     */
    protected $deferred = [];
    /**
     * @var array<string,array<string|object|callable>>
     */
    protected $tags = [];
    /**
     * @var array<ServiceProvider>
     */
    protected $bootable = [];
    /**
     * @var string
     */
    protected $whenClass;
    /**
     * @var string
     */
    protected $needsClass;
    /**
     * A map from class name and static methods to the built callback.
     *
     * @var array<string,Closure>
     */
    protected $callbacks = [];
    /**
     * @var Builders\Resolver
     */
    protected $resolver;
    /**
     * @var Builders\Factory
     */
    protected $builders;
    /**
     * What kind of masking should be applied to throwables catched by the container during resolution.
     *
     * @var int
     */
    private $maskThrowables = self::EXCEPTION_MASK_MESSAGE | self::EXCEPTION_MASK_FILE_LINE;

    /**
     * Container constructor.
     *
     * @param false $resolveUnboundAsSingletons Whether unbound classes should be resolved as singletons by default,
     *                                          or not.
     */
    public function __construct($resolveUnboundAsSingletons = false)
    {
        $this->resolver = new Builders\Resolver($resolveUnboundAsSingletons);
        $this->builders = new Builders\Factory($this, $this->resolver);
        $this->bindThis();
    }

    /**
     * Sets a variable on the container.
     *
     * @param string $key   The alias the container will use to reference the variable.
     * @param mixed  $value The variable value.
     *
     * @return void The method does not return any value.
     */
    public function setVar($key, $value)
    {
        $this->resolver->bind($key, ValueBuilder::of($value));
    }

    /**
     * Sets a variable on the container using the ArrayAccess API.
     *
     * When using the container as an array bindings will be bound as singletons.
     * These are equivalent: `$container->singleton('foo','ClassOne');`, `$container['foo'] = 'ClassOne';`.
     *
     * @param string $offset The alias the container will use to reference the variable.
     * @param mixed  $value  The variable value.
     *
     * @return void This method does not return any value.
     *
     * @throws ContainerException If the closure building fails.
     */
    #[ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        $this->singleton($offset, $value);
    }

    /**
     * Binds an interface a class or a string slug to an implementation and will always return the same instance.
     *
     * @param string             $id                A class or interface fully qualified name or a string slug.
     * @param mixed              $implementation    The implementation that should be bound to the alias(es); can be a
     *                                              class name, an object or a closure.
     * @param array<string>|null $afterBuildMethods An array of methods that should be called on the built
     *                                              implementation after resolving it.
     *
     * @return void This method does not return any value.
     * @throws ContainerException If there's any issue reflecting on the class, interface or the implementation.
     */
    public function singleton($id, $implementation = null, ?array $afterBuildMethods = null)
    {
        if ($implementation === null) {
            $implementation = $id;
        }

        $this->resolver->singleton($id, $this->builders->getBuilder($id, $implementation, $afterBuildMethods));
    }

    /**
     * Returns a variable stored in the container.
     *
     * If the variable is a binding then the binding will be resolved before returning it.
     *
     * @param string     $key     The alias of the variable or binding to fetch.
     * @param mixed|null $default A default value to return if the variable is not set in the container.
     *
     * @return mixed The variable value or the resolved binding.
     * @throws ContainerException If there's an issue resolving the variable.
     *
     * @see Container::get()
     */
    public function getVar($key, $default = null)
    {
        if ($this->resolver->isBound($key)) {
            return $this->resolver->resolve($key);
        }

        return $default;
    }

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @template T
     *
     * @param string|class-string<T> $offset Identifier of the entry to look for.
     *
     * @return T|mixed The value for the offset.
     * @phpstan-return ($offset is class-string ? T : mixed)
     *
     * @throws ContainerException Error while retrieving the entry.
     * @throws NotFoundException  No entry was found for **this** identifier.
     */
    #[ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @template T
     *
     * @param  string|class-string<T>  $id  A fully qualified class or interface name or an already built object.
     *
     * @return T|mixed The entry for an id.
     * @phpstan-return ($id is class-string ? T : mixed)
     *
     * @throws ContainerException Error while retrieving the entry.
     */
    public function get($id)
    {
        try {
            return $this->resolver->resolve($id, [$id]);
        } catch (Throwable $throwable) {
            throw $this->castThrown($throwable, $id);
            // @codeCoverageIgnoreStart
        } catch (Exception $exception) { // @phan-suppress-current-line PhanUnreachableCatch @phpstan-ignore-line
            throw $this->castThrown($exception, $id);
            // @codeCoverageIgnoreEnd
        }
    }

    /**
     * Builds an instance of the exception with a pretty message.
     *
     * @param Exception|Throwable $thrown The exception to cast.
     * @param string|object $id The top identifier the containe was attempting to build, or object.
     *
     * @return ContainerException|Exception|Throwable The cast exception.
     */
    private function castThrown($thrown, $id)
    {
        if ($this->maskThrowables === self::EXCEPTION_MASK_NONE) {
            return $thrown;
        }

        return ContainerException::fromThrowable(
            $id,
            $thrown,
            $this->maskThrowables,
            $this->resolver->getBuildLine()
        );
    }

    /**
     * Returns an instance of the class or object bound to an interface, class  or string slug if any, else it will try
     * to automagically resolve the object to a usable instance.
     *
     * If the implementation has been bound as singleton using the `singleton` method
     * or the ArrayAccess API then the implementation will be resolved just on the first request.
     *
     * @template T
     *
     * @param string|class-string<T> $id A fully qualified class or interface name or an already built object.
     *
     * @return T|mixed
     * @phpstan-return ($id is class-string ? T : mixed)
     *
     * @throws ContainerException If the target of the make is not bound and is not a valid,
     *                                              concrete, class name or there's any issue making the target.
     */
    public function make($id)
    {
        return $this->get($id);
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `$container[$id]` returning true does not mean that `$container[$id]` will not throw an exception.
     * It does however mean that `$container[$id]` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string|class-string $offset An offset to check for.
     *
     * @return boolean true on success or false on failure.
     */
    #[ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return $this->has($offset);
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string|class-string $id Identifier of the entry to look for.
     *
     * @return bool Whether the container contains a binding for an id or not.
     */
    public function has($id)
    {
        return $this->resolver->isBound($id) || class_exists($id);
    }

    /**
     * Tags an array of implementations bindings for later retrieval.
     *
     * The implementations can also reference interfaces, classes or string slugs.
     * Example:
     *
     *        $container->tag(['Posts', 'Users', 'Comments'], 'endpoints');
     *
     * @param array<string|callable|object> $implementationsArray The ids, class names or objects to apply the tag to.
     * @param string                        $tag                  The tag to apply.
     *
     * @return void This method does not return any value.
     * @see Container::tagged()
     *
     */
    public function tag(array $implementationsArray, $tag)
    {
        $this->tags[$tag] = $implementationsArray;
    }

    /**
     * Retrieves an array of bound implementations resolving them.
     *
     * The array of implementations should be bound using the `tag` method:
     *
     *        $container->tag(['Posts', 'Users', 'Comments'], 'endpoints');
     *        foreach($container->tagged('endpoints') as $endpoint){
     *            $endpoint->register();
     *        }
     *
     * @param string $tag The tag to return the tagged values for.
     *
     * @return array<mixed> An array of resolved bound implementations.
     * @throws NotFoundException If nothing is tagged with the tag.
     * @throws ContainerException If one of the bindings is not of the correct type.
     * @see Container::tag()
     */
    public function tagged($tag)
    {
        if (!$this->hasTag($tag)) {
            throw new NotFoundException("Nothing is tagged as '{$tag}'");
        }

        return array_map(
            function ($id) {
                if (is_string($id)) {
                    return $this->get($id);
                }
                return $this->builders->getBuilder($id)->build();
            },
            $this->tags[$tag]
        );
    }

    /**
     * Checks whether a tag group exists in the container.
     *
     * @param string $tag
     *
     * @return bool
     * @see Container::tag()
     *
     */
    public function hasTag($tag)
    {
        return isset($this->tags[$tag]);
    }

    /**
     * A wrapper around the `class_exists` function to capture and handle possible fatal errors on PHP 7.0+.
     *
     * @param string|class-string $class The class name to check.
     *
     * @return bool Whether the class exists or not.
     *
     * @throws ContainerException|ReflectionException If the class has syntax or other errors preventing its load.
     */
    protected function classIsInstantiable($class)
    {
        if (isset($this->classIsInstantiatableCache[$class])) {
            return $this->classIsInstantiatableCache[$class];
        }

        // @codeCoverageIgnoreStart
        if (PHP_VERSION_ID < 70000) {
            $isInstantiatable = $this->checkClassIsInstantiatable($class);
            $this->classIsInstantiatableCache[$class] = $isInstantiatable;
            return $isInstantiatable;
        }
        // @codeCoverageIgnoreEnd

        // PHP 7.0+ allows handling fatal errors; x_exists will trigger auto-loading, that might result in an error.
        try {
            $isInstantiatable = $this->checkClassIsInstantiatable($class);
            $this->classIsInstantiatableCache[$class] = $isInstantiatable;
            return $isInstantiatable;
        } catch (Throwable $e) {
            $this->classIsInstantiatableCache[$class] = false;
            throw new ContainerException($e->getMessage());
        }
    }

    /**
     * Checks a class, interface or trait exists.
     *
     * @param string|class-string $class The class, interface or trait to check.
     *
     * @return bool Whether the class, interface or trait exists or not.
     * @throws ReflectionException If the class should be checked for concreteness and it does not exist.
     */
    protected function checkClassIsInstantiatable($class)
    {
        $exists = class_exists($class);
        if (!$exists) {
            return false;
        }
        $classReflection = new ReflectionClass($class);
        if ($classReflection->isAbstract()) {
            return false;
        }
        $constructor = $classReflection->getConstructor();
        if ($constructor === null) {
            return true;
        }
        return $constructor->isPublic();
    }

    /**
     * Registers a service provider implementation.
     *
     * The `register` method will be called immediately on the service provider.
     *
     * If the provider overloads the  `isDeferred` method returning a truthy value then the `register` method will be
     * called only if one of the implementations provided by the provider is requested. The container defines which
     * implementations is offering overloading the `provides` method; the method should return an array of provided
     * implementations.
     *
     * If a provider overloads the `boot` method that method will be called when the `boot` method is called on the
     * container itself.
     *
     * @param class-string $serviceProviderClass The fully-qualified Service Provider class name.
     * @param string ...$alias             A list of aliases the provider should be registered with.
     * @return void This method does not return any value.
     * @throws ContainerException If the Service Provider is not correctly configured or there's an issue
     *                                     reflecting on it.
     * @see ServiceProvider::register()
     * @see ServiceProvider::isDeferred()
     * @see ServiceProvider::provides()
     * @see Container::getProvider()
     * @see ServiceProvider::boot()
     */
    public function register($serviceProviderClass, ...$alias)
    {
        /** @var ServiceProvider $provider */
        $provider = $this->get($serviceProviderClass);
        if (!$provider->isDeferred()) {
            $provider->register();
        } else {
            $provided = $provider->provides();
            // @phpstan-ignore-next-line
            if (!is_array($provided) || count($provided) === 0) {
                throw new ContainerException(
                    "Service provider '{$serviceProviderClass}' is marked as deferred" .
                    " but is not providing any implementation."
                );
            }
            foreach ($provided as $id) {
                $this->resolver->bind(
                    $id,
                    $this->builders->getBuilder($this->getDeferredProviderMakeClosure($provider, $id))
                );
            }
        }

        try {
            $bootMethod = new ReflectionMethod($provider, 'boot');
        } catch (ReflectionException $e) {
            throw new ContainerException('Could not reflect on the provider boot method.');
        }

        $requiresBoot = ($bootMethod->getDeclaringClass()->getName() === get_class($provider));
        if ($requiresBoot) {
            $this->bootable[] = $provider;
        }
        $this->resolver->singleton($serviceProviderClass, new ValueBuilder($provider));
        foreach ($alias as $a) {
            $this->resolver->singleton($a, new ValueBuilder($provider));
        }
    }

    /**
     * Returns a closure that will build a provider on demand, if an implementation provided by the provider is
     * required.
     *
     * @param ServiceProvider $provider The provider instance to register.
     * @param string          $id       The id of the implementation to bind.
     *
     * @return Closure A Closure ready to be bound to the id as implementation.
     */
    private function getDeferredProviderMakeClosure(ServiceProvider $provider, $id)
    {
        return function () use ($provider, $id) {
            static $registered;
            if ($registered === null) {
                $provider->register();
                $registered = true;
            }

            return $this->get($id);
        };
    }

    /**
     * Binds an interface, a class or a string slug to an implementation.
     *
     * Existing implementations are replaced.
     *
     * @param  string|class-string  $id                 A class or interface fully qualified name or a string slug.
     * @param  mixed                $implementation     The implementation that should be bound to the alias(es); can
     *                                                  be a class name, an object or a closure.
     * @param  string[]|null        $afterBuildMethods  An array of methods that should be called on the built
     *                                                  implementation after resolving it.
     *
     * @return void The method does not return any value.
     *
     * @throws ContainerException      If there's an issue while trying to bind the implementation.
     */
    public function bind($id, $implementation = null, ?array $afterBuildMethods = null)
    {
        if ($implementation === null) {
            $implementation = $id;
        }
        if ($implementation === $id && !$this->classIsInstantiable($implementation)) {
            throw new NotFoundException("Class {$implementation} does not exist.");
        }
        $this->resolver->bind($id, $this->builders->getBuilder($id, $implementation, $afterBuildMethods));
    }

    /**
     * Boots up the application calling the `boot` method of each registered service provider.
     *
     * If there are bootable providers (providers overloading the `boot` method) then the `boot` method will be
     * called on each bootable provider.
     *
     * @return void This method does not return any value.
     *
     * @see ServiceProvider::boot()
     */
    public function boot()
    {
        if (!empty($this->bootable)) {
            foreach ($this->bootable as $provider) {
                /** @var ServiceProvider $provider */
                $provider->boot();
            }
        }
    }

    /**
     * Binds a class, interface or string slug to a chain of implementations decorating a base
     * object; the chain will be lazily resolved only on the first call.
     * The base decorated object must be the last element of the array.
     *
     * @param  string|class-string            $id                 The class, interface or slug the decorator chain
     *                                                            should be bound to.
     * @param  array<string|object|callable>  $decorators         An array of implementations that decorate an object.
     * @param  string[]|null                  $afterBuildMethods  An array of methods that should be called on the
     *                                                            instance after it has been built; the methods should
     *                                                            not require any argument.
     * @param bool $afterBuildAll                                 Whether to call the after build methods on only the
     *                                                            base instance or all instances of the decorator chain.
     *
     * @return void This method does not return any value.
     * @throws ContainerException
     */
    public function singletonDecorators($id, $decorators, ?array $afterBuildMethods = null, $afterBuildAll = false)
    {
        $this->resolver->singleton(
            $id,
            $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods, $afterBuildAll)
        );
    }

    /**
     * Builds and returns a closure that will start building the chain of decorators.
     *
     * @param array<string|object|callable> $decorators        A list of decorators.
     * @param string                        $id                The id to bind the decorator tail to.
     * @param array<string>|null            $afterBuildMethods A set of method to run on the built decorated instance
     *                                                         after it's built.
     * @param bool $afterBuildAll                              Whether to run the after build methods only on the base
     *                                                         instance (default, false) or on all instances of the
     *                                                         decorator chain.
     *
     * @return BuilderInterface The callable or Closure that will start building the decorator chain.
     *
     * @throws ContainerException If there's any issue while trying to register any decorator step.
     */
    private function getDecoratorBuilder(
        array $decorators,
        $id,
        ?array $afterBuildMethods = null,
        $afterBuildAll = false
    ) {
        $decorator = array_pop($decorators);

        if ($decorator === null) {
            throw new ContainerException('The decorator chain cannot be empty.');
        }

        do {
            $previous = isset($builder) ? $builder : null;
            $builder = $this->builders->getBuilder($id, $decorator, $afterBuildMethods, $previous);
            $decorator = array_pop($decorators);
            if (!$afterBuildAll) {
                $afterBuildMethods = [];
            }
        } while ($decorator !== null);

        return $builder;
    }

    /**
     * Binds a class, interface or string slug to a chain of implementations decorating a
     * base object.
     *
     * The base decorated object must be the last element of the array.
     *
     * @param  string|class-string            $id                 The class, interface or slug the decorator chain
     *                                                            should be bound to.
     * @param  array<string|object|callable>  $decorators         An array of implementations that decorate an object.
     * @param  string[]|null                  $afterBuildMethods  An array of methods that should be called on the
     *                                                            base instance after it has been built; the methods
     *                                                            should not require any argument.
     * @param bool $afterBuildAll                                 Whether to call the after build methods on only the
     *                                                            base instance or all instances of the decorator chain.
     *
     * @return void This method does not return any value.
     * @throws ContainerException If there's any issue binding the decorators.
     */
    public function bindDecorators($id, array $decorators, ?array $afterBuildMethods = null, $afterBuildAll = false)
    {
        $this->resolver->bind($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods, $afterBuildAll));
    }

    /**
     * Unsets a binding or tag in the container.
     *
     * @param mixed $offset The offset to unset.
     *
     * @return void The method does not return any value.
     */
    #[ReturnTypeWillChange]
    public function offsetUnset($offset)
    {
        if (!is_string($offset)) {
            return;
        }
        $this->resolver->unbind($offset);
        unset($this->tags[$offset]);
    }

    /**
     * Starts the `when->needs->give` chain for a contextual binding.
     *
     * @param string|class-string $class The fully qualified name of the requesting class.
     *
     * Example:
     *
     *      // Any class requesting an implementation of `LoggerInterface` will receive this implementation ...
     *      $container->singleton('LoggerInterface', 'FilesystemLogger');
     *      // But if the requesting class is `Worker` return another implementation
     *      $container->when('Worker')
     *          ->needs('LoggerInterface')
     *          ->give('RemoteLogger');
     *
     * @return Container The container instance, to continue the when/needs/give chain.
     */
    public function when($class)
    {
        $this->whenClass = $class;

        return $this;
    }

    /**
     * Second step of the `when->needs->give` chain for a contextual binding.
     *
     * Example:
     *
     *      // Any class requesting an implementation of `LoggerInterface` will receive this implementation ...
     *      $container->singleton('LoggerInterface', 'FilesystemLogger');
     *      // But if the requesting class is `Worker` return another implementation.
     *      $container->when('Worker')
     *          ->needs('LoggerInterface')
     *          ->give('RemoteLogger');
     *
     * @param string|class-string $id The class or interface needed by the class.
     *
     * @return Container The container instance, to continue the when/needs/give chain.
     */
    public function needs($id)
    {
        $this->needsClass = $id;

        return $this;
    }

    /**
     * Third step of the `when->needs->give` chain for a contextual binding.
     *
     * Example:
     *
     *      // any class requesting an implementation of `LoggerInterface` will receive this implementation...
     *      $container->singleton('LoggerInterface', 'FilesystemLogger');
     *      // but if the requesting class is `Worker` return another implementation
     *      $container->when('Worker')
     *          ->needs('LoggerInterface')
     *          ->give('RemoteLogger');
     *
     * @param mixed $implementation The implementation specified
     *
     * @return void This method does not return any value.
     * @throws NotFoundException
     */
    public function give($implementation)
    {
        $id = "{$this->whenClass}::{$this->needsClass}";
        $builder = $this->builders->getBuilder($id, $implementation);
        $this->resolver->setWhenNeedsGive($this->whenClass, $this->needsClass, $builder);
        // @phpstan-ignore unset.possiblyHookedProperty
        unset($this->whenClass, $this->needsClass); // @phpstan-ignore unset.possiblyHookedProperty
    }

    /**
     * Returns a lambda function suitable to use as a callback; when called the function will build the implementation
     * bound to `$id` and return the value of a call to `$method` method with the call arguments.
     *
     * @param  string|class-string|object  $id      A fully-qualified class name, a bound slug or an object o call the
     *                                              callback on.
     * @param  string                      $method  The method that should be called on the resolved implementation
     *                                              with the specified array arguments.
     *
     * @return callable|Closure The callback function.
     * @throws ContainerException If the id is not a bound implementation or valid class name.
     */
    public function callback($id, $method)
    {
        $callbackIdPrefix = is_object($id) ? spl_object_hash($id) : $id;

        // @phpstan-ignore-next-line
        if (!is_string($callbackIdPrefix)) {
            $typeOfId = gettype($id);
            throw new ContainerException(
                "Callbacks can only be built on ids, class names or objects; '{$typeOfId}' is neither."
            );
        }

        // @phpstan-ignore-next-line
        if (!is_string($method)) {
            throw new ContainerException("Callbacks second argument must be a string method name.");
        }

        $callbackId = $callbackIdPrefix . '::' . $method;

        if (isset($this->callbacks[$callbackId])) {
            return $this->callbacks[$callbackId];
        }

        $callbackClosure = function (...$args) use ($id, $method) {
            $instance = is_string($id) ?
                $this->resolver->resolve($id)
                : $this->builders->getBuilder($id)->build();
            return $instance->{$method}(...$args);
        };

        if (is_string($id) && ($this->resolver->isSingleton($id) || $this->isStaticMethod($id, $method))) {
            // If we can know immediately, without actually resolving the binding, then build and cache immediately.
            $this->callbacks[$callbackId] = $callbackClosure;
        }

        return $callbackClosure;
    }

    /**
     * Whether a method of an id, possibly not a class, is static or not.
     *
     * @param  object|string|class-string  $object  A class name, instance or something that does not map to a class.
     * @param  string                      $method  The method to check.
     *
     * @return bool Whether a method of an id or class is static or not.
     */
    protected function isStaticMethod($object, $method)
    {
        $key = is_string($object) ? $object . '::' . $method : get_class($object) . '::' . $method;

        if (!isset($this->isStaticMethodCache[$key])) {
            try {
                $this->isStaticMethodCache[$key] = (new ReflectionMethod($object, $method))->isStatic();
            } catch (ReflectionException $e) {
                return false;
            }
        }

        return $this->isStaticMethodCache[$key];
    }

    /**
     * Returns a callable object that will build an instance of the specified class using the
     * specified arguments when called.
     * The callable will be a closure on PHP 5.3+ or a lambda function on PHP 5.2.
     *
     * @param  string|class-string|mixed  $id                 The fully qualified name of a class or an interface.
     * @param  array<mixed>               $buildArgs          An array of arguments that should be used to build the
     *                                                        instance; note that any argument will be resolved using
     *                                                        the container itself and bindings will apply.
     * @param  string[]|null              $afterBuildMethods  An array of methods that should be called on the built
     *                                                        implementation after resolving it.
     *
     * @return callable|Closure  A callable function that will return an instance of the specified class when
     *                   called.
     */
    public function instance($id, array $buildArgs = [], ?array $afterBuildMethods = null)
    {
        return function () use ($id, $afterBuildMethods, $buildArgs) {
            if (is_string($id)) {
                return $this->resolver->resolveWithArgs($id, $afterBuildMethods, ...$buildArgs);
            }

            return $this->builders->getBuilder($id, $id, $afterBuildMethods, ...$buildArgs)->build();
        };
    }

    /**
     * Protects a value to make sure it will not be resolved, if callable or if the name of an existing class.
     *
     * @param mixed $value The value to protect.
     *
     * @return ValueBuilder A protected value instance, its value set to the provided value.
     */
    public function protect($value)
    {
        return new ValueBuilder($value);
    }

    /**
     * Returns the Service Provider instance registered.
     *
     * @param string|class-string $providerId The Service Provider clas to return the instance for.
     *
     * @return ServiceProvider The service provider instance.
     *
     * @throws NotFoundException|ContainerException If the Service Provider class was never registered in the container
     *                                              or there's an issue retrieving it.
     */
    public function getProvider($providerId)
    {
        if (!$this->resolver->isBound($providerId)) {
            throw new NotFoundException("Service provider '$providerId' is not registered in the container.");
        }

        $provider = $this->get($providerId);

        if (! $provider instanceof ServiceProvider) {
            throw new NotFoundException("Bound implementation for '$providerId' is not Service Provider.");
        }

        return $provider;
    }

    /**
     * Returns whether a binding exists in the container or not.
     *
     * `isBound($id)` returning `true` means the a call to `bind($id, $implementaion)` or `singleton($id,
     * $implementation)` (or equivalent ArrayAccess methods) was explicitly made.
     *
     * @param string $id The id to check for bindings in the container.
     *
     * @return bool Whether an explicit binding for the id exists in the container or not.
     */
    public function isBound($id)
    {
        // @phpstan-ignore-next-line
        return is_string($id) && $this->resolver->isBound($id);
    }

    /**
     * Sets the mask for the throwables that should be caught and re-thrown as container exceptions.
     *
     * @param int $maskThrowables The mask for the throwables that should be caught and re-thrown as container
     *
     * @return void
     */
    public function setExceptionMask($maskThrowables)
    {
        $this->maskThrowables = (int)$maskThrowables;
    }

    /**
     * Binds the container to the base class name, the current class name and the container interface.
     *
     * @return void
     */
    private function bindThis()
    {
        $this->singleton(ContainerInterface::class, $this);
        $this->singleton(Container::class, $this);
        if (get_class($this) !== Container::class) {
            $this->singleton(get_class($this), $this);
        }
    }

    /**
     * Upon cloning, clones the resolver and builders instances.
     *
     * @return void
     */
    public function __clone()
    {
        $this->resolver = clone $this->resolver;
        $this->builders = clone $this->builders;
        $this->builders->setContainer($this);
        $this->builders->setResolver($this->resolver);
        $this->bindThis();
    }
}
