<?php
/**
 * The global WPGraphQL class.
 *
 * @package WPGraphQL
 */

use WPGraphQL\Admin\Admin;
use WPGraphQL\AppContext;
use WPGraphQL\Registry\SchemaRegistry;
use WPGraphQL\Registry\TypeRegistry;
use WPGraphQL\Router;
use WPGraphQL\Utils\InstrumentSchema;
use WPGraphQL\Utils\Preview;

/**
 * Class WPGraphQL
 *
 * This is the one true WPGraphQL class
 */
final class WPGraphQL {

	/**
	 * Stores the instance of the WPGraphQL class
	 *
	 * @var \WPGraphQL The one true WPGraphQL
	 * @since  0.0.1
	 */
	private static self $instance;

	/**
	 * Holds the Schema def
	 *
	 * @var ?\WPGraphQL\WPSchema $schema The Schema used for the GraphQL API
	 */
	protected static $schema;

	/**
	 * Holds the TypeRegistry instance
	 *
	 * @var ?\WPGraphQL\Registry\TypeRegistry $type_registry The registry that holds all GraphQL Types
	 */
	protected static $type_registry;

	/**
	 * Stores an array of allowed post types
	 *
	 * @var ?\WP_Post_Type[] allowed_post_types
	 * @since  0.0.5
	 */
	protected static ?array $allowed_post_types;

	/**
	 * Stores an array of allowed taxonomies
	 *
	 * @var ?\WP_Taxonomy[] allowed_taxonomies
	 * @since  0.0.5
	 */
	protected static ?array $allowed_taxonomies;

	/**
	 * Whether a GraphQL request is currently being processed.
	 */
	protected static bool $is_graphql_request = false;

	/**
	 * Whether an Introspection query is currently being processed.
	 */
	protected static bool $is_introspection_query = false;

	/**
	 * The instance of the WPGraphQL object
	 *
	 * @return \WPGraphQL - The one true WPGraphQL
	 * @since  0.0.1
	 */
	public static function instance(): self {
		if ( ! isset( self::$instance ) ) {
			self::$instance = new self();
			self::$instance->setup_constants();
			self::$instance->includes();
			self::$instance->actions();
			self::$instance->filters();
			self::$instance->upgrade();
			self::$instance->deprecated();
			self::$instance->commands();
		}

		/**
		 * Return the WPGraphQL Instance
		 */
		return self::$instance;
	}

	/**
	 * Throw error on object clone.
	 * The whole idea of the singleton design pattern is that there is a single object
	 * therefore, we don't want the object to be cloned.
	 *
	 * @return void
	 * @since  0.0.1
	 */
	public function __clone() {
		// Cloning instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'The WPGraphQL class should not be cloned.', 'wp-graphql' ), '0.0.1' );
	}

	/**
	 * Disable unserializing of the class.
	 *
	 * @return void
	 * @since  0.0.1
	 */
	public function __wakeup() {
		// De-serializing instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'De-serializing instances of the WPGraphQL class is not allowed', 'wp-graphql' ), '0.0.1' );
	}

	/**
	 * Setup plugin constants.
	 *
	 * @since  0.0.1
	 */
	private function setup_constants(): void {
		graphql_setup_constants();
	}

	/**
	 * Setup Experiments.
	 */
	public function setup_experiments(): void {
		$experimental = new \WPGraphQL\Experimental\Experimental();
		$experimental->init();
	}

	/**
	 * Include required files.
	 * Uses composer's autoload
	 *
	 * @since  0.0.1
	 */
	private function includes(): void {
	}

	/**
	 * Set whether the request is a GraphQL request or not
	 *
	 * @param bool $is_graphql_request
	 */
	public static function set_is_graphql_request( $is_graphql_request = false ): void {
		self::$is_graphql_request = $is_graphql_request;
	}

	/**
	 * Whether the request is a graphql request or not
	 */
	public static function is_graphql_request(): bool {
		return self::$is_graphql_request;
	}

	/**
	 * Set whether the request is an introspection query or not
	 *
	 * @param bool $is_introspection_query
	 *
	 * @since 1.28.0
	 */
	public static function set_is_introspection_query( bool $is_introspection_query = false ): void {
		self::$is_introspection_query = $is_introspection_query;
	}

