<?php

namespace Planaday\Utility\DependencyInjection;

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

class ComponentRegistry {
	use Logging;

	/** @var Logger */
	protected $logger;

	/** @var ComponentEntry[] */
	protected $components = [];

	/** @var class-string[] */
	protected $currentlyFetching = [];

	const MAX_DEPTH = 100;

	public function __construct(?Logger $logger = null) {
		$logger ? $this->logger =& $logger : $this->logger = new Logger();
		$this->setComponent(Logger::class, $this->logger);
		$this->setComponent(self::class, $this);
		$this->logInfo('Created new registry');
	}

	/**
	 * @template T of object
	 * @param class-string<T> $componentClass
	 * @param T $component
	 *
	 * @return void
	 */
	public function setComponent(string $componentClass, object $component): void {
		$this->components[$componentClass] = new ComponentEntry($this, $componentClass, get_class($component), $component);
		$this->checkAndInjectTraits($component);
		$this->logInfo('Manually registered ' . $componentClass);
	}

	/**
	 * @template T of object
	 * @param class-string<T> $componentClass
	 * @param class-string<T> $implementation
	 *
	 * @return void
	 */
	public function setComponentImplementation(string $componentClass, string $implementation): void {
		$this->components[$componentClass] = new ComponentEntry($this, $componentClass, $implementation);
		$this->logInfo("Set $componentClass to $implementation");
	}

	/**
	 * @template T of object
	 * @param class-string<T> $componentClass
	 *
	 * @return ComponentEntry<T>|null
	 */
	public function getComponentEntry(string $componentClass): ?ComponentEntry {
		return $this->components[$componentClass] ?? null;
	}

	/**
	 * @template T of object
	 * @param class-string<T> $componentClass
	 *
	 * @return T
	 * @throws InjectionImpossibleException
	 */
	public function getComponent(string $componentClass) {
		if (is_a($componentClass, ComponentRegistryShortCut::class, true)){
			$this->logDebug("Detected and taking shortcut for $componentClass");
			$component = $componentClass::componentRegistryConstruct($this);

			if (is_a($component, $componentClass)) {
				return $component;
			}
		}

		$currentEntry = $this->components[$componentClass] ?? null;

		if ($currentEntry === null || !is_a($currentEntry->getComponentClass(), $componentClass, true)){
			$this->components[$componentClass] = $currentEntry = new ComponentEntry($this, $componentClass);
		}

		if (in_array($componentClass, $this->currentlyFetching)) {
			$this->logError("Circular dependency detected in $componentClass", ['currentlyConstructing' => $this->currentlyFetching]);
			throw new InjectionImpossibleException("Circular dependency detected in " . $componentClass);
		}

		if ( count($this->currentlyFetching) + 1 > self::MAX_DEPTH){
			$this->logError("Depth exceeded in $componentClass", ['maxDepth' => self::MAX_DEPTH, 'currentlyConstructing' => $this->currentlyFetching]);
			throw new InjectionImpossibleException("dependency injection depth exceeded in " . $componentClass . " starting at " . $this->currentlyFetching[0]);
		}

		$this->currentlyFetching[] = $componentClass;
		try {
			$component = $currentEntry->getInstance();
		} catch (InjectionImpossibleException $e){
			array_pop($this->currentlyFetching);
			throw $e;
		}
		array_pop($this->currentlyFetching);

		$this->checkAndInjectTraits($component);

		return $component;
	}

	public function checkAndInjectTraits(object $instance): void {
		if (TraitChecker::hasTrait(Logging::class, $instance)){
			$this->logDebug("Logging detected in " . get_class($instance) . ", injecting Logger");
			if (method_exists($instance, "setLogger")) $instance->setLogger($this->logger);
		}
	}
}
