<?php

declare(strict_types=1);

namespace System\Application;

use ReflectionClass;
use ReflectionNamedType;
use RuntimeException;
use System\Contracts\ContainerInterface;

final class Container implements ContainerInterface
{
    /**
     * @var array<string,mixed>
     */
    private array $bindings = [];

    /**
     * @var array<string,mixed>
     */
    private array $instances = [];

    public function bind(string $abstract, mixed $concrete): void
    {
        $this->bindings[$abstract] = $concrete;
    }

    public function singleton(string $abstract, mixed $concrete): void
    {
        $this->instances[$abstract] = null;

        $this->bindings[$abstract] = $concrete;
    }

    public function has(string $abstract): bool
    {
        return isset($this->bindings[$abstract]);
    }

    public function make(string $abstract): mixed
    {
        /*
        |--------------------------------------------------------------------------
        | Existing Singleton
        |--------------------------------------------------------------------------
        */

        if (array_key_exists($abstract, $this->instances)) {

            if ($this->instances[$abstract] !== null) {
                return $this->instances[$abstract];
            }
        }

        /*
        |--------------------------------------------------------------------------
        | Registered Binding
        |--------------------------------------------------------------------------
        */

        if (isset($this->bindings[$abstract])) {

            $binding = $this->bindings[$abstract];

            if (is_callable($binding)) {

                $object = $binding($this);

            } elseif (is_string($binding)) {

                $object = $this->build($binding);

            } else {

                $object = $binding;
            }

            if (array_key_exists($abstract, $this->instances)) {
                $this->instances[$abstract] = $object;
            }

            return $object;
        }

        /*
        |--------------------------------------------------------------------------
        | Auto Resolve
        |--------------------------------------------------------------------------
        */

        return $this->build($abstract);
    }

    /**
     * Build class using Reflection.
     */
    private function build(string $class): object
    {
        if (!class_exists($class)) {

            throw new RuntimeException(
                "Cannot resolve {$class}"
            );
        }

        $reflection = new ReflectionClass($class);

        if (!$reflection->isInstantiable()) {

            throw new RuntimeException(
                "{$class} is not instantiable."
            );
        }

        $constructor = $reflection->getConstructor();

        if ($constructor === null) {
            return new $class();
        }

        $dependencies = [];

        foreach ($constructor->getParameters() as $parameter) {

            $type = $parameter->getType();

            if (!$type instanceof ReflectionNamedType) {

                throw new RuntimeException(
                    "Unable to resolve constructor parameter."
                );
            }

            if ($type->isBuiltin()) {

                throw new RuntimeException(
                    "Primitive constructor parameter not supported."
                );
            }

            $dependencies[] = $this->make(
                $type->getName()
            );
        }

        return $reflection->newInstanceArgs($dependencies);
    }
}