	/**
	 * Whether the request is an introspection query or not (query for __type or __schema)
	 *
	 * @since 1.28.0
	 */
	public static function is_introspection_query(): bool {
		return (bool) self::$is_introspection_query;
	}

	/**
	 * Sets up actions to run at certain spots throughout WordPress and the WPGraphQL execution
	 * cycle
	 */
	private function actions(): void {
		/**
		 * Init WPGraphQL after themes have been set up,
		 * allowing for both plugins and themes to register
		 * things before graphql_init
		 */
		add_action(
			'after_setup_theme',
			static function () {
				new \WPGraphQL\Data\Config();
				$router = new Router();
				$router->init();
				$instance = self::instance();

				/**
				 * Fire off init action
				 *
				 * @param \WPGraphQL $instance The instance of the WPGraphQL class
				 */
				do_action( 'graphql_init', $instance );
			}
		);

		// Load plugin textdomain for translations
		add_action( 'init', [ self::class, 'load_textdomain' ], 0 );

		// Initialize the plugin url constant
		// see: https://developer.wordpress.org/reference/functions/plugins_url/#more-information
		add_action( 'init', [ $this, 'setup_plugin_url' ] );

		// Prevent WPGraphQL Insights from running
		remove_action( 'init', '\WPGraphQL\Extensions\graphql_insights_init' );

		/**
		 * Flush permalinks if the registered GraphQL endpoint has not yet been registered.
		 */
		add_action( 'wp_loaded', [ $this, 'maybe_flush_permalinks' ] );

		/**
		 * Hook in before fields resolve to check field permissions
		 */
		add_action(
			'graphql_before_resolve_field',
			[
				'\WPGraphQL\Utils\InstrumentSchema',
				'check_field_permissions',
			],
			10,
			8
		);

		// Determine what to show in graphql
		add_action( 'init_graphql_request', 'register_initial_settings', 10 );

		// Throw an exception
		add_action( 'do_graphql_request', [ $this, 'min_php_version_check' ] );
		add_action( 'do_graphql_request', [ $this, 'introspection_check' ], 10, 4 );

		// Initialize Admin functionality
		add_action( 'after_setup_theme', [ $this, 'init_admin' ] );
		add_action( 'after_setup_theme', [ $this, 'setup_experiments' ] );

		add_action(
			'init_graphql_request',
			static function () {
				$tracing = new \WPGraphQL\Utils\Tracing();
				$tracing->init();

				$query_log = new \WPGraphQL\Utils\QueryLog();
				$query_log->init();
			}
		);

		// Initialize Update functionality.
		( new \WPGraphQL\Admin\Updates\Updates() )->init();
	}

	/**
	 * Registers WP-CLI commands.
	 *
	 * @since 2.7.0
	 */
	private function commands(): void {
		if ( ! class_exists( 'WP_CLI' ) || ! defined( 'WP_CLI' ) || ! WP_CLI ) {
			return;
		}

		// Ensure the Commands class is loaded before trying to register it
		// If the class doesn't exist, try to require the file directly
		// (in case the autoloader hasn't loaded it yet)
		if ( ! class_exists( \WPGraphQL\CLI\Commands::class ) ) {
			// phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- WPGRAPHQL_PLUGIN_DIR is a constant, not user input
			if ( file_exists( WPGRAPHQL_PLUGIN_DIR . 'src/CLI/Commands.php' ) ) {
				require_once WPGRAPHQL_PLUGIN_DIR . 'src/CLI/Commands.php';
			}
		}

		// Only register if the class exists after attempting to load it
		if ( class_exists( \WPGraphQL\CLI\Commands::class ) ) {
			\WP_CLI::add_command( 'graphql', \WPGraphQL\CLI\Commands::class );
		}
	}

