<?php
/**
 * AbstractServiceProvider class file.
 *
 * @package DokanKits\Core\DI
 * @since   3.0.0
 */

namespace DokanKits\Core\DI;

use DokanKits\Dependencies\League\Container\Definition\DefinitionInterface;
use DokanKits\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider;

/**
 * Base class for the service providers used to register classes in the container.
 *
 * See the documentation of the original class this one is based on (https://container.thephpleague.com/4.x/service-providers)
 * for basic usage details. What this class adds is:
 * Note that `AbstractInterfaceServiceProvider` likely serves as a better base class for service providers
 * tasked with registering classes that implement interfaces.
 *
 * @since 3.13.0
 */
abstract class BaseServiceProvider extends AbstractServiceProvider {
	/**
	 * Services that this provider provides
	 *
	 * @var array
	 */
	protected array $services = array();

	/**
	 * Tags associated with this provider
	 *
	 * @var array
	 */
	protected array $tags = array();

	/**
	 * {@inheritDoc}
	 *
	 * Check if the service provider can provide the given service alias.
	 *
	 * @param string $alias The service alias to check.
	 *
	 * @return bool True if the service provider can provide the service, false otherwise.
	 */
	public function provides( string $alias ): bool {
		static $implements = array();

		// Direct match in services or tags.
		if ( in_array( $alias, $this->services, true ) || in_array( $alias, $this->tags, true ) ) {
			return true;
		}

		// Look at implemented interfaces.
		foreach ( $this->services as $class ) {
			if ( ! class_exists( $class ) ) {
				continue;
			}

			$implements_more = class_implements( $class );
			if ( $implements_more ) {
				$implements = array_merge( $implements, $implements_more );
			}
		}

		$implements = array_unique( $implements );

		return array_key_exists( $alias, $implements );
	}

	/**
	 * Register a class in the container and add tags for all the interfaces it implements.
	 *
	 * This also updates the `$this->provides` property with the interfaces provided by the class, and ensures
	 * that the property doesn't contain duplicates.
	 *
	 * @param string $id       Entry ID (typically a class or interface name).
	 * @param mixed  $concrete Concrete entity to register under that ID, null for automatic creation.
	 * @param bool   $shared   Whether to register the class as shared (`get` always returns the same instance) or not.
	 *
	 * @return DefinitionInterface
	 */
	protected function add_with_implements_tags( string $id, $concrete = '', bool $shared = false ): DefinitionInterface {
		$definition = $this->getContainer()->add( $id, $concrete )->setShared( $shared );

		if ( is_string( $concrete ) && class_exists( $concrete ) ) {
			foreach ( class_implements( $concrete ) as $interface ) {
				$definition->addTag( $interface );

				if ( ! in_array( $interface, $this->services, true ) ) {
					$this->services[] = $interface;
				}
			}
		}

		return $definition;
	}

	/**
	 * Register a shared class in the container and add tags for all the interfaces it implements.
	 *
	 * @param string     $id       Entry ID (typically a class or interface name).
	 * @param mixed|null $concrete Concrete entity to register under that ID, null for automatic creation.
	 *
	 * @return DefinitionInterface
	 */
	protected function share_with_implements_tags( string $id, $concrete = '' ): DefinitionInterface {
		// If $concrete is an empty string, use $id as the concrete class.
		$concrete = '' === $concrete ? $id : $concrete;

		return $this->add_with_implements_tags( $id, $concrete, true );
	}

	/**
	 * Adds tags to the given definition.
	 *
	 * @param DefinitionInterface $definition The definition to which tags will be added.
	 * @param array               $tags       An array of tags to add to the definition.
	 *
	 * @return void
	 */
	protected function add_tags( DefinitionInterface $definition, array $tags ): void {
		foreach ( $tags as $tag ) {
			$definition = $definition->addTag( $tag );
		}
	}
}
