<?php
namespace Booktics\Container;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

use Booktics\Container\Exception\Dependency_Has_No_Default_Value_Exception;
use Booktics\Container\Exception\Dependency_Is_Not_Instantiable_Exception;
use Exception;
use ReflectionClass;
use ReflectionException;
use ReflectionParameter;

class Container {

    /**
     * Store all services
     *
     * @var array
     */
    protected $services = array();

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return mixed Entry.
     * @throws Container_Exception_Interface|ReflectionException Error while retrieving the entry.
     *
     * @throws Not_Found_Exception_Interface  No entry was found for **this** identifier.
     */
    public function get( string $key ): object {
        if ( ! $this->has( $key ) ) {
            // translators: %s is the name of the class that is not instantiable.
            throw new \Exception( sprintf( esc_html__( 'Service provider %s not found.', 'booktics' ), esc_html( $key ) ) );
        }

        $class_name = $this->services[ $key ];

        return $this->resolve( $class_name );
    }

    /**
     * Sets an entry of the container by its identifier
     *
     * @param string $id
     * @param string $class_name
     *
     * @return void
     */
    public function addServiceProvider( string $key, $class_name ): void {
        $this->services[ $key ] = $class_name;
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($key)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $key Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has( string $key ): bool {
        return array_key_exists( $key, $this->services );
    }


    /**
     * Resolves the service object by its name.
     *
     * @param string $class_name
     *
     * @return object
     * @throws Dependency_Is_Not_Instantiable_Exception
     * @throws ReflectionException|Exception
     *
     * @throws Dependency_Has_No_Default_Value_Exception
     */
    public function resolve( string $class_name ): object {
        if ( ! class_exists( $class_name ) ) {
            // translators: %s is the name of the class that is not instantiable.
            throw new Exception( sprintf( esc_html__( 'Class: %s does not exist.', 'booktics' ), esc_html( $class_name ) ) );
        }

        $reflection_class = new ReflectionClass( $class_name );

        if ( ! $reflection_class->isInstantiable() ) {
            throw new Dependency_Is_Not_Instantiable_Exception(
            // translators: %s is the name of the class that is not instantiable.
                sprintf( esc_html__( 'Class: %s is not instantiable.', 'booktics' ), esc_html( $class_name ) )
            );
        }

        if ( null === $reflection_class->getConstructor() ) {
            return $reflection_class->newInstance();
        }

        $parameters = $reflection_class->getConstructor()->getParameters();

        $dependencies = $this->build_dependencies( $parameters );

        return $reflection_class->newInstanceArgs( $dependencies );
    }


    /**
     * Builds the dependencies for the given parameters.
     *
     * @param ReflectionParameter[] $parameters
     *
     * @return array
     * @throws Dependency_Has_No_Default_Value_Exception
     *
     * @throws ReflectionException
     */
    public function build_dependencies( array $parameters ): array {
        $dependencies = array();

        foreach ( $parameters as $parameter ) {
            $dependency = $parameter->getType() ? $parameter->getType()->getName() : null;

            if ( is_null( $dependency ) ) {
                if ( $parameter->isDefaultValueAvailable() ) {
                    $dependencies[] = $parameter->getDefaultValue();
                } else {
                    throw new Dependency_Has_No_Default_Value_Exception(
                    // translators: %s is the name of the class that is not instantiable.
                        sprintf( esc_html__( 'Class: %s dependency cannot be resolved.', 'booktics' ), esc_html( $parameter->name ) )
                    );
                }
            } else {
                $dependencies[] = $this->get( $dependency );
            }
        }

        return $dependencies;
    }
}
