<?php
/**
 * Plugin container and boot sequence.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse;

use WPXero\MenuVerse\Admin\Admin_Page;
use WPXero\MenuVerse\Admin\Editor;
use WPXero\MenuVerse\Admin\Nav_Menu_Fields;
use WPXero\MenuVerse\Compat\Integration_Registry;
use WPXero\MenuVerse\Frontend\Assets;
use WPXero\MenuVerse\Frontend\Icon_Library;
use WPXero\MenuVerse\Menus\Menu_Source_Registry;
use WPXero\MenuVerse\Modules\Module_Registry;
use WPXero\MenuVerse\Rest\Content_Controller;
use WPXero\MenuVerse\Rest\Counts_Controller;
use WPXero\MenuVerse\Rest\Icons_Controller;
use WPXero\MenuVerse\Rest\Menus_Controller;
use WPXero\MenuVerse\Rest\Modules_Controller;
use WPXero\MenuVerse\Rest\Preview_Controller;
use WPXero\MenuVerse\Rest\Rest_Controller;
use WPXero\MenuVerse\Rest\Settings_Controller;
use WPXero\MenuVerse\Rest\Tools_Controller;
use WPXero\MenuVerse\Settings\Formats;
use WPXero\MenuVerse\Settings\Settings;

defined( 'ABSPATH' ) || exit;

/**
 * Wires the plugin together and boots only what the current request needs.
 */
final class Plugin {

	/**
	 * Singleton instance.
	 *
	 * @var Plugin|null
	 */
	private static $instance = null;

	/**
	 * Settings store.
	 *
	 * @var Settings
	 */
	private $settings;

	/**
	 * Module registry.
	 *
	 * @var Module_Registry
	 */
	private $modules;

	/**
	 * Menu source registry.
	 *
	 * @var Menu_Source_Registry
	 */
	private $menu_sources;

	/**
	 * Frontend asset orchestration.
	 *
	 * @var Assets
	 */
	private $assets;

	/**
	 * Third party integration registry.
	 *
	 * @var Integration_Registry
	 */
	private $integrations;

	/**
	 * Returns the singleton instance, booting it on first call.
	 *
	 * @return Plugin
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
			self::$instance->boot();
		}

		return self::$instance;
	}

	/**
	 * Builds the container.
	 */
	private function __construct() {
		$this->settings     = new Settings();
		$this->modules      = new Module_Registry( $this->settings );
		$this->menu_sources = new Menu_Source_Registry();
		$this->assets       = new Assets( $this->settings );
		$this->integrations = new Integration_Registry();

		$this->settings->set_registry( $this->modules );
	}

	/**
	 * Runs the boot sequence.
	 *
	 * @return void
	 */
	private function boot() {
		$this->maybe_upgrade();

		$this->menu_sources->boot();
		( new Icon_Library() )->boot();
		( new Formats() )->boot();
		// The tier map first: it stamps the schemas, and the gate below reads
		// what it stamped.
		( new Pro_Tiers() )->boot();
		( new Pro() )->boot();
		$this->integrations->boot();
		$this->register_core_modules();

		/**
		 * Fires so modules can register themselves.
		 *
		 * Registration takes a class name, not an instance, so a module that
		 * is switched off is never constructed.
		 *
		 * @since 1.0.0
		 *
		 * @param Module_Registry $registry Module registry.
		 */
		do_action( 'menuverse_register_modules', $this->modules );

		$this->modules->boot_enabled();
		$this->assets->boot();

		( new Editor() )->boot();

		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );

		if ( is_admin() ) {
			( new Admin_Page( $this->settings, $this->modules ) )->boot();
			( new Nav_Menu_Fields() )->boot();
		}

