<?php

namespace frdl;

use IO4\Container\ContainerCollectionInterface;
use Psr\Container\ContainerInterface;

/**
 * A composite container that acts as a normal container, but delegates method calls to one or more internal containers
 * inspired by https://github.com/AcclimateContainer/acclimate-container
 */
class ContainerCollectionV2 implements ContainerCollectionInterface, ContainerInterface
{
	public const CONTAINER_SPLIT = '.';
	public const THROW_ONERRROR = 0;
	public const NULL_ONERROR = 1;
	public const CALL_ID = 'call';

	protected $containers = [];
	protected $factories;
	protected $container = [];
	protected $onFalseGet;
	protected $resolved = [];
	protected $resolvedIds = [];
	protected $callId;
	protected $unresolved = [];
	protected $finalFallbackContainer = null;


	public function setFinalFallbackContainer(ContainerInterface $container): ContainerCollectionInterface
	{
		$this->finalFallbackContainer=$container;
		  return $this;
	}


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


	public function getKnownEntryNames(): array
	{
		$entries = [];

		$entries = array_merge($entries, array_keys($this->container));

		foreach ($this->containers as $containerId => $container) {
				if(\is_callable([$container, 'getKnownEntryNames'])){
					$entries = array_merge($entries, \call_user_func([$container, 'getKnownEntryNames']));
				}elseif(\is_callable([$container, 'all'])){
					$entries = array_merge($entries, array_keys(\call_user_func([$container, 'all'])));
				}
		}


		if(null !== $this->finalFallbackContainer){
		    if(\is_callable([$this->finalFallbackContainer, 'getKnownEntryNames'])){
					$entries = array_merge($entries, \call_user_func([$this->finalFallbackContainer, 'getKnownEntryNames']));
			}
		}



		return $entries;
	}


	protected function un(string $id, bool $requested = false)
	{
		if(!isset($this->unresolved[$id])){
			$this->unresolved[$id] = [0, 0];
		}
		$this->unresolved[$id][(int)$requested]++;
		return $this;
	}


	protected function r(string $id)
	{
		unset($this->unresolved[$id]);
	}


	public function getUnresolved(): array
	{
		return $this->unresolved;
	}


	public function __construct(array $entries = null, int $onFalseGet = 0, string $callId = null)
	{
		$this->callId = (!is_null($callId)) ? $callId : self::CALL_ID;
		$this->onFalseGet = $onFalseGet;
		$this->resolved = [];
		$this->factories = new \SplObjectStorage();
		$this->container = [];
		$this->containers = [];
		$this->unresolved = [];

		//$this->TreeContainer = \frdl\Context::create($this->container);


		if(is_null($entries)){
		   $entries = [];
		}
		foreach ($entries as $id => $containerOrEntry) {
			$type = 'default';
			if(is_array($containerOrEntry)){
			   list($containerOrEntry, $type) = $containerOrEntry;
			}
			if(!is_null($containerOrEntry) && is_object($containerOrEntry) && $containerOrEntry instanceof ContainerInterface){
				$this->addContainer($containerOrEntry, $id);
			}elseif(true === $type || 'factory' === $type){
				$this->factory($id, $containerOrEntry);
			}else{
				$this->set($id, $containerOrEntry);
			}
		}
	}


	public function getCallId(): string
	{
		return $this->callId;
	}


	public function set(string $id, $entry)
	{
		if(isset($this->container[$id]) && (\is_callable($entry) || (is_object($entry) && $entry instanceof \Closure ))) {
			return $this->extend($id, $entry);
		}elseif(isset($this->container[$id])){
			throw new \Exception(sprintf('The container entry #id: %1$s is defined allready in %2$s!', $id, __METHOD__));
		}

		 $this->container[$id] = $entry;
		return $this;
	}


	public function extend($id, \Closure|\callable $callable): ContainerCollectionInterface
	{
		if (!isset($this->container[$id])) {
		 //   throw new \frdl\NotFoundException($id);
			return $this->factory($id, $callable);
		}
		$me = &$this;
		$factory = $this->container[$id];
		$extended = function (ContainerInterface $container, $previous = null) use ($id, $callable, $factory) {

			if(is_null($previous)){
			  $previous = $factory;
			}

			if($id !== $container->getCallId() && $container->has($container->getCallId())
			    && (\is_callable($factory) || (is_object($factory) && $factory instanceof \Closure ))
			  ){
				$call = $container->get($container->getCallId());
				$previous = $call($factory,[
					'container' => $container,
					'previous' => $previous,
				]);



			}elseif(\is_callable($factory) || (is_object($factory) && $factory instanceof \Closure )){
				$previous = $factory($container, $previous);

			}else{
				$previous = $factory;
			}

			if($id !== $container->getCallId() && $container->has($container->getCallId()) ){
				$call = $container->get($container->getCallId());
				return $call($callable,[
					'container' => $container,
					'previous' => $previous,
				]);
			}else{
				return $callable($container, $previous);
			}
		};

		if ($this->factories->contains($factory)) {
		    $this->factories->detach($factory);
		    $this->factories->attach($extended);
		}

		$this->container[$id] = $extended;

		  return $this;
	}


