<?php
/**
 * Handles registering and storing feature instances
 *
 * @since  2.1
 * @package elasticpress
 */

namespace ElasticPress;

use ElasticPress\Utils as Utils;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class for storing and managing features
 */
class Features {

	/**
	 * Stores all features that have been properly included (both active and inactive)
	 *
	 * @since  2.1
	 * @var array
	 */
	public $registered_features = [];

	/**
	 * Initiate class actions
	 *
	 * @since 2.1
	 */
	public function setup() {
		// hooks order matters, make sure feature activation goes before features setup
		add_action( 'init', array( $this, 'handle_feature_activation' ), 0 );
		add_action( 'init', array( $this, 'setup_features' ), 0 );
	}

	/**
	 * Activate a feature
	 *
	 * @param  string $slug Feature slug
	 * @since  2.2
	 */
	public function activate_feature( $slug ) {
		$this->update_feature( $slug, array( 'active' => true ) );
	}

	/**
	 * Dectivate a feature
	 *
	 * @param  string $slug Feature slug
	 * @param  bool   $force Whether to force deactivation
	 * @since  2.2
	 */
	public function deactivate_feature( $slug, $force = true ) {
		$this->update_feature( $slug, array( 'active' => false ), $force );
	}

	/**
	 * Registers a feature for use in ElasticPress
	 *
	 * @param  Feature $feature An instance of the Feature class
	 * @since  3.0
	 * @return boolean
	 */
	public function register_feature( Feature $feature ) {
		$feature_args['slug'] = $feature->slug;

		$this->registered_features[ $feature->slug ] = $feature;

		return true;
	}

	/**
	 * Easy access function to get a Feature object from a slug
	 *
	 * @param  string $slug Feature slug
	 * @since  2.1
	 * @return Feature
	 */
	public function get_registered_feature( $slug ) {
		if ( empty( $this->registered_features[ $slug ] ) ) {
			return false;
		}

		return $this->registered_features[ $slug ];
	}

	/**
	 * Activate or deactivate a feature
	 *
	 * @param  string $slug Feature slug
	 * @param  array  $settings Array of settings
	 * @param  bool   $force Whether to force activate/deactivate
	 * @since  2.2
	 * @return array|bool
	 */
	public function update_feature( $slug, $settings, $force = true ) {
		$feature = $this->get_registered_feature( $slug );

		if ( empty( $feature ) ) {
			return false;
		}

		$original_state = $feature->is_active();

		$feature_settings = Utils\get_option( 'ep_feature_settings', [] );
		// VIP: Backfill option
		if ( function_exists( 'vip_maybe_backfill_ep_option' ) ) { // TODO: Remove
			$feature_settings = \vip_maybe_backfill_ep_option( $feature_settings, 'ep_feature_settings' );
		}

		if ( empty( $feature_settings[ $slug ] ) ) {
			// If doesn't exist, merge with feature defaults
			$feature_settings[ $slug ] = wp_parse_args( $settings, $feature->default_settings );
		} else {
			// If exist just merge changed values into current
			$feature_settings[ $slug ] = wp_parse_args( $settings, $feature_settings[ $slug ] );
		}

		// Make sure active is a proper bool
		$feature_settings[ $slug ]['active'] = (bool) $feature_settings[ $slug ]['active'];

		if ( $feature_settings[ $slug ]['active'] ) {
			$feature_settings[ $slug ]['force_inactive'] = false;
		}

		// This means someone has explicitly deactivated the feature
		if ( $force ) {
			if ( ! (bool) $settings['active'] && $original_state ) {
				$feature_settings[ $slug ]['force_inactive'] = true;
			}
		}

		$sanitize_feature_settings = apply_filters( 'ep_sanitize_feature_settings', $feature_settings, $feature );

		Utils\update_option( 'ep_feature_settings', $sanitize_feature_settings );

		$data = array(
			'active'  => $sanitize_feature_settings[ $slug ]['active'],
			'reindex' => false,
		);

		if ( $feature_settings[ $slug ]['active'] && ! $original_state ) {
			if ( ! empty( $feature->requires_install_reindex ) ) {
				$data['reindex'] = true;
			}

			$feature->post_activation();
		}

		/**
		 * Fires after activating, inactivating, or just updating a feature.
		 *
		 * @hook ep_after_update_feature
		 * @param  {string} $feature Feature slug
		 * @param  {array} $settings Feature settings
		 * @param  {array} $data Feature activation data
		 *
		 * @since 3.5.5
		 */
		do_action(
			'ep_after_update_feature',
			$slug,
			$settings,
			$data
		);

		return $data;
	}