		/**
		 * Fires once MenuVerse has finished booting.
		 *
		 * @since 1.0.0
		 *
		 * @param Plugin $plugin Plugin container.
		 */
		do_action( 'menuverse_loaded', $this );
	}

	/**
	 * Registers the modules that ship with the plugin.
	 *
	 * Core modules go through the same registry as third party ones; the only
	 * difference is that they do not need to wait for the action to fire.
	 *
	 * Registration order is the order the dashboard lists them in, so the ones
	 * that have shipped come first and the one still being finished follows
	 * them marked as such.
	 *
	 * Block Menu Builder and Vertical Menu are held back: an unregistered
	 * module is absent everywhere at once — dashboard, block, shortcode and
	 * template tag — rather than listed with a switch that leads to a screen
	 * we are not ready to show. Whatever either of them has already saved
	 * stays in `menuverse_settings` untouched, so restoring one is putting its
	 * line back and nothing else.
	 *
	 * @return void
	 */
	private function register_core_modules() {
		$this->modules->register( \WPXero\MenuVerse\Module\Bottom_Menu\Module::class );
		$this->modules->register( \WPXero\MenuVerse\Module\Mobile_Menu\Module::class );
		$this->modules->register( \WPXero\MenuVerse\Module\Mega_Menu\Module::class );
	}

	/**
	 * Registers every REST controller.
	 *
	 * @return void
	 */
	public function register_rest_routes() {
		$controllers = array(
			'modules'  => new Modules_Controller(),
			'settings' => new Settings_Controller(),
			'menus'    => new Menus_Controller(),
			'icons'    => new Icons_Controller(),
			'tools'    => new Tools_Controller(),
			'preview'  => new Preview_Controller(),
			'content'  => new Content_Controller(),
			'counts'   => new Counts_Controller(),
		);

		/**
		 * Filters the REST controllers MenuVerse registers.
		 *
		 * @since 1.0.0
		 *
		 * @param Rest_Controller[] $controllers Controllers keyed by name.
		 * @param string            $namespace   REST namespace.
		 */
		$controllers = apply_filters( 'menuverse_rest_controllers', $controllers, Rest_Controller::REST_NAMESPACE );

		foreach ( $controllers as $controller ) {
			if ( $controller instanceof Rest_Controller ) {
				$controller->register_routes();
			}
		}

		/**
		 * Fires after MenuVerse registered its REST routes.
		 *
		 * @since 1.0.0
		 *
		 * @param string            $namespace   REST namespace.
		 * @param Rest_Controller[] $controllers Registered controllers.
		 */
		do_action( 'menuverse_register_rest_routes', Rest_Controller::REST_NAMESPACE, $controllers );
	}

	/**
	 * Returns the settings store.
	 *
	 * @return Settings
	 */
	public function settings() {
		return $this->settings;
	}

	/**
	 * Returns the module registry.
	 *
	 * @return Module_Registry
	 */
	public function modules() {
		return $this->modules;
	}

	/**
	 * Returns the menu source registry.
	 *
	 * @return Menu_Source_Registry
	 */
	public function menu_sources() {
		return $this->menu_sources;
	}

	/**
	 * Returns the frontend asset orchestrator.
	 *
	 * @return Assets
	 */
	public function assets() {
		return $this->assets;
	}

	/**
	 * Returns the third party integration registry.
	 *
	 * @return Integration_Registry
	 */
	public function integrations() {
		return $this->integrations;
	}

	/**
	 * Runs upgrade routines when the stored version is behind the code.
	 *
	 * @return void
	 */
	private function maybe_upgrade() {
		$stored = $this->settings->get_state( 'version', '' );

		if ( MENUVERSE_VERSION === $stored ) {
			return;
		}

		/**
		 * Fires when the plugin version changed since the last request.
		 *
		 * @since 1.0.0
		 *
		 * @param string $from Previously stored version, empty on first run.
		 * @param string $to   Version now running.
		 */
		do_action( 'menuverse_upgrade', $stored, MENUVERSE_VERSION );

		$this->settings->set_state( 'version', MENUVERSE_VERSION );
	}
}