	public function factory($id, \Closure|\callable $callable): ContainerCollectionInterface
	{
		if(isset($this->container[$id]) && (\is_callable($callable) || (is_object($callable) && $callable instanceof \Closure ))) {
			return $this->extend($id, $callable);
		}elseif(isset($this->container[$id])){
			throw new \Exception(sprintf('The container entry #id: %1$s is defined allready in %2$s!', $id, __METHOD__));
		}

		$this->set($id, $callable);
		$this->factories->attach($callable);
		return $this;
	}


	public function addContainer(
		ContainerInterface &$container,
		string|int &$containerId = null,
	): ContainerCollectionInterface {
		if(null === $containerId){
		  $containerId = count($this->containers);
		}

		$containerId_start = implode(self::CONTAINER_SPLIT, $this->_splitId($containerId));
		$containerId = $containerId_start;
		$i=2;
		while(isset($this->containers[$containerId]) ){
			$containerId = $containerId_start
				. self::CONTAINER_SPLIT
					. 'i'
					. $i++
					. 'c'
					. count($this->containers)
					.'r'
					.mt_rand(2,99999);
		}

		$this->containers[$containerId] = &$container;

		ksort($this->containers);

		return $this;
	}


	public function &getContainer(string|int $containerId = null)
	{
		if($this->hasContainer($containerId)){
			return $this->containers[$containerId];
		}

		switch($this->onFalseGet){
			case self::NULL_ONERROR :
				  return null;
				break;
			case self::THROW_ONERRROR :
			default:
				 throw new \Exception(sprintf('Cannot resolve container #id: %s in %s2', $containerId, __METHOD__));
				break;
		}
	}


	public function hasContainer(string|int $containerId = null): bool
	{
		return isset($this->containers[$containerId]) ? true : false;
	}


	public function get($id)
	{
		$name = $id;

		if(isset($this->resolvedIds[$name])){
			$this->r($name);
			return $this->getContainer( $this->resolvedIds[$name][0] )->get( $this->resolvedIds[$name][1] );
		}

		if(isset($this->resolved[$id])){
			$this->r($name);
		  return $this->resolved[$id];
		}


		if (isset($this->container[$id])
			&& (\is_callable($this->container[$id]) && (is_object($this->container[$id]) && $this->container[$id] instanceof \Closure ) )
			&& $this->factories->contains($this->container[$id])
		   ) {
			if($this->getCallId() !== $id && $this->has($this->getCallId()) ){
				$call = $this->get($this->getCallId());
				return $call($this->container[$id],[
					'container' => $this,
					'previous' => $this->container[$id],
				]);
			}
		//elseif(\is_callable($this->container[$id]) && (is_object($this->container[$id]) && $this->container[$id] instanceof \Closure ) ){
			//	return \call_user_func($this->container[$id], $this, $this->container[$id]);
			//}
		}


		if ($this->getCallId() !== $id && isset($this->container[$id])
			&& !isset($this->resolved[$id])
			&& (\is_callable($this->container[$id]) || $this->container[$id] instanceof \Closure)
		   ) {

			if($this->getCallId() !== $id && $this->has($this->getCallId()) ){
				$call = $this->get($this->getCallId());
				$this->resolved[$id] = $call($this->container[$id],[
					'container' => $this,
					'previous' => $this->container[$id],
				]);
			}elseif($this->getCallId() !== $id
					&& \is_callable($this->container[$id])
					&& !is_object($this->container[$id])){
				$this->resolved[$id] = \call_user_func($this->container[$id], $this, $this->container[$id]);
			}elseif($this->getCallId() !== $id
					&& \is_callable($this->container[$id])
					&& is_object($this->container[$id])
				    && $this->container[$id] instanceof \Closure
				   ){
				$this->resolved[$id] = \call_user_func($this->container[$id], $this, $this->container[$id]);
			}else{
				$this->resolved[$id] = $this->container[$id];
			}
		 //   $this->container[$id] = &$this->resolved[$id];
		}

		if(isset($this->container[$id])){
			$this->r($name);
		  return $this->container[$id];
		}



		$parts = $this->_splitId($name);
		$current = '';
		while(0<count($parts) ){
			$current .= self::CONTAINER_SPLIT.array_shift($parts);
		    $current = ltrim($current, self::CONTAINER_SPLIT);
			 if($this->hasContainer($current)){
				 $id = substr($name, strlen($current), strlen($name) );
				 if ($this->getContainer($current)->has($id)) {
					  $this->resolvedIds[$name] = [$current, $id];
					 $this->r($name);
					  return $this->getContainer($current)->get($id);
				 }
			 }
		}




		foreach ($this->containers as $containerId => $container) {
		    if ($container->has($name)) {
				/*
		        $this->resolved[$name] = &$container->get($name);
				$this->container[$name] = &$this->resolved[$name];
				return $this->container[$name];
				*/
				$this->resolvedIds[$name] = [$containerId, $name];
				$this->r($name);
				return $container->get($name);
		    }
		}

		if(null !== $this->finalFallbackContainer){
		    if ($this->finalFallbackContainer->has($name)) {
				//$this->resolvedIds[$name] = [$containerId, $name];
				$this->r($name);
		        $this->resolved[$name] = $this->finalFallbackContainer->get($name);
				return $this->resolved[$name];
		    }
		}



		$this->un($name, true);

		switch($this->onFalseGet){
			case self::NULL_ONERROR :
				  return null;
				break;
			case self::THROW_ONERRROR :
			default:
				 throw new \Exception(sprintf('Cannot resolve container-entry #id: %s|%s in %s', $id, $name, __METHOD__));
				break;
		}
	}