	/**
	 * @param ?string                         $query     The GraphQL query
	 * @param ?string                         $operation The name of the operation
	 * @param ?array<mixed>                   $variables Variables to be passed to your GraphQL
	 *                                                   request
	 * @param \GraphQL\Server\OperationParams $params    The Operation Params. This includes any
	 *                                                   extra params,
	 *
	 * @throws \GraphQL\Error\SyntaxError
	 * @throws \Exception
	 */
	public function introspection_check( ?string $query, ?string $operation, ?array $variables, \GraphQL\Server\OperationParams $params ): void {

		if ( empty( $query ) ) {
			return;
		}

		$ast              = \GraphQL\Language\Parser::parse( $query );
		$is_introspection = false;

		\GraphQL\Language\Visitor::visit(
			$ast,
			[
				'Field' => static function ( \GraphQL\Language\AST\Node $node ) use ( &$is_introspection ) {
					if ( $node instanceof \GraphQL\Language\AST\FieldNode && ( '__schema' === $node->name->value || '__type' === $node->name->value ) ) {
						$is_introspection = true;
						return \GraphQL\Language\Visitor::stop();
					}
				},
			]
		);

		self::set_is_introspection_query( $is_introspection );
	}

	/**
	 * Check if the minimum PHP version requirement is met before execution begins.
	 *
	 * If the server is running a lower version than required, throw an exception and prevent
	 * further execution.
	 *
	 * @throws \Exception
	 */
	public function min_php_version_check(): void {
		if ( defined( 'GRAPHQL_MIN_PHP_VERSION' ) && version_compare( PHP_VERSION, GRAPHQL_MIN_PHP_VERSION, '<' ) ) {
			throw new \Exception(
				esc_html(
					sprintf(
					// translators: %1$s is the current PHP version, %2$s is the minimum required PHP version.
						__( 'The server\'s current PHP version %1$s is lower than the WPGraphQL minimum required version: %2$s', 'wp-graphql' ),
						PHP_VERSION,
						GRAPHQL_MIN_PHP_VERSION
					)
				)
			);
		}
	}

	/**
	 * Sets up the plugin url
	 */
	public function setup_plugin_url(): void {
		// Plugin Folder URL.
		if ( ! defined( 'WPGRAPHQL_PLUGIN_URL' ) ) {
			define( 'WPGRAPHQL_PLUGIN_URL', plugin_dir_url( dirname( __DIR__ ) . '/wp-graphql.php' ) );
		}
	}

	/**
	 * Load the plugin textdomain for translations.
	 *
	 * @since 2.7.0
	 */
	public static function load_textdomain(): void {
		load_plugin_textdomain(
			'wp-graphql',
			false,
			dirname( plugin_basename( WPGRAPHQL_PLUGIN_FILE ) ) . '/languages/'
		);
	}

	/**
	 * Determine the post_types and taxonomies, etc that should show in GraphQL.
	 */
	public function setup_types(): void {
		/**
		 * Set up the settings, post_types and taxonomies to show_in_graphql
		 */
		self::show_in_graphql();
	}

