<?php

namespace SugarCalendarBookings\Support;

use Exception;

/**
 * Abstract Loader.
 *
 * Base class for all loaders in the plugin and addons.
 *
 * @since 1.0.0
 */
abstract class AbstractLoader {

	/**
	 * Container instance.
	 *
	 * @since 1.0.0
	 *
	 * @var Container
	 */
	protected $container;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param Container $container Container instance.
	 */
	public function __construct( Container $container ) {

		$this->container = $container;
	}

	/**
	 * Register classes with the container.
	 *
	 * @since 1.0.0
	 */
	public function register() { }

	/**
	 * Boot the loader.
	 *
	 * @since 1.0.0
	 */
	public function boot() { }

	/**
	 * Get the classes that should be registered with the container and initialized.
	 *
	 * Usually this method should be overridden by the loader class.
	 *
	 * @since 1.0.0
	 *
	 * @return array<class-string<Bootable>, (callable|null)> Keys are the class names, values are the builders.
	 */
	public function provides(): array {

		return [];
	}

	/**
	 * Register classes with the container.
	 *
	 * @since 1.0.0
	 */
	public function register_all() {

		$this->register();

		foreach ( $this->provides() as $class => $builder ) {
			if ( ! class_exists( $class ) || ( $builder !== null && ! is_callable( $builder ) ) ) {
				continue;
			}

			$this->container->singleton( $class, $builder );
		}
	}

	/**
	 * Initialize all registered providers.
	 *
	 * @since 1.0.0
	 * @since 1.2.0 Skip already-booted classes to prevent double-booting when Pro overrides Lite.
	 */
	public function boot_providers() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		foreach ( $this->provides() as $class => $builder ) {

			if ( ! in_array( Bootable::class, class_implements( $class ), true ) ) {
				continue;
			}

			// Skip if already fully booted by another loader.
			if ( $this->container->is_resolved( $class ) ) {
				continue;
			}

			try {
				$object = $this->container->get( $class );

				if ( method_exists( $object, 'allow_load' ) && ! $object->allow_load() ) {
					continue;
				}

				if ( method_exists( $object, 'init' ) ) {
					$object->init();
				}

				if ( method_exists( $object, 'hooks' ) ) {
					$object->hooks();
				}

				// Mark as fully booted after init() and hooks() have been called.
				$this->container->mark_as_booted( $class );
			} catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				// Exception intentionally silenced.
			}
		}
	}

	/**
	 * Get an instance of a class from the container.
	 *
	 * @since 1.0.0
	 *
	 * @template T
	 *
	 * @param class-string<T> $class_name The class to retrieve.
	 *
	 * @throws Exception If the class is not found in the container.
	 *
	 * @return T
	 */
	public function get( $class_name ) {

		return $this->container->get( $class_name );
	}
}
