<?php

namespace Uid\Utils\Container;

use Psr\Container\ContainerInterface as PsrContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
use function array_key_exists;
use function array_map;
use function class_exists;
use function class_implements;
use function class_parents;
use function count;
use function get_class;
use function in_array;
use function is_object;
use function is_string;
use function method_exists;
use function sprintf;

/** @inheritdoc*/
class Container implements ContainerInterface
{
    private array $definitions;

    public function __construct(array $definitions = [])
    {
        $this->definitions = $definitions;
        $this->definitions[ContainerInterface::class] = ['alias' => Container::class];
        $this->definitions[PsrContainerInterface::class] = ['alias' => Container::class];
        $this->definitions[self::class] = ['value' => $this];
    }
    
    public function value(string $name, $value): self
    {
        $this->definitions[$name] = ['value' => $value];
        return $this;
    }

    public function alias(string $name, string $alias): self
    {
        $this->definitions[$name] = ['alias' => $alias];
        return $this;
    }
    
    public function create(string $class, array $constants = [], bool $singleton = false): self
    {
        return $this->set($class, $class, $constants, $singleton);
    }

    public function factory(string $name, callable $callback, array $constants = [], bool $singleton = false): self
    {
        $this->definitions[$name] = ['callback' => $this->callback($callback, $constants), 'singleton' => $singleton];
        return $this;
    }

    public function set(string $name, string|callable $value, array $constants = [], bool $singleton = false): self
    {
        $this->definitions[$name] = [
            'callback' => is_string($value) 
                ? $this->constructor($value, $constants)
                : $this->callback($value, $constants), 
            'singleton' => $singleton
        ];
        return $this;
    }


    /** @inheritdoc*/
    public function has(string $id): bool
    {
        return $this->definition($this->resolve($id)) !== null;
    }

    /** @inheritdoc*/
    public function get(string $id): mixed
    {
        $name = $this->resolve($id);
        $definition = $this->definition($name);
        if ($definition === null) {
            throw new NotFoundException(sprintf("Cannot find definition for '%s'", $name));
        }
        if (array_key_exists('value', $definition)) {
            return $definition['value'];
        }
        $result = ($definition['callback'])($this);
        if (isset($definition['singleton'])) {
            $this->definitions[$name]['value'] = $result;
        }
        return $result;
    }

    public function invoke(string|object $entry, string $method = '__invoke', array $constants = []): mixed
    {
        $object = is_string($entry) ? $this->get($entry) : $entry;
        if (!is_object($object)) {
            throw new ContainerException(sprintf("Unable to invoke method '%s' not exists for non-object '%s'", $method, $entry));
        }
        if (!method_exists($object, $method)) {
            throw new NotFoundException(sprintf("Method '%s' does not exist for instance of '%s'", $method, get_class($object)));
        }
        try {
            $reflection = new ReflectionMethod($object, $method);
        } catch (ReflectionException $ex) {
            throw new ContainerException(sprintf("Unable to invoke method '%s' for instance of '%s'", $method, get_class($object)), 0, $ex);
        }
        $arguments = array_map(fn($parameter) => $this->argument($parameter, $constants), $reflection->getParameters());
        return $object->$method(... array_map(fn($arg) => $arg[0] ? $this->get($arg[1]): $arg[1], $arguments));
    }
    
    private function definition(string $name): ?array
    {
        if (array_key_exists($name, $this->definitions)) {
            return $this->definitions[$name];
        } elseif (class_exists($name)) {
            $this->definitions[$name] = ['callback' => $this->constructor($name), 'singleton' => false];
            return $this->definitions[$name]; 
        }
        return null;
    }

    private function resolve(string $id): string
    {
        while (array_key_exists($id, $this->definitions)) {
            if (array_key_exists('alias', $this->definitions[$id])) {
                $id = $this->definitions[$id]['alias'];
            } else {
                return $id;
            }
        }
        return $id;
    }

    private function constructor(string $class, array $constants = []): callable
    {
        try {
            $reflection = new ReflectionClass($class);
        } catch (ReflectionException $e) {
            throw new ContainerException(sprintf("Unable to create object '%s'", $class), 0, $e);
        }
        $arguments = [];
        if (($constructor = $reflection->getConstructor())) {
            $arguments = array_map(fn($parameter) => $this->argument($parameter, $constants), $constructor->getParameters());
        }
        return fn(Container $container) => new $class(... array_map(fn($arg) => $arg[0] ? $container->get($arg[1]) : $arg[1], $arguments));
    }

    private function callback(callable $callback, array $constants = []): callable
    {
        try {
            $reflection = new ReflectionFunction($callback);
        } catch (ReflectionException $ex) {
            throw new ContainerException("Unable to prepare factory", 0, $ex);
        }
        $parameters = $reflection->getParameters();
        if (empty($parameters)) {
            return $callback;
        }
        if (count($parameters) === 1 && self::isImplements($parameters[0]->getType(), self::class)) {
            return $callback;
        }
        $arguments = array_map(fn($parameter) => $this->argument($parameter, $constants), $parameters);
        return fn(Container $container) => $callback(... array_map(fn($arg) => $arg[0] ? $container->get($arg[1]): $arg[1], $arguments));
    }

    private function argument(ReflectionParameter $parameter, array $constants): ?array
    {
        $name = $parameter->getName();
        if (array_key_exists($name, $constants)) {
            return [false, $constants[$name]];
        } elseif (($type = $parameter->getType()) && !$type->isBuiltin() && ($this->has($typename = $type->getName()) || class_exists($typename))) {
            return [true, $typename];
        } elseif ($parameter->isDefaultValueAvailable()) {
            return [false, $parameter->getDefaultValue()];
        }
        throw new ContainerException(sprintf("Failed to process a parameter: '%s' of type '%s'", $name, $type));
    }
    
    private static function isImplements(string $type, string $class): bool
    {
        return $type == $class 
                || in_array($type, class_parents($class))
                || in_array($type, class_implements($class));
    }
}
