<?php
/**
 * PHP DI Container.
 *
 * @package Super_Advent_Calendar
 * @author  Verdant Studio
 * @since   1.20.0
 */

namespace SuperAdventCalendar;

/**
 * Exit when accessed directly.
 */
if ( ! defined( 'ABSPATH' )) {
	exit;
}

use InvalidArgumentException;
use ReflectionType;
use ReflectionNamedType;
use ReflectionUnionType;
use ReflectionClass;
use RuntimeException;

use SuperAdventCalendar\Providers\AppServiceProvider;

/**
 * PHP DI Container.
 */
class Container
{
	/** @var array<string, callable> */
	protected array $bindings = array();

	/**
	 * Set an abstract with the given factory.
	 *
	 * @param string         $abstract
	 * @param callable|mixed $factory
	 * return $this
	 */
	public function set($abstract, $factory )
	{
		if ( ! is_callable( $factory ) ) {
			$factory = function () use ($factory ) {
				return $factory;
			};
		}

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

		return $this;
	}

	/**
	 * Get the concretion of the given abstract.
	 *
	 * @param  string $abstract
	 * @param  mixed  ...$args
	 * @return mixed
	 */
	public function get($abstract, ...$args )
	{
		if ($this->has( $abstract )) {
			return call_user_func( $this->bindings[ $abstract ], $this, ...$args );
		}

		return $this->autoBuildAbstract( $abstract, $args );
	}

	/**
	 * Check if a binding exists.
	 *
	 * @param string $abstract
	 * @return bool
	 */
	public function has($abstract )
	{
		return isset( $this->bindings[ $abstract ] );
	}

	/**
	 * Automatically builds a class using reflection.
	 *
	 * @param string $abstract
	 * @param array  $dependencies
	 * @return object
	 */
	protected function autoBuildAbstract($abstract, array $dependencies = array() )
	{
		if (class_exists( $abstract ) === false) {
			throw new RuntimeException( 'Unable to autobuild ' . esc_html( $abstract ) . ': class does not exist' );
		}

		$reflection = new ReflectionClass( $abstract );

		if (empty( $dependencies )) {
			$dependencies = $this->buildDependencies( $reflection );
		}

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

	/**
	 * Build dependencies for the given reflected class.
	 *
	 * @param  ReflectionClass $reflection
	 * @return array<int, mixed>
	 */
	protected function buildDependencies(ReflectionClass $reflection )
	{
		$constructor = $reflection->getConstructor();

		if ( ! $constructor ) {
			return array();
		}

		$parameters = $constructor->getParameters();
		$container  = $this;

		return array_map(
			function ($parameter ) use ($container ) {
				$type = $parameter->getType();

				if ( ! $type ) {
					throw new RuntimeException( 'Unable to infer parameter type' );
				}

				if (method_exists( $type, 'isBuiltin' ) && $type->isBuiltin() && $parameter->isDefaultValueAvailable()) {
					return $parameter->getDefaultValue();
				}

				if (method_exists( $type, 'isBuiltin' ) && $type->isBuiltin() && $type->allowsNull()) {
					return null;
				}

				$dependencyName = $container->getReflectionTypeName( $type );
				return $container->get( $dependencyName );
			},
			$parameters
		);
	}

	/**
	 * Gets the name of the type from a ReflectionType.
	 *
	 * @param ReflectionType $type
	 * @return string
	 */
	protected function getReflectionTypeName(ReflectionType $type )
	{
		if ($type instanceof ReflectionNamedType) {
			return $type->getName();
		}

		// PHP 7 does not have ReflectionUnionType, so skip this safely.
		if (class_exists( 'ReflectionUnionType' ) && $type instanceof ReflectionUnionType) {
			$types = $type->getTypes();
			return $this->getReflectionTypeName( reset( $types ) );
		}

		throw new InvalidArgumentException(
			'Unknown reflection type ' . esc_html( get_class( $type ) ) . '.'
		);
	}
}
