<?php
/**
 * AG WP Plugin Install Manager — コアクラス。
 *
 * @package AG_WP_Plugin_Install_Manager
 */

// WordPress 外からの直接アクセスを禁止。
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'AG_WP_Plugin_Install_Manager' ) ) {

	/**
	 * プラグインの登録・状態管理を担うコアクラス。
	 *
	 * 使い方:
	 *   1. ag_wpim_register フックに関数を登録する。
	 *   2. AG_WP_Plugin_Install_Manager::register() でプラグインを登録する。
	 *
	 * @since 1.0.0
	 */
	class AG_WP_Plugin_Install_Manager {

		/**
		 * 名前付きインスタンスのレジストリ。
		 * キーは menu_slug。
		 *
		 * @since 1.0.0
		 * @var array<string, AG_WP_Plugin_Install_Manager>
		 */
		protected static $instances = array();

		/**
		 * 登録されたプラグインの配列。
		 * キーはプラグインスラッグ。
		 *
		 * @since 1.0.0
		 * @var array<string, array>
		 */
		protected $plugins = array();

		/**
		 * ライブラリの設定。
		 *
		 * @since 1.0.0
		 * @var array
		 */
		protected $config = array();

		/**
		 * デフォルト設定。
		 *
		 * @since 1.0.0
		 * @var array
		 */
		protected $default_config = array(
			'default_path' => '',        // バンドルzipのデフォルトパス。
			'menu_slug'    => 'ag-wpim', // 管理メニューのスラッグ。
			'parent_slug'  => null,      // null = 使用元に応じて自動判定。
			'capability'   => null,      // null = 使用元に応じて自動判定。
			'has_notices'  => true,      // 管理通知を表示するか。
			'dismissable'  => true,      // 通知を非表示にできるか。
			'context'      => null,      // 'theme' | 'plugin' | null（自動判定）。
			'version'      => '',        // 使用元のバージョン。指定するとバージョン変更時に非表示設定がリセットされ通知が再表示される。
		);

		/**
		 * プラグイン1件あたりのデフォルト設定。
		 *
		 * @since 1.0.0
		 * @var array
		 */
		protected $default_plugin = array(
			'name'               => '',    // プラグイン表示名（必須）。
			'slug'               => '',    // プラグインスラッグ（必須）。
			'required'           => false, // true = 必須 / false = 推奨。
			'version'            => '',    // 最低バージョン。空の場合はバージョン不問。
			'force_activation'   => false, // インストール済みなら自動有効化するか。
			'force_deactivation' => false, // テーマ切り替え時に自動無効化するか。
		);

		/**
		 * コンストラクタ。直接呼び出し禁止。インスタンスは init() 経由で生成する。
		 *
		 * @since 1.0.0
		 *
		 * @param string $menu_slug このインスタンスを識別する管理メニュースラッグ。
		 */
		protected function __construct( $menu_slug ) {
			$this->config = array( 'menu_slug' => $menu_slug );
		}

		/**
		 * menu_slug に対応するインスタンスを返す。
		 *
		 * @since 1.0.0
		 *
		 * @param string $menu_slug 取得するインスタンスの管理メニュースラッグ。
		 * @return AG_WP_Plugin_Install_Manager
		 */
		public static function get_instance( $menu_slug = 'ag-wpim' ) {
			if ( ! isset( self::$instances[ $menu_slug ] ) ) {
				self::$instances[ $menu_slug ] = new self( $menu_slug );
			}
			return self::$instances[ $menu_slug ];
		}

		/**
		 * ライブラリを初期化してWordPressフックに登録する。
		 *
		 * テーマの functions.php や プラグインのメインファイルから呼び出す。
		 * 同一サイトに複数のテーマ・プラグインが導入する場合は、
		 * それぞれ異なる $menu_slug を指定して独立したインスタンスを生成する。
		 *
		 * @since 1.0.0
		 *
		 * @param string $menu_slug このインスタンスの管理メニュースラッグ（必須・一意にすること）。
		 * @param string $context   使用元を明示する場合に指定。'theme' | 'plugin'。
		 *                          省略時はWordPressの状態から自動判定する。
		 * @return void
		 */
		public static function init( $menu_slug, $context = null ) {
			$instance = self::get_instance( $menu_slug );

			if ( ! is_null( $context ) ) {
				$instance->config['context'] = $context;
			}

			add_action( 'after_setup_theme', array( $instance, 'load' ) );
		}

		/**
		 * プラグイン登録フックを発火し、管理クラスを起動する。
		 *
		 * @since 1.0.0
		 * @return void
		 */
		public function load() {
			$config = $this->get_config();

			// インスタンスごとに固有のフックを発火する。
			// 利用側は ag_wpim_register_{menu_slug} に登録して register() を呼ぶ。
			do_action( 'ag_wpim_register_' . $config['menu_slug'], $this );

			// 登録完了後に管理クラスを起動する。
			if ( is_admin() ) {
				$admin = new AG_WP_Plugin_Admin( $this );
				$admin->hooks();
			}
		}

		/**
		 * プラグインを1件登録する。
		 *
		 * @since 1.0.0
		 *
		 * @param array $plugin {
		 *     プラグイン設定。
		 *
		 *     @type string $name               プラグイン表示名（必須）。
		 *     @type string $slug               プラグインスラッグ（必須）。
		 *     @type bool   $required           true = 必須 / false = 推奨。デフォルト false。
		 *     @type string $version            最低バージョン。省略時はバージョン不問。
		 *     @type bool   $force_activation   インストール済みなら自動有効化するか。デフォルト false。
		 *     @type bool   $force_deactivation テーマ切り替え時に自動無効化するか。デフォルト false。
		 * }
		 * @return void
		 */
		public function register( array $plugin ) {
			$plugin = wp_parse_args( $plugin, $this->default_plugin );

			// name と slug は必須。
			if ( empty( $plugin['name'] ) || empty( $plugin['slug'] ) ) {
				return;
			}

			$slug = sanitize_key( $plugin['slug'] );
			$this->plugins[ $slug ] = $plugin;
		}

		/**
		 * 複数のプラグインをまとめて登録する。
		 *
		 * @since 1.0.0
		 *
		 * @param array[] $plugins プラグイン設定の配列。
		 * @param array   $config  ライブラリ設定（省略可）。
		 * @return void
		 */
		public function register_plugins( array $plugins, array $config = array() ) {
			// init() で設定した menu_slug は上書き不可。
			$menu_slug    = $this->get_config()['menu_slug'];
			$this->config = wp_parse_args( $config, $this->default_config );
			$this->config['menu_slug'] = $menu_slug;

			foreach ( $plugins as $plugin ) {
				$this->register( $plugin );
			}
		}

		// =========================================================
		// 状態確認メソッド
		// =========================================================

		/**
		 * 指定プラグインがインストール済みかどうかを返す。
		 *
		 * @since 1.0.0
		 *
		 * @param string $slug プラグインスラッグ。
		 * @return bool
		 */
		public function is_plugin_installed( $slug ) {
			$installed = $this->get_installed_plugins();
			foreach ( $installed as $plugin_file => $plugin_data ) {
				if ( 0 === strpos( $plugin_file, $slug . '/' ) ) {
					return true;
				}
			}
			return false;
		}

		/**
		 * 指定プラグインが有効化されているかどうかを返す。
		 *
		 * @since 1.0.0
		 *
		 * @param string $slug プラグインスラッグ。
		 * @return bool
		 */
		public function is_plugin_active( $slug ) {
			foreach ( $this->get_installed_plugins() as $plugin_file => $plugin_data ) {
				if ( 0 === strpos( $plugin_file, $slug . '/' ) ) {
					return is_plugin_active( $plugin_file );
				}
			}
			return false;
		}

		/**
		 * 登録された全プラグインを返す。
		 *
		 * @since 1.0.0
		 *
		 * @return array<string, array>
		 */
		public function get_plugins() {
			return $this->plugins;
		}

		/**
		 * ライブラリ設定を返す。
		 *
		 * parent_slug・capability が未指定の場合、
		 * context（テーマ/プラグイン）に応じて自動的に適切な値を補完する。
		 *
		 * @since 1.0.0
		 *
		 * @return array
		 */
		public function get_config() {
			$config = wp_parse_args( $this->config, $this->default_config );

			// context が未指定の場合は自動判定する。
			if ( is_null( $config['context'] ) ) {
				$config['context'] = $this->detect_context();
			}

			// parent_slug が未指定の場合はコンテキストに応じて補完する。
			if ( is_null( $config['parent_slug'] ) ) {
				$config['parent_slug'] = 'plugin' === $config['context']
					? 'plugins.php'
					: 'themes.php';
			}

			// capability が未指定の場合はコンテキストに応じて補完する。
			if ( is_null( $config['capability'] ) ) {
				$config['capability'] = 'plugin' === $config['context']
					? 'activate_plugins'
					: 'edit_theme_options';
			}

			return $config;
		}

		/**
		 * 使用元（テーマ/プラグイン）を自動判定する。
		 *
		 * WordPressのアクティブなテーマのディレクトリパスと
		 * 呼び出し元のファイルパスを比較して判定する。
		 *
		 * @since 1.0.0
		 *
		 * @return string 'theme' | 'plugin'
		 */
		protected function detect_context() {
			// バックトレースから呼び出し元のファイルパスを取得する。
			$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
			foreach ( $backtrace as $frame ) {
				if ( empty( $frame['file'] ) ) {
					continue;
				}
				// テーマディレクトリ内のファイルから呼ばれていればテーマと判定。
				if ( 0 === strpos( $frame['file'], get_theme_root() ) ) {
					return 'theme';
				}
				// プラグインディレクトリ内のファイルから呼ばれていればプラグインと判定。
				if ( 0 === strpos( $frame['file'], WP_PLUGIN_DIR ) ) {
					return 'plugin';
				}
			}

			// 判定できない場合はテーマとして扱う。
			return 'theme';
		}

		/**
		 * 指定プラグインの状態を返す。
		 *
		 * @since 1.0.0
		 *
		 * @param string $slug プラグインスラッグ。
		 * @return string 'active' | 'inactive' | 'missing'
		 */
		public function get_plugin_status( $slug ) {
			if ( ! $this->is_plugin_installed( $slug ) ) {
				return 'missing';
			}
			if ( ! $this->is_plugin_active( $slug ) ) {
				return 'inactive';
			}
			return 'active';
		}

		/**
		 * 指定した状態のプラグイン一覧を返す。
		 *
		 * @since 1.0.0
		 *
		 * @param string $status 'active' | 'inactive' | 'missing'
		 * @return array<string, array>
		 */
		public function get_plugins_by_status( $status ) {
			$result = array();
			foreach ( $this->plugins as $slug => $plugin ) {
				if ( $this->get_plugin_status( $slug ) === $status ) {
					$result[ $slug ] = $plugin;
				}
			}
			return $result;
		}

		/**
		 * WordPressにインストール済みのプラグイン一覧を返す。
		 * get_plugins() のラッパー（キャッシュなし）。
		 *
		 * @since 1.0.0
		 *
		 * @return array
		 */
		protected function get_installed_plugins() {
			if ( ! function_exists( 'get_plugins' ) ) {
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
			}
			return get_plugins();
		}
	}
}