	protected function _splitId($id)
	{
		return preg_split("/([\.\t\n\@])/i", $id);
	}


	public function has($id)
	{
		$name = $id;


		if(isset($this->resolved[$name])){
			$this->r($name);
			return true;
		}


		if(isset($this->container[$name])){
			$this->r($name);
			return true;
		}

		if(isset($this->resolvedIds[$name])){
			$this->r($name);
			return true;
		}


		$parts = $this->_splitId($name);
		$current = '';
		while(0<count($parts) ){
			$current .= self::CONTAINER_SPLIT.array_shift($parts);
		    $current = ltrim($current, self::CONTAINER_SPLIT);
			$id = substr($name, strlen($current), strlen($name) );
		    $id = rtrim($id, self::CONTAINER_SPLIT);
			 if($this->hasContainer($current)){
				 if ($this->getContainer($current)->has($id)) {
					 $this->resolvedIds[$name] = [$current, $id];
					 $this->r($name);
					 return true;
				 }
			 }
		}

		foreach ($this->containers as $containerId => $container) {
		    if ($container->has($name)) {
				$this->resolvedIds[$name] = [$containerId, $name];
				$this->r($name);
		        return true;
		    }
		}

		if(null !== $this->finalFallbackContainer){
		    if ($this->finalFallbackContainer->has($name)) {
				//$this->resolvedIds[$name] = [$containerId, $name];
				$this->r($name);
		        return true;
		    }
		}

		$this->un($name, false);
		return false;
	}
}
__halt_compiler();----SIGNATURE:----gQWb8lYsjuMuF5PwPrR7Ax3SUJgT01EKfUejVDs6bDNBvcVAR7Lser9R2PmjiGcYEA2W0xdSYB+6Ns9Y6N7uxhzQGO2Nh/xaZddVjB4l3Ky7bqbp1xsA6WcQJTYoB8ukxw++TDp/kT7/wEeO/3H5e7Z3VEHwGa1rOLAV7xSfc443zuQHjrjL9SJEfWf0WYS3e4fNSESQUY3CAwOEnpOWQt08OXnGn50MRTjL3OTJEGzjHAhDkNLhfw9vGR5nE7etOjYFQaRVloZckL+i1cXDYV0M8SQjnyrhe+1pyK5SIbmav647OmGErr2I+ebI0TJnlnVE8M0YRqbzRaGVgmMQBUJlWK6RhWY/cvm/Oh7NpIpX84PdfLpmxZ1Dtlhsozez7OMazADOrPfYLfDvCL2ZKod/nPV/VqF+JYvKGYcWYawACee0nm6dJV9ym+mXjMpgL1jzzQbSDtaj2CaltWCt1KD0Z2gActMSZjH3SHL8x9eE/fvEMzfpHhivY0QUUOp3T5H2asxJGhJL6YzaHsLqKUCtVYfuJ/iXx7VcDstEWmD3eCIYjP8lYLhC83iWmbgMDLVAmZzGWLttVi46k/rNpVNIGex4xEvIjZQkbGxPhdjIGbovIngg/ptO+YW2CkYatvpkm3/4RT+ySNgC4ouN9gekeUGj3+8Ang1yM5CLVQM=----ATTACHMENT:----MTE5NDc3NTM1MTEwNjczOCAxMjkyODMwMDQ0NDk3NjA4IDk3MzMwOTkwNDE0MjEwMDA=