<?php

/* @description Dice - A minimal Dependency Injection Container for PHP
 * @author Tom Butler tom@r.je
 * @copyright 2012-2020 Tom Butler <tom@r.je> | https://r.je/dice
 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
 * @version 4.0 */
namespace Avatar_Privacy\Vendor\Dice;

class Dice
{
    const CONSTANT = 'Dice::CONSTANT';
    const GLOBAL = 'Dice::GLOBAL';
    const INSTANCE = 'Dice::INSTANCE';
    const CHAIN_CALL = 'Dice::CHAIN_CALL';
    const SELF = 'Dice::SELF';
    /**
     * @var array $rules Rules which have been set using addRule()
     */
    private $rules = [];
    /**
     * @var array $cache A cache of closures based on class name so each class is only reflected once
     */
    private $cache = [];
    /**
     * @var array $instances Stores any instances marked as 'shared' so create() can return the same instance
     */
    private $instances = [];
    /**
     * Add a rule $rule to the class $name
     * @param string $name The name of the class to add the rule for
     * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
     */
    public function addRule(string $name, array $rule) : self
    {
        $dice = clone $this;
        $this->addRuleTo($dice, $name, $rule);
        return $dice;
    }
    /**
     * Add rules as array. Useful for JSON loading $dice->addRules(json_decode(file_get_contents('foo.json'));
     * @param array Rules in a single array [name => $rule] format
     */
    public function addRules($rules) : self
    {
        if (\is_string($rules)) {
            $rules = \json_decode(\file_get_contents($rules), \true);
        }
        $dice = clone $this;
        foreach ($rules as $name => $rule) {
            $this->addRuleTo($dice, $name, $rule);
        }
        return $dice;
    }
    private function addRuleTo(Dice $dice, string $name, array $rule)
    {
        if (isset($rule['instanceOf']) && (!\array_key_exists('inherit', $rule) || $rule['inherit'] === \true)) {
            $rule = \array_replace_recursive($dice->getRule($rule['instanceOf']), $rule);
        }
        //Allow substitutions rules to be defined with a leading a slash
        if (isset($rule['substitutions'])) {
            foreach ($rule['substitutions'] as $key => $value) {
                $rule['substitutions'][\ltrim($key, '\\')] = $value;
            }
        }
        //Clear any existing instance or cache for this class
        unset($dice->instances[$name], $dice->cache[$name]);
        $dice->rules[\ltrim(\strtolower($name), '\\')] = \array_replace_recursive($dice->getRule($name), $rule);
    }
    /**
     * Returns the rule that will be applied to the class $name when calling create()
     * @param string name The name of the class to get the rules for
     * @return array The rules for the specified class
     */
    public function getRule(string $name) : array
    {
        $lcName = \strtolower(\ltrim($name, '\\'));
        if (isset($this->rules[$lcName])) {
            return $this->rules[$lcName];
        }
        foreach ($this->rules as $key => $rule) {
            // Find a rule which matches the class described in $name where:
            if (empty($rule['instanceOf']) && $key !== '*' && \is_subclass_of($name, $key) && (!\array_key_exists('inherit', $rule) || $rule['inherit'] === \true)) {
                // And that rule should be inherited to subclasses
                return $rule;
            }
        }
        // No rule has matched, return the default rule if it's set
        return isset($this->rules['*']) ? $this->rules['*'] : [];
    }
    /**
     * Returns a fully constructed object based on $name using $args and $share as constructor arguments if supplied
     * @param string name The name of the class to instantiate
     * @param array $args An array with any additional arguments to be passed into the constructor upon instantiation
     * @param array $share a list of defined in shareInstances for objects higher up the object graph, should only be used internally
     * @return object A fully constructed object based on the specified input arguments
     */
    public function create(string $name, array $args = [], array $share = [])
    {
        // Is there a shared instance set? Return it. Better here than a closure for this, calling a closure is slower.
        if (!empty($this->instances[$name])) {
            return $this->instances[$name];
        }
        // Create a closure for creating the object if there isn't one already
        if (empty($this->cache[$name])) {
            $this->cache[$name] = $this->getClosure(\ltrim($name, '\\'), $this->getRule($name));
        }
        // Call the cached closure which will return a fully constructed object of type $name
        return $this->cache[$name]($args, $share);
    }
    /**
     * Returns a closure for creating object $name based on $rule, caching the reflection object for later use
     * @param string $name the Name of the class to get the closure for
     * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
     * @return callable A closure
     */
    private function getClosure(string $name, array $rule)
    {
        // Reflect the class and constructor, this should only ever be done once per class and get cached
        $class = new \ReflectionClass(isset($rule['instanceOf']) ? $rule['instanceOf'] : $name);
        $constructor = $class->getConstructor();
        // Create parameter generating function in order to cache reflection on the parameters. This way $reflect->getParameters() only ever gets called once
        $params = $constructor ? $this->getParams($constructor, $rule) : null;
        //PHP throws a fatal error rather than an exception when trying to instantiate an interface, detect it and throw an exception instead
        if ($class->isInterface()) {
            $closure = function () {
                throw new \InvalidArgumentException('Cannot instantiate interface');
            };
        } else {
            if ($params) {
                $closure = function (array $args, array $share) use($class, $params) {
                    // This class has depenencies, call the $params closure to generate them based on $args and $share
                    return new $class->name(...$params($args, $share));
                };
            } else {
                $closure = function () use($class) {
                    // No constructor arguments, just instantiate the class
                    return new $class->name();
                };
            }
        }
        if (!empty($rule['shared'])) {
            $closure = function (array $args, array $share) use($class, $name, $constructor, $params, $closure) {
                //Internal classes may not be able to be constructed without calling the constructor and will not suffer from #7, construct them normally.
                if ($class->isInternal()) {
                    $this->instances[$class->name] = $this->instances['\\' . $class->name] = $closure($args, $share);
                } else {
                    //Otherwise, create the class without calling the constructor (and write to \$name and $name, see issue #68)
                    $this->instances[$name] = $this->instances['\\' . $name] = $class->newInstanceWithoutConstructor();
                    // Now call this constructor after constructing all the dependencies. This avoids problems with cyclic references (issue #7)
                    if ($constructor) {
                        $constructor->invokeArgs($this->instances[$name], $params($args, $share));
                    }
                }
                return $this->instances[$name];
            };
        }
        // If there are shared instances, create them and merge them with shared instances higher up the object graph
        if (isset($rule['shareInstances'])) {
            $closure = function (array $args, array $share) use($closure, $rule) {
                foreach ($rule['shareInstances'] as $instance) {
                    $share[] = $this->create($instance, [], $share);
                }
                return $closure($args, $share);
            };
        }
        // When $rule['call'] is set, wrap the closure in another closure which will call the required methods after constructing the object
        // By putting this in a closure, the loop is never executed unless call is actually set
        return isset($rule['call']) ? function (array $args, array $share) use($closure, $class, $rule, $name) {
            // Construct the object using the original closure
            $object = $closure($args, $share);
            foreach ($rule['call'] as $call) {
                // Generate the method arguments using getParams() and call the returned closure
                $params = $this->getParams($class->getMethod($call[0]), ['shareInstances' => isset($rule['shareInstances']) ? $rule['shareInstances'] : []])($this->expand(isset($call[1]) ? $call[1] : []), $share);
                $return = $object->{$call[0]}(...$params);
                if (isset($call[2])) {
                    if ($call[2] === self::CHAIN_CALL) {
                        if (!empty($rule['shared'])) {
                            $this->instances[$name] = $return;
                        }
                        if (\is_object($return)) {
                            $class = new \ReflectionClass(\get_class($return));
                        }
                        $object = $return;
                    } else {
                        if (\is_callable($call[2])) {
                            \call_user_func($call[2], $return);
                        }
                    }
                }
            }
            return $object;
        } : $closure;
    }
    /**
     * Looks for Dice::INSTANCE, Dice::GLOBAL or Dice::CONSTANT array keys in $param and when found returns an object based on the value see {@link https:// r.je/dice.html#example3-1}
     * @param mixed $param Either a string or an array,
     * @param array $share Array of instances from 'shareInstances', required for calls to `create`
     * @param bool $createFromString
     * @return mixed
     */
    private function expand($param, array $share = [], bool $createFromString = \false)
    {
        if (\is_array($param)) {
            //if a rule specifies Dice::INSTANCE, look up the relevant instance
            if (isset($param[self::INSTANCE])) {
                if ($param[self::INSTANCE] === self::SELF) {
                    return $this;
                }
                //Check for 'params' which allows parameters to be sent to the instance when it's created
                //Either as a callback method or to the constructor of the instance
                $args = isset($param['params']) ? $this->expand($param['params']) : [];
                //Support Dice::INSTANCE by creating/fetching the specified instance
                if (\is_array($param[self::INSTANCE])) {
                    $param[self::INSTANCE][0] = $this->expand($param[self::INSTANCE][0], $share, \true);
                }
                if (\is_callable($param[self::INSTANCE])) {
                    return \call_user_func($param[self::INSTANCE], ...$args);
                } else {
                    return $this->create($param[self::INSTANCE], \array_merge($args, $share));
                }
            } else {
                if (isset($param[self::GLOBAL])) {
                    return $GLOBALS[$param[self::GLOBAL]];
                } else {
                    if (isset($param[self::CONSTANT])) {
                        return \constant($param[self::CONSTANT]);
                    } else {
                        foreach ($param as $name => $value) {
                            $param[$name] = $this->expand($value, $share);
                        }
                    }
                }
            }
        }
        return \is_string($param) && $createFromString ? $this->create($param) : $param;
    }
    /**
    	* Looks through the array $search for any object which can be used to fulfil $param
    	The original array $search is modifed so must be passed by reference.
    */
    private function matchParam(\ReflectionParameter $param, $class, array &$search)
    {
        foreach ($search as $i => $arg) {
            if ($class && ($arg instanceof $class || $arg === null && $param->allowsNull())) {
                // The argument matched, return it and remove it from $search so it won't wrongly match another parameter
                return \array_splice($search, $i, 1)[0];
            }
        }
        return \false;
    }
    /**
     * Returns a closure that generates arguments for $method based on $rule and any $args passed into the closure
     * @param object $method An instance of ReflectionMethod (see: {@link http:// php.net/manual/en/class.reflectionmethod.php})
     * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules.
     * @return callable A closure that uses the cached information to generate the arguments for the method
     */
    private function getParams(\ReflectionMethod $method, array $rule)
    {
        // Cache some information about the parameter in $paramInfo so (slow) reflection isn't needed every time
        $paramInfo = [];
        foreach ($method->getParameters() as $param) {
            $type = $param->getType();
            $class = $type instanceof \ReflectionNamedType && !$type->isBuiltIn() ? $type->getName() : null;
            $paramInfo[] = [$class, $param, isset($rule['substitutions']) && \array_key_exists($class, $rule['substitutions'])];
        }
        // Return a closure that uses the cached information to generate the arguments for the method
        return function (array $args, array $share = []) use($paramInfo, $rule) {
            // If the rule has construtParams set, construct any classes reference and use them as $args
            if (isset($rule['constructParams'])) {
                $args = \array_merge($args, $this->expand($rule['constructParams'], $share));
            }
            // Array of matched parameters
            $parameters = [];
            // Fnd a value for each method argument
            foreach ($paramInfo as list($class, $param, $sub)) {
                // Loop through $args and see whether or not each value can match the current parameter based on type hint
                if ($args && ($match = $this->matchParam($param, $class, $args)) !== \false) {
                    $parameters[] = $match;
                } else {
                    if (($copy = $share) && ($match = $this->matchParam($param, $class, $copy)) !== \false) {
                        $parameters[] = $match;
                    } else {
                        if ($class) {
                            try {
                                if ($sub) {
                                    $parameters[] = $this->expand($rule['substitutions'][$class], $share, \true);
                                } else {
                                    $parameters[] = !$param->allowsNull() ? $this->create($class, [], $share) : null;
                                }
                            } catch (\InvalidArgumentException $e) {
                            }
                        } else {
                            if ($args && $param->getType()) {
                                for ($i = 0; $i < \count($args); $i++) {
                                    if (\call_user_func('is_' . $param->getType()->getName(), $args[$i])) {
                                        $parameters[] = \array_splice($args, $i, 1)[0];
                                        break;
                                    }
                                }
                            } else {
                                if ($args) {
                                    $parameters[] = $this->expand(\array_shift($args));
                                } else {
                                    if ($param->isVariadic()) {
                                        $parameters = \array_merge($parameters, $args);
                                    } else {
                                        $parameters[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            return $parameters;
        };
    }
}
