<?php
/**
 * Container
 *
 * @package   WP Grid Builder
 * @author    Loïc Blascos
 * @copyright 2019-2024 Loïc Blascos
 */

namespace WP_Grid_Builder\Includes;

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

/**
 * Dependency Injection Container
 *
 * @class WP_Grid_Builder\Includes\Container
 * @since 1.0.0
 */
final class Container {

	/**
	 * Holds container instances.
	 *
	 * @since 1.0.0
	 * @access private
	 *
	 * @var array
	 */
	private static $instances = [];

	/**
	 * Optional namespace for abstract class names.
	 *
	 * @since 1.0.0
	 * @access private
	 *
	 * @var array
	 */
	private $namespace = '';

	/**
	 * Holds registered properties
	 *
	 * @since 1.0.0
	 * @access private
	 *
	 * @var array
	 */
	private $properties = [];

	/**
	 * Holds registered classes
	 *
	 * @since 1.0.0
	 * @access private
	 *
	 * @var array
	 */
	private $objects = [];

	/**
	 * Holds resolved class
	 *
	 * @since 1.0.0
	 * @access private
	 *
	 * @var array
	 */
	private $resolved = [];

	/**
	 * Retrieve a specific instance of container
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param string $slug Slug of the container.
	 * @param string $namespace Optional Namespace.
	 * @return object Instance
	 */
	public static function instance( $slug, $namespace = '' ) {

		if ( ! is_string( $slug ) ) {
			return;
		}

		if ( ! isset( self::$instances[ $slug ] ) ) {

			$class = self::class;
			$class = new $class( $slug, $namespace );

			if ( ! isset( self::$instances[ $slug ] ) || self::$instances[ $slug ] !== $class ) {
				self::$instances[ $slug ] = $class;
			}
		}

		return self::$instances[ $slug ];

	}

	/**
	 * Constructor (altenrative to instance)
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param string $slug Slug of the container.
	 * @param string $namespace Optional Namespace.
	 */
	public function __construct( $slug = null, $namespace = '' ) {

		if ( empty( $slug ) || ! is_string( $slug ) ) {
			$slug = uniqid( 'wpgb_' );
		}

		$this->namespace = $namespace;
		self::$instances[ $slug ] = $this;

	}

	/**
	 * Destroy instance(s)
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param string $slugs Holds container slugs to unset.
	 */
	public function destroy( $slugs = null ) {

		if ( ! empty( $slugs ) ) {

			foreach ( (array) $slugs as $slug ) {

				if ( is_string( $slug ) && isset( self::$instances[ $slug ] ) ) {
					unset( self::$instances[ $slug ] );
				}
			}
		} else {
			self::$instances = [];
		}
	}

	/**
	 * Define a service
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param string $abstract Abstract class name.
	 * @param mixed  $concrete Concrete Class.
	 */
	public function set( $abstract, $concrete = null ) {

		if ( ! is_string( $abstract ) ) {
			return;
		}

		if ( null === $concrete ) {
			$concrete = $this->namespace . $abstract;
		}

		$this->objects[ $abstract ] = $concrete;

		return $this;

	}

	/**
	 * Define a property
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param mixed $key   Property key.
	 * @param mixed $value Property value.
	 */
	public function add( $key, $value = null ) {

		if ( ! is_string( $key ) ) {
			return;
		}

		$this->properties[ $key ] = $value;

		return $this;

	}

	/**
	 * Get property
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param mixed $key Property key.
	 */
	public function prop( $key ) {

		if ( ! is_string( $key ) || ! isset( $this->properties[ $key ] ) ) {
			return;
		}

		return $this->properties[ $key ];

	}

	/**
	 * Check if container exist
	 *
	 * @since 1.1.5
	 * @access public
	 *
	 * @param string $slug Slug of the container.
	 * @return boolean
	 */
	public static function has( $slug ) {

		return ! empty( self::$instances[ $slug ] );

	}

	/**
	 * Get and Instantiate class
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param mixed $abstract   Abstract class name.
	 * @param array $parameters Class parameters.
	 * @return mixed
	 * @throws \Exception Resolve errors.
	 */
	public function get( $abstract, $parameters = [] ) {

		if ( ! is_string( $abstract ) ) {
			return;
		}

		// If not registered.
		if ( ! isset( $this->objects[ $abstract ] ) ) {
			$this->set( $abstract );
		}

		// Get concrete class to resolve.
		$concrete = $this->objects[ $abstract ];

		// If not resolved.
		if ( ! isset( $this->resolved[ $concrete ] ) ) {
			$this->resolve( $concrete, $parameters );
		}

		// Return instantiated class object.
		return $this->resolved[ $concrete ];

	}

	/**
	 * Resolve single
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param mixed $concrete   Concrete class name.
	 * @param array $parameters Class Parameters.
	 * @return object
	 * @throws \Exception Resolve errors.
	 */
	public function resolve( $concrete, $parameters = '' ) {

		$reflector = new \ReflectionClass( $concrete );

		// If class is not instantiable.
		if ( ! $reflector->isInstantiable() ) {

			throw new \Exception(
				esc_html(
					sprintf(
						/* translators: %s: class name */
						__( 'Class %s is not instantiable.', 'wp-grid-builder' ),
						$concrete
					)
				)
			);
		}

		// Get class constructor.
		$constructor = $reflector->getConstructor();

		if ( is_null( $constructor ) ) {
			// Get new instance from class.
			return $reflector->newInstance();
		}

		// Get default constructor parameters.
		if ( empty( $parameters ) ) {

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

		}

		// Get new instance with dependencies resolved.
		$resolved = $reflector->newInstanceArgs( $parameters );
		$this->resolved[ $concrete ] = $resolved;

		return $resolved;

	}

	/**
	 * Get all dependencies resolved
	 *
	 * @since 1.0.0
	 * @access public
	 *
	 * @param array $parameters Class parameters.
	 * @return array
	 * @throws \Exception Dependy error.
	 */
	public function get_dependencies( $parameters ) {

		$dependencies = [];

		foreach ( $parameters as $parameter ) {

			// Get the type hinted class.
			$class = $this->get_class( $parameter );

			if ( null === $class ) {

				$value = $this->prop( $parameter->name );

				if ( $parameter->isDefaultValueAvailable() ) {
					// Get default value of parameter.
					$dependencies[] = $parameter->getDefaultValue();
				} elseif ( '' !== $value ) {
					// Get defined parameter from properties.
					$dependencies[] = $value;
				} else {

					throw new \Exception(
						esc_html(
							sprintf(
								/* translators: %s: class name */
								__( 'Can not resolve class dependency %s.', 'wp-grid-builder' ),
								$parameter->name
							)
						)
					);
				}
			} else {

				if ( $this->namespace ) {
					$concrete = $class->getShortName();
				} else {
					$concrete = $class->getName();
				}

				// Get dependency resolved.
				$dependencies[] = $this->get( $concrete );

			}
		}

		return $dependencies;

	}

	/**
	 * Get the type hinted class.
	 *
	 * @since 1.5.0
	 * @access public
	 *
	 * @param array $parameter Class parameter.
	 * @return null|object
	 */
	public function get_class( $parameter ) {

		if ( ! method_exists( $parameter, 'getType' ) ) {
			return $parameter->getClass();
		}

		$type = $parameter->getType();

		if ( ! $type || $type->isBuiltin() ) {
			return null;
		}

		// Because of ReflectionType::__toString() (PHP 7.0.X).
		if ( ! method_exists( $type, 'getName' ) ) {
			return $parameter->getClass();
		}

		return new \ReflectionClass( $type->getName() );

	}
}
