<?php

namespace Planaday\Utility\DependencyInjection;

use Planaday\Utility\Debug\Log\Logging;
use Planaday\Utility\Debug\TraitChecker;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;

/**
 * @template T
 */
class ComponentEntry {
	use Logging;

	/** @var class-string<T> */
	protected string $class;

	/** @var class-string<T> */
	protected string $instanceClass;

	/**
	 * @var T|null
	 * @noinspection PhpMissingFieldTypeInspection
	 */
	protected $instance;

	/** @var Container */
	protected Container $registry;

	/** @var null|callable():T */
	protected $constructor = null;

	/**
	 * @param Container $registry
	 * @param class-string<T> $class
	 * @param null|class-string<T> $instanceClass
	 * @param null|T $instance
	 */
	public function __construct(Container $registry, string $class, ?string $instanceClass = null, $instance = null) {
		$this->class = $class;
		$this->instanceClass = $instanceClass ?? $class;
		$this->registry = $registry;
		$this->instance = $instance;
	}

	/**
	 * @return class-string<T>
	 */
	public function getComponentClass(): string {
		return $this->class;
	}

	/**
	 * @return class-string<T>
	 */
	public function getInstanceClass(): string {
		return $this->instanceClass;
	}

	/**
	 * @return T
	 *
	 * @throws InjectionImpossibleException
	 */
	public function getInstance() {
		if ( $this->instance === null ) {
			$this->instance = $this->createInstance();
		}

		return $this->instance;
	}

	public function hasInstance(): bool {
		return $this->instance !== null;
	}

	/**
	 * @return T
	 *
	 * @throws InjectionImpossibleException
	 */
	public function createInstance(){
		if ($this->instanceClass !== $this->class) {
			$this->logDebug("Implementation set: Getting instance of $this->instanceClass");
			try {
				/** @var T $instance */
				$instance = $this->registry->getInstance($this->instanceClass);
			} catch (InjectionImpossibleException $e){
				$this->logWarning("Get of $this->instanceClass failed: " . $e->getMessage());
				throw $e;
			}

			if (!is_a($instance, $this->class)) {
				$this->logWarning("Get of $this->instanceClass failed: did not receive a correct instance ");
				throw new InjectionImpossibleException("Get of $this->instanceClass failed: did not receive a correct instance");
			}

			return $instance;
		}

		$this->logDebug("Implementation not set: Creating instance of $this->class");

		if ($this->constructor !== null){
			$this->logDebug("Using cached constructor");
			/** @var T $instance */
			$instance = ($this->constructor)();
			$this->registry->doInjections($instance);
			return $instance;
		}

		try{
			/** @var ReflectionClass<T> $reflectionClass */
			$reflectionClass = new ReflectionClass($this->class);
		} catch ( ReflectionException $e ) {
			$this->logError("Creation of $this->class failed: Class does not exist", $e);
			throw new InjectionImpossibleException("Component with class " . $this->instanceClass . " does not exist");
		}

		if ($reflectionClass->isInterface()){
			$this->logError("Creation of $this->class failed: No implementation was set for interface");
			throw new InjectionImpossibleException("No implementation for interface " . $this->class . " was set");
		}

		if ( !$reflectionClass->isInstantiable() ) {
			$this->logError( "Creation of $this->class failed: Class can't be instantiated" );
			throw new InjectionImpossibleException( "Component with class $this->class can not be instantiated");
		}

		$reflectionConstructor = $reflectionClass->getConstructor();

		$phpVersion = explode('.', phpversion());
		if ($reflectionConstructor === null ){
			$constructor = function () use (&$reflectionClass) {
				$instance = $reflectionClass->newInstance();
				if (TraitChecker::hasTrait(Logging::class, $instance)){
					if (method_exists($instance, "setLogger")) $instance->setLogger($this->logger);
				}
				$this->logInfo("Created new instance of {$reflectionClass->getName()}");
				return $instance;
			};
			if ($phpVersion[0] > 8 || ($phpVersion[0] == 8 && $phpVersion[1] >= 4)) {
				$lazyConstructor = function () use (&$reflectionClass, &$constructor) {
					/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
					return $reflectionClass->newLazyProxy($constructor);
				};
			} else {
				$lazyConstructor = $constructor;
			}
			$this->constructor = $lazyConstructor;

			return $lazyConstructor();
		}

		if (!$reflectionConstructor->isPublic()){
			$this->logError( "Creation of $this->class failed: Constructor is not public" );
			throw new InjectionImpossibleException( "Component with class $this->class has a non-public constructor");
		}

		$parameters = [];

		$this->logDebug("Creating instance with constructor, fetching parameters...");

		foreach ($reflectionConstructor->getParameters() as $index => $parameter) {
			$parameterClass = $parameter->getType();
			$parameterClassName = ($parameterClass instanceof ReflectionNamedType) ? $parameterClass->getName() : null;

			if ($parameterClass === null ||
			    $parameterClassName === null ||
			    !(class_exists($parameterClassName) || interface_exists($parameterClassName))
			) {
				if ($parameter->isOptional()){
					try {
						$parameters[ $index ] = $parameter->getDefaultValue();
					} catch ( ReflectionException $e ) {
						$this->logError("Something went wrong internally", $e);
					}
					continue;
				} elseif ($parameter->allowsNull()) {
					$parameters[ $index ] = null;
					continue;
				}
				$this->logError("Creation of $this->class failed: Parameter " . $parameter->getName() . " is not an object");
				throw new InjectionImpossibleException("Component with class $this->class has a parameter that is not an object");
			}

			try {
				$this->logDebug("Fetching parameter " . $parameter->getName() . " of type " . $parameterClassName);
				$parameters[$index] = $this->registry->getInstance($parameterClassName);
			} catch (InjectionImpossibleException $e){
				if (!$parameter->isOptional() && !$parameter->allowsNull()){
					throw $e;
				}

				if ($parameter->isOptional()){
					try {
						$parameters[ $index ] = $parameter->getDefaultValue();
					} catch ( ReflectionException $e ) {
						$this->logError("Something went wrong internally", $e);
					}
				} else {
					$parameters[ $index ] = null;
				}
			}
		}

		$this->logDebug("Fetched all parameters, calling constructor...");

		$constructor = function () use (&$reflectionClass, &$parameters) {
			$instance = $reflectionClass->newInstanceArgs( $parameters );
			$this->logInfo("Created new instance of {$reflectionClass->getName()}");
			return $instance;
		};

		if ($phpVersion[0] > 8 || ($phpVersion[0] == 8 && $phpVersion[1] >= 4)) {
			$lazyConstructor = function () use (&$reflectionClass, &$constructor) {
				/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
				return $reflectionClass->newLazyProxy($constructor);
			};
		} else {
			$lazyConstructor = $constructor;
		}

		try {
			$newInstance = $lazyConstructor();
		} /** @noinspection PhpRedundantCatchClauseInspection */
		catch ( ReflectionException $e ) {
			$this->logError("Something went wrong internally", $e);
			throw new InjectionImpossibleException("Component with class " . $reflectionClass->getName() ." can not be instantiated");
		}

		if ($newInstance === null ){
			$this->logError("Something went wrong internally");
			throw new InjectionImpossibleException("Component with class " . $reflectionClass->getName() ." can not be instantiated");
		}

		$this->logDebug("Saving constructor");
		$this->constructor = $lazyConstructor;

		return $newInstance;
	}

	/**
	 * @return class-string<T>
	 */
	public function getEntryClass(): string {
		return $this->class;
	}

	/**
	 * @return class-string<T>
	 */
	public function getEntryInstanceClass(): string {
		return $this->instanceClass;
	}
}