	/**
	 * When plugins are adjusted, we need to determine how to activate/deactivate features
	 *
	 * @since 2.2
	 */
	public function handle_feature_activation() {
		/**
		 * Save our current requirement statuses for later
		 */

		$old_requirement_statuses = Utils\get_option( 'ep_feature_requirement_statuses', false );

		$new_requirement_statuses = [];

		foreach ( $this->registered_features as $slug => $feature ) {
			$status                            = $feature->requirements_status();
			$new_requirement_statuses[ $slug ] = (int) $status->code;
		}

		$is_wp_cli = defined( 'WP_CLI' ) && \WP_CLI;

		if ( $is_wp_cli || is_admin() ) {
			Utils\update_option( 'ep_feature_requirement_statuses', $new_requirement_statuses );
		}

		/**
		 * If feature settings aren't created, let's create them and finish
		 */

		$feature_settings = Utils\get_option( 'ep_feature_settings', false );

		if ( false === $feature_settings ) {
			$registered_features = $this->registered_features;

			foreach ( $registered_features as $slug => $feature ) {
				if ( 0 === $feature->requirements_status()->code ) {
					$this->activate_feature( $slug );
				}
			}

			/**
			 * Nothing else to do since we are doing initial activation
			 */
			return;
		}

		/**
		 * If a requirement status changes, we need to handle that by activating/deactivating/showing notification
		 */

		if ( ( $is_wp_cli || is_admin() ) && ! empty( $old_requirement_statuses ) ) {
			foreach ( $new_requirement_statuses as $slug => $code ) {
				$feature = $this->get_registered_feature( $slug );

				// If a feature is forced inactive, do nothing
				$feature_settings = $feature->get_settings();
				if ( is_array( $feature_settings ) && ! empty( $feature_settings['force_inactive'] ) ) {
					continue;
				}

				// This is a new feature
				if ( ! isset( $old_requirement_statuses[ $slug ] ) ) {
					if ( 0 === $code ) {
						$this->activate_feature( $slug );

						if ( $feature->requires_install_reindex ) {
							Utils\update_option( 'ep_feature_auto_activated_sync', sanitize_text_field( $slug ) );
						}
					}
				} else {
					// This feature has a 0 "ok" code when it did not before
					if ( $old_requirement_statuses[ $slug ] !== $code && ( 0 === $code || 2 === $code ) ) {
						$active = ( 0 === $code );

						if ( ! $feature->is_active() && $active ) {
							$this->activate_feature( $slug );

							// Need to activate and maybe set a sync notice
							if ( $feature->requires_install_reindex ) {
								Utils\update_option( 'ep_feature_auto_activated_sync', sanitize_text_field( $slug ) );
							}
						} elseif ( $feature->is_active() && ! $active ) {
							// Just deactivate, don't force
							$this->deactivate_feature( $slug, false );
						}
					}
				}
			}
		}
	}

	/**
	 * Set up all active features
	 *
	 * @since  2.1
	 */
	public function setup_features() {
		/**
		 * Fires before features are setup
		 *
		 * @hook ep_setup_features
		 * @since  2.1
		 */
		do_action( 'ep_setup_features' );

		foreach ( $this->registered_features as $feature_slug => $feature ) {
			if ( $feature->is_active() ) {
				$feature->setup();
			}
		}
	}

	/**
	 * Return singleton instance of class
	 *
	 * @return object
	 * @since 2.1
	 */
	public static function factory() {
		static $instance = false;

		if ( ! $instance ) {
			$instance = new self();
			$instance->setup();
		}

		return $instance;
	}
}