	/**
	 * Flush permalinks if the GraphQL Endpoint route isn't yet registered.
	 */
	public function maybe_flush_permalinks(): void {
		$rules = get_option( 'rewrite_rules' );
		if ( ! isset( $rules[ graphql_get_endpoint() . '/?$' ] ) ) {
			flush_rewrite_rules(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules
		}
	}

	/**
	 * Setup filters
	 */
	private function filters(): void {
		// Filter the post_types and taxonomies to show in the GraphQL Schema
		$this->setup_types();

		/**
		 * Instrument the Schema to provide Resolve Hooks and sanitize Schema output
		 */
		add_filter(
			'graphql_get_type',
			[
				InstrumentSchema::class,
				'instrument_resolvers',
			],
			10,
			2
		);

		// Filter how metadata is retrieved during GraphQL requests
		add_filter(
			'get_post_metadata',
			[
				Preview::class,
				'filter_post_meta_for_previews',
			],
			10,
			4
		);

		/**
		 * Prevent WPML from redirecting within WPGraphQL requests
		 *
		 * @see https://github.com/wp-graphql/wp-graphql/issues/1626#issue-769089073
		 * @since 1.27.0
		 */
		add_filter(
			'wpml_is_redirected',
			static function ( $is_redirect ) {
				if ( is_graphql_request() ) {
					return false;
				}
				return $is_redirect;
			},
			10,
			1
		);
	}

	/**
	 * Private function to register deprecated functionality.
	 */
	private function deprecated(): void {
		$deprecated = new WPGraphQL\Deprecated();
		$deprecated->register();
	}

	/**
	 * Upgrade routine.
	 */
	public function upgrade(): void {
		$version = get_option( 'wp_graphql_version', null );

		// If the version is not set, this is a fresh install, not an update.
		// set the version and return.
		if ( ! $version ) {
			update_option( 'wp_graphql_version', WPGRAPHQL_VERSION );
			return;
		}

		// If the version is less than the current version, run the update routine
		if ( version_compare( $version, WPGRAPHQL_VERSION, '<' ) ) {
			$this->run_update_routines( $version );
			update_option( 'wp_graphql_version', WPGRAPHQL_VERSION );
		}
	}

	/**
	 * Executes update routines based on the previously stored version.
	 *
	 * This triggers an action that passes the previous version and new version and allows for specific actions or
	 * modifications needed to bring installations up-to-date with the current plugin version.
	 *
	 * Each update routine (callback that hooks into "graphql_do_update_routine") should handle backward compatibility as gracefully as possible.
	 *
	 * @since 1.2.3
	 * @param string|null $stored_version The version number currently stored in the database.
	 *                                    Null if no version has been previously stored.
	 */
	public function run_update_routines( ?string $stored_version = null ): void {

		// bail if the stored version is empty, or the WPGRAPHQL_VERSION constant is not set
		if ( ! defined( 'WPGRAPHQL_VERSION' ) || ! $stored_version ) {
			return;
		}

		// If the stored version is less than the current version, run the upgrade routine
		if ( version_compare( $stored_version, WPGRAPHQL_VERSION, '<' ) ) {

			// Clear the extensions cache
			$this->clear_extensions_cache();

			/**
			 * Fires the update routine.
			 *
			 * @param string $stored_version The version number currently stored in the database.
			 * @param string $new_version    The version number of the current plugin.
			 */
			do_action( 'graphql_do_update_routine', $stored_version, WPGRAPHQL_VERSION );
		}
	}

	/**
	 * Clear all caches in the "wpgraphql_extensions" cache group.
	 */
	public function clear_extensions_cache(): void {
		global $wp_object_cache;

		if ( isset( $wp_object_cache->cache['wpgraphql_extensions'] ) ) {
			foreach ( $wp_object_cache->cache['wpgraphql_extensions'] as $key => $value ) {
				wp_cache_delete( $key, 'wpgraphql_extensions' );
			}
		}
	}

	/**
	 * Initialize admin functionality.
	 */
	public function init_admin(): void {
		$admin = new Admin();
		$admin->init();
	}

	/**
	 * This sets up built-in post_types and taxonomies to show in the GraphQL Schema.
	 *
	 * @since  0.0.2
	 */
	public static function show_in_graphql(): void {
		add_filter( 'register_post_type_args', [ self::class, 'setup_default_post_types' ], 10, 2 );
		add_filter( 'register_taxonomy_args', [ self::class, 'setup_default_taxonomies' ], 10, 2 );

		// Run late so the user can filter the args themselves.
		add_filter( 'register_post_type_args', [ self::class, 'register_graphql_post_type_args' ], 99, 2 );
		add_filter( 'register_taxonomy_args', [ self::class, 'register_graphql_taxonomy_args' ], 99, 2 );
	}

	/**
	 * Sets up the default post types to show_in_graphql.
	 *
	 * @param array<string,mixed> $args      Array of arguments for registering a post type.
	 * @param string              $post_type Post type key.
	 *
	 * @return array<string,mixed>
	 */
	public static function setup_default_post_types( $args, $post_type ) {
		// Adds GraphQL support for attachments.
		if ( 'attachment' === $post_type ) {
			$args['show_in_graphql']     = true;
			$args['graphql_single_name'] = 'mediaItem';
			$args['graphql_plural_name'] = 'mediaItems';
			$args['graphql_description'] = __( 'Represents uploaded media, including images, videos, documents, and audio files.', 'wp-graphql' );
		} elseif ( 'page' === $post_type ) { // Adds GraphQL support for pages.
			$args['show_in_graphql']     = true;
			$args['graphql_single_name'] = 'page';
			$args['graphql_plural_name'] = 'pages';
			$args['graphql_description'] = __( 'A standalone content entry generally used for static, non-chronological content such as "About Us" or "Contact" pages.', 'wp-graphql' );
		} elseif ( 'post' === $post_type ) { // Adds GraphQL support for posts.
			$args['show_in_graphql']     = true;
			$args['graphql_single_name'] = 'post';
			$args['graphql_plural_name'] = 'posts';
			$args['graphql_description'] = __( 'A chronological content entry typically used for blog posts, news articles, or similar date-based content.', 'wp-graphql' );
		}

		return $args;
	}

	/**
	 * Sets up the default taxonomies to show_in_graphql.
	 *
	 * @param array<string,mixed> $args     Array of arguments for registering a taxonomy.
	 * @param string              $taxonomy Taxonomy key.
	 *
	 * @return array<string,mixed>
	 * @since 1.12.0
	 */
	public static function setup_default_taxonomies( $args, $taxonomy ) {
		// Adds GraphQL support for categories.
		if ( 'category' === $taxonomy ) {
			$args['show_in_graphql']     = true;
			$args['graphql_single_name'] = 'category';
			$args['graphql_plural_name'] = 'categories';
			$args['graphql_description'] = __( 'A taxonomy term that classifies content. Categories support hierarchy and can be used to create a nested structure.', 'wp-graphql' );
		} elseif ( 'post_tag' === $taxonomy ) { // Adds GraphQL support for tags.
			$args['show_in_graphql']     = true;
			$args['graphql_single_name'] = 'tag';
			$args['graphql_plural_name'] = 'tags';
			$args['graphql_description'] = __( 'A taxonomy term used to organize and classify content. Tags do not have a hierarchy and are generally used for more specific classifications.', 'wp-graphql' );
		} elseif ( 'post_format' === $taxonomy ) { // Adds GraphQL support for post formats.
			$args['show_in_graphql']     = true;
			$args['graphql_single_name'] = 'postFormat';
			$args['graphql_plural_name'] = 'postFormats';
			$args['graphql_description'] = __( 'A standardized classification system for content presentation styles. These formats can be used to display content differently based on type, such as "standard", "gallery", "video", etc.', 'wp-graphql' );
		}

		return $args;
	}

	/**
	 * Set the GraphQL Post Type Args and pass them through a filter.
	 *
	 * @param array<string,mixed> $args           The graphql specific args for the post type
	 * @param string              $post_type_name The name of the post type being registered
	 *
	 * @return array<string,mixed>
	 * @throws \Exception
	 * @since 1.12.0
	 */
	public static function register_graphql_post_type_args( array $args, string $post_type_name ): array {
		// Bail early if the post type is hidden from the WPGraphQL schema.
		if ( empty( $args['show_in_graphql'] ) ) {
			return $args;
		}

		$graphql_args = self::get_default_graphql_type_args();

		/**
		 * Filters the graphql args set on a post type
		 *
		 * @param array<string,mixed> $args           The graphql specific args for the post type
		 * @param string              $post_type_name The name of the post type being registered
		 */
		$graphql_args = apply_filters( 'register_graphql_post_type_args', $graphql_args, $post_type_name );

		return wp_parse_args( $args, $graphql_args );
	}

	/**
	 * Set the GraphQL Taxonomy Args and pass them through a filter.
	 *
	 * @param array<string,mixed> $args          The graphql specific args for the taxonomy
	 * @param string              $taxonomy_name The name of the taxonomy being registered
	 *
	 * @return array<string,mixed>
	 * @throws \Exception
	 * @since 1.12.0
	 */
	public static function register_graphql_taxonomy_args( array $args, string $taxonomy_name ): array {
		// Bail early if the taxonomy  is hidden from the WPGraphQL schema.
		if ( empty( $args['show_in_graphql'] ) ) {
			return $args;
		}

		$graphql_args = self::get_default_graphql_type_args();

		/**
		 * Filters the graphql args set on a taxonomy
		 *
		 * @param array<string,mixed> $args          The graphql specific args for the taxonomy
		 * @param string              $taxonomy_name The name of the taxonomy being registered
		 */
		$graphql_args = apply_filters( 'register_graphql_taxonomy_args', $graphql_args, $taxonomy_name );

		return wp_parse_args( $args, $graphql_args );
	}

	/**
	 * This sets the post type /taxonomy GraphQL properties.
	 *
	 * @since 1.12.0
	 *
	 * @return array{
	 *   graphql_kind: 'interface'|'object'|'union',
	 *   graphql_resolve_type: ?callable,
	 *   graphql_interfaces: string[],
	 *   graphql_connections: string[],
	 *   graphql_union_types: string[],
	 *   graphql_register_root_field: bool,
	 *   graphql_register_root_connection: bool,
	 * }
	 */
	public static function get_default_graphql_type_args(): array {
		return [
			// The "kind" of GraphQL type to register. Can be `interface`, `object`, or `union`.
			'graphql_kind'                     => 'object',
			// The callback used to resolve the type. Only used if `graphql_kind` is an `interface` or `union`.
			'graphql_resolve_type'             => null,
			// An array of custom interfaces the type should implement.
			'graphql_interfaces'               => [],
			// An array of default interfaces the type should exclude.
			'graphql_exclude_interfaces'       => [],
			// An array of custom connections the type should implement.
			'graphql_connections'              => [],
			// An array of default connection field names the type should exclude.
			'graphql_exclude_connections'      => [],
			// An array of possible type the union can resolve to. Only used if `graphql_kind` is a `union`.
			'graphql_union_types'              => [],
			// Whether to register default connections to the schema.
			'graphql_register_root_field'      => true,
			'graphql_register_root_connection' => true,
		];
	}

	/**
	 * Get the post types that are allowed to be used in GraphQL.
	 *
	 * This gets all post_types that are set to show_in_graphql, but allows for external code
	 * (plugins/theme) to filter the list of allowed_post_types to add/remove additional post_types
	 *
	 * @param 'names'|'objects'   $output Optional. The type of output to return. Accepts post type 'names' or 'objects'. Default 'names'.
	 * @param array<string,mixed> $args   Optional. Arguments to filter allowed post types
	 *
	 * @return string[]|\WP_Post_Type[]
	 * @phpstan-return ( $output is 'objects' ? \WP_Post_Type[] : string[] )
	 *
	 * @since  0.0.4
	 * @since  1.8.1 adds $output as first param, and stores post type objects in class property.
	 */
	public static function get_allowed_post_types( $output = 'names', $args = [] ): array {
		// Support deprecated param order.
		if ( is_array( $output ) ) {
			_deprecated_argument( __METHOD__, '1.8.1', 'Passing `$args` to the first parameter will no longer be supported in the next major version of WPGraphQL.' );
			$args   = $output;
			$output = 'names';
		}

		// Initialize array of allowed post type objects.
		if ( empty( self::$allowed_post_types ) ) {
			/**
			 * Get all post types objects.
			 *
			 * @var \WP_Post_Type[] $post_type_objects
			 */
			$post_type_objects = get_post_types(
				[ 'show_in_graphql' => true ],
				'objects'
			);

			$post_type_names = wp_list_pluck( $post_type_objects, 'name' );

			/**
			 * Pass through a filter to allow the post_types to be modified.
			 * For example if a certain post_type should not be exposed to the GraphQL API.
			 *
			 * @param string[]        $post_type_names   Array of post type names.
			 * @param \WP_Post_Type[] $post_type_objects Array of post type objects.
			 *
			 * @since 1.8.1 add $post_type_objects parameter.
			 * @since 0.0.2
			 */
			$allowed_post_type_names = apply_filters( 'graphql_post_entities_allowed_post_types', $post_type_names, $post_type_objects );

			// Filter the post type objects if the list of allowed types have changed.
			$post_type_objects = array_filter(
				$post_type_objects,
				static function ( $obj ) use ( $allowed_post_type_names ) {
					if ( empty( $obj->graphql_plural_name ) && ! empty( $obj->graphql_single_name ) ) {
						$obj->graphql_plural_name = $obj->graphql_single_name;
					}

					/**
					 * Validate that the post_types have a graphql_single_name and graphql_plural_name
					 */
					if ( empty( $obj->graphql_single_name ) || empty( $obj->graphql_plural_name ) ) {
						graphql_debug(
							sprintf(
							/* translators: %s will replaced with the registered type */
								__( 'The "%s" post_type isn\'t configured properly to show in GraphQL. It needs a "graphql_single_name" and a "graphql_plural_name"', 'wp-graphql' ),
								$obj->name
							),
							[
								'invalid_post_type' => $obj,
							]
						);
						return false;
					}

					return in_array( $obj->name, $allowed_post_type_names, true );
				}
			);

			self::$allowed_post_types = $post_type_objects;
		}

		/**
		 * Filter the list of allowed post types either by the provided args or to only return an array of names.
		 */
		if ( ! empty( $args ) || 'names' === $output ) {
			$field = 'names' === $output ? 'name' : false;

			return wp_filter_object_list( self::$allowed_post_types, $args, 'and', $field );
		}

		return self::$allowed_post_types;
	}

	/**
	 * Get the taxonomies that are allowed to be used in GraphQL.
	 * This gets all taxonomies that are set to "show_in_graphql" but allows for external code
	 * (plugins/themes) to filter the list of allowed_taxonomies to add/remove additional
	 * taxonomies
	 *
	 * @param 'names'|'objects'   $output Optional. The type of output to return. Accepts taxonomy 'names' or 'objects'. Default 'names'.
	 * @param array<string,mixed> $args   Optional. Arguments to filter allowed taxonomies.
	 *
	 * @return string[]|\WP_Taxonomy[]
	 * @phpstan-return ( $output is 'objects' ? \WP_Taxonomy[] : string[] )
	 * @since  0.0.4
	 */
	public static function get_allowed_taxonomies( $output = 'names', $args = [] ): array {

		// Initialize array of allowed post type objects.
		if ( empty( self::$allowed_taxonomies ) ) {
			/**
			 * Get all post types objects.
			 *
			 * @var \WP_Taxonomy[] $tax_objects
			 */
			$tax_objects = get_taxonomies(
				[ 'show_in_graphql' => true ],
				'objects'
			);

			$tax_names = wp_list_pluck( $tax_objects, 'name' );

			/**
			 * Pass through a filter to allow the taxonomies to be modified.
			 * For example if a certain taxonomy should not be exposed to the GraphQL API.
			 *
			 * @param string[]       $tax_names   Array of taxonomy names
			 * @param \WP_Taxonomy[] $tax_objects Array of taxonomy objects.
			 *
			 * @since 1.8.1 add $tax_names and $tax_objects parameters.
			 * @since 0.0.2
			 */
			$allowed_tax_names = apply_filters( 'graphql_term_entities_allowed_taxonomies', $tax_names, $tax_objects );

			$tax_objects = array_filter(
				$tax_objects,
				static function ( $obj ) use ( $allowed_tax_names ) {
					if ( empty( $obj->graphql_plural_name ) && ! empty( $obj->graphql_single_name ) ) {
						$obj->graphql_plural_name = $obj->graphql_single_name;
					}

					/**
					 * Validate that the post_types have a graphql_single_name and graphql_plural_name
					 */
					if ( empty( $obj->graphql_single_name ) || empty( $obj->graphql_plural_name ) ) {
						graphql_debug(
							sprintf(
							/* translators: %s will replaced with the registered taxonomy */
								__( 'The "%s" taxonomy isn\'t configured properly to show in GraphQL. It needs a "graphql_single_name" and a "graphql_plural_name"', 'wp-graphql' ),
								$obj->name
							),
							[
								'invalid_taxonomy' => $obj,
							]
						);
						return false;
					}

					return in_array( $obj->name, $allowed_tax_names, true );
				}
			);

			self::$allowed_taxonomies = $tax_objects;
		}

		$taxonomies = self::$allowed_taxonomies;
		/**
		 * Filter the list of allowed taxonomies either by the provided args or to only return an array of names.
		 */
		if ( ! empty( $args ) || 'names' === $output ) {
			$field = 'names' === $output ? 'name' : false;

			$taxonomies = wp_filter_object_list( $taxonomies, $args, 'and', $field );
		}

		return $taxonomies;
	}

	/**
	 * Allow Schema to be cleared.
	 */
	public static function clear_schema(): void {
		self::$type_registry      = null;
		self::$schema             = null;
		self::$allowed_post_types = null;
		self::$allowed_taxonomies = null;
	}

	/**
	 * Returns the Schema as defined by static registrations throughout
	 * the WP Load.
	 *
	 * @return \WPGraphQL\WPSchema
	 *
	 * @throws \Exception
	 */
	public static function get_schema() {
		if ( ! isset( self::$schema ) ) {
			$schema_registry = new SchemaRegistry();
			$schema          = $schema_registry->get_schema();

			/**
			 * Generate & Filter the schema.
			 *
			 * @param \WPGraphQL\WPSchema $schema The executable Schema that GraphQL executes against
			 * @param \WPGraphQL\AppContext $app_context Object The AppContext object containing all of the
			 * information about the context we know at this point
			 *
			 * @since 0.0.5
			 */
			self::$schema = apply_filters( 'graphql_schema', $schema, self::get_app_context() );
		}

		/**
		 * Fire an action when the Schema is returned
		 */
		do_action( 'graphql_get_schema', self::$schema );

		/**
		 * Return the Schema after applying filters
		 */
		return self::$schema;
	}

	/**
	 * Whether WPGraphQL is operating in Debug mode
	 */
	public static function debug(): bool {
		if ( defined( 'GRAPHQL_DEBUG' ) ) {
			$enabled = (bool) GRAPHQL_DEBUG;
		} else {
			$enabled = get_graphql_setting( 'debug_mode_enabled', 'off' );
			$enabled = 'on' === $enabled;
		}

		/**
		 * @param bool $enabled Whether GraphQL Debug is enabled or not
		 */
		return (bool) apply_filters( 'graphql_debug_enabled', $enabled );
	}

	/**
	 * Returns the type registry, instantiating it if it doesn't exist.
	 *
	 * @return \WPGraphQL\Registry\TypeRegistry
	 *
	 * @throws \Exception
	 */
	public static function get_type_registry() {
		if ( ! isset( self::$type_registry ) ) {
			$type_registry = new TypeRegistry();

			/**
			 * Generate & Filter the schema.
			 *
			 * @param \WPGraphQL\Registry\TypeRegistry $type_registry The TypeRegistry for the API
			 * @param \WPGraphQL\AppContext $app_context Object The AppContext object containing all of the
			 * information about the context we know at this point
			 *
			 * @since 0.0.5
			 */
			self::$type_registry = apply_filters( 'graphql_type_registry', $type_registry, self::get_app_context() );
		}

		/**
		 * Fire an action when the Type Registry is returned
		 */
		do_action( 'graphql_get_type_registry', self::$type_registry );

		/**
		 * Return the Schema after applying filters
		 */
		return self::$type_registry;
	}

	/**
	 * Return the static schema if there is one.
	 */
	public static function get_static_schema(): ?string {
		$schema_file = WPGRAPHQL_PLUGIN_DIR . 'schema.graphql';

		if ( ! file_exists( $schema_file ) ) {
			return null;
		}

		$schema = file_get_contents( WPGRAPHQL_PLUGIN_DIR . 'schema.graphql' );

		return ! empty( $schema ) ? $schema : null;
	}

	/**
	 * Get the AppContext for use in passing down the Resolve Tree
	 */
	public static function get_app_context(): AppContext {
		/**
		 * Configure the app_context which gets passed down to all the resolvers.
		 *
		 * @since 0.0.4
		 */
		$app_context           = new AppContext();
		$app_context->viewer   = wp_get_current_user();
		$app_context->root_url = get_bloginfo( 'url' );
		$app_context->request  = ! empty( $_REQUEST ) ? $_REQUEST : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		return $app_context;
	}
}
