<?php
	/**
	 * @package     Freemius
	 * @copyright   Copyright (c) 2015, Freemius, Inc.
	 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
	 * @since       1.2.3
	 */

	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	/**
	 * Class FS_Options
	 *
	 * A wrapper class for handling network level and single site level options.
	 */
	class FS_Options {
		/**
		 * @var string
		 */
		private $_id;

		/**
		 * @var array[string]FS_Options {
		 * @key   string
		 * @value FS_Options
		 * }
		 */
		private static $_instances;

		/**
		 * @var FS_Option_Manager Site level options.
		 */
		private $_options;

		/**
		 * @var FS_Option_Manager Network level options.
		 */
		private $_network_options;

		/**
		 * @var int The ID of the blog that is associated with the current site level options.
		 */
		private $_blog_id = 0;

		/**
		 * @var bool
		 */
		private $_is_multisite;

		/**
		 * @var string[] Lazy collection of params on the site level.
		 */
		private static $_SITE_OPTIONS_MAP;

		/**
		 * @author Leo Fajardo (@leorw)
		 * @since  2.0.0
		 *
		 * @param string $id
		 * @param bool   $load
		 *
		 * @return FS_Options
		 */
		static function instance( $id, $load = false ) {
			if ( ! isset( self::$_instances[ $id ] ) ) {
				self::$_instances[ $id ] = new FS_Options( $id, $load );
			}

			return self::$_instances[ $id ];
		}

		/**
		 * @author Leo Fajardo (@leorw)
		 * @since  2.0.0
		 *
		 * @param string $id
		 * @param bool   $load
		 */
		private function __construct( $id, $load = false ) {
			$this->_id           = $id;
			$this->_is_multisite = is_multisite();

			if ( $this->_is_multisite ) {
				$this->_blog_id         = get_current_blog_id();
				$this->_network_options = FS_Option_Manager::get_manager( $id, $load, true );
			}

			$this->_options = FS_Option_Manager::get_manager( $id, $load, $this->_blog_id );
		}

		/**
		 * Switch the context of the site level options manager.
		 *
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param $blog_id
		 */
		function set_site_blog_context( $blog_id ) {
			$this->_blog_id = $blog_id;

			$this->_options = FS_Option_Manager::get_manager( $this->_id, false, $this->_blog_id );
		}

		/**
		 * @author Leo Fajardo (@leorw)
		 *
		 * @param string        $option
		 * @param mixed         $default
		 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_SITE_LEVEL_PARAMS).
		 *
		 * @return mixed
		 */
		function get_option( $option, $default = null, $network_level_or_blog_id = null ) {
			if ( $this->should_use_network_storage( $option, $network_level_or_blog_id ) ) {
				return $this->_network_options->get_option( $option, $default );
			}

			$site_options = $this->get_site_options( $network_level_or_blog_id );

			return $site_options->get_option( $option, $default );
		}

		/**
		 * @author Leo Fajardo (@leorw)
		 * @since  2.0.0
		 *
		 * @param string        $option
		 * @param mixed         $value
		 * @param bool          $flush
		 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_SITE_LEVEL_PARAMS).
		 */
		function set_option( $option, $value, $flush = false, $network_level_or_blog_id = null ) {
			if ( $this->should_use_network_storage( $option, $network_level_or_blog_id ) ) {
				$this->_network_options->set_option( $option, $value, $flush );
			} else {
				$site_options = $this->get_site_options( $network_level_or_blog_id );
				$site_options->set_option( $option, $value, $flush );
			}
		}

		/**
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param string        $option
		 * @param bool          $flush
		 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_SITE_LEVEL_PARAMS).
		 */
		function unset_option( $option, $flush = false, $network_level_or_blog_id = null ) {
			if ( $this->should_use_network_storage( $option, $network_level_or_blog_id ) ) {
				$this->_network_options->unset_option( $option, $flush );
			} else {
				$site_options = $this->get_site_options( $network_level_or_blog_id );
				$site_options->unset_option( $option, $flush );
			}
		}

		/**
		 * @author Leo Fajardo (@leorw)
		 * @since  2.0.0
		 *
		 * @param bool $flush
		 * @param bool $network_level
		 */
		function load( $flush = false, $network_level = true ) {
			if ( $this->_is_multisite && $network_level ) {
				$this->_network_options->load( $flush );
			} else {
				$this->_options->load( $flush );
			}
		}

		/**
		 * @author Leo Fajardo (@leorw)
		 * @since  2.0.0
		 *
		 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, store both network storage and the current context blog storage.
		 */
		function store( $network_level_or_blog_id = null ) {
			if ( ! $this->_is_multisite ||
				 false === $network_level_or_blog_id ||
				 0 == $network_level_or_blog_id ||
				 is_null( $network_level_or_blog_id )
			) {
				$site_options = $this->get_site_options( $network_level_or_blog_id );
				$site_options->store();
			}

			if ( $this->_is_multisite &&
				 ( is_null( $network_level_or_blog_id ) || true === $network_level_or_blog_id )
			) {
				$this->_network_options->store();
			}
		}

		/**
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param int|null|bool $network_level_or_blog_id
		 * @param bool          $flush
		 */
		function clear( $network_level_or_blog_id = null, $flush = false ) {
			if ( ! $this->_is_multisite ||
				 false === $network_level_or_blog_id ||
				 is_null( $network_level_or_blog_id ) ||
				 is_numeric( $network_level_or_blog_id )
			) {
				$site_options = $this->get_site_options( $network_level_or_blog_id );
				$site_options->clear( $flush );
			}

			if ( $this->_is_multisite &&
				 ( true === $network_level_or_blog_id || is_null( $network_level_or_blog_id ) )
			) {
				$this->_network_options->clear( $flush );
			}
		}

		/**
		 * Migration script to the new storage data structure that is network compatible.
		 *
		 * IMPORTANT:
		 *      This method should be executed only after it is determined if this is a network
		 *      level compatible product activation.
		 *
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param int $blog_id
		 */
		function migrate_to_network( $blog_id = 0 ) {
			if ( ! $this->_is_multisite ) {
				return;
			}

			$updated = false;

			$site_options = $this->get_site_options( $blog_id );

			$keys = $site_options->get_options_keys();

			foreach ( $keys as $option ) {
				if ( $this->is_site_option( $option ) ||
					 // Don't move admin notices to the network storage.
					in_array($option, array(
						// Don't move admin notices to the network storage.
						'admin_notices',
						// Don't migrate the module specific data, it will be migrated by the FS_Storage.
						'plugin_data',
						'theme_data',
					))
				) {
					continue;
				}

				$option_updated = false;

				// Migrate option to the network storage.
				$site_option = $site_options->get_option( $option );

				if ( ! $this->_network_options->has_option( $option ) ) {
					// Option not set on the network level, so just set it.
					$this->_network_options->set_option( $option, $site_option, false );

					$option_updated = true;
				} else {
					// Option already set on the network level, so we need to merge it inelegantly.
					$network_option = $this->_network_options->get_option( $option );

					if ( is_array( $network_option ) && is_array( $site_option ) ) {
						// Option is an array.
						foreach ( $site_option as $key => $value ) {
							if ( ! isset( $network_option[ $key ] ) ) {
								$network_option[ $key ] = $value;

								$option_updated = true;
							} else if ( is_array( $network_option[ $key ] ) && is_array( $value ) ) {
								if ( empty( $network_option[ $key ] ) ) {
									$network_option[ $key ] = $value;

									$option_updated = true;
								} else if ( empty( $value ) ) {
									// Do nothing.
								} else {
									reset($value);
									$first_key = key($value);
									if ( $value[$first_key] instanceof FS_Entity ) {
										// Merge entities by IDs.
										$network_entities_ids = array();
										foreach ( $network_option[ $key ] as $entity ) {
											$network_entities_ids[ $entity->id ] = true;
										}

										foreach ( $value as $entity ) {
											if ( ! isset( $network_entities_ids[ $entity->id ] ) ) {
												$network_option[ $key ][] = $entity;

												$option_updated = true;
											}
										}
									}
								}
							}
						}
					}

					if ( $option_updated ) {
						$this->_network_options->set_option( $option, $network_option, false );
					}
				}

				/**
				 * Remove the option from site level storage.
				 *
				 * IMPORTANT:
				 *      The line below is intentionally commented since we want to preserve the option
				 *      on the site storage level for "downgrade compatibility". Basically, if the user
				 *      will downgrade to an older version of the plugin with the prev storage structure,
				 *      it will continue working.
				 *
				 * @todo After a few releases we can remove this.
				 */
//                    $site_options->unset_option($option, false);

				if ( $option_updated ) {
					$updated = true;
				}
			}

			if ( ! $updated ) {
				return;
			}

			// Update network level storage.
			$this->_network_options->store();
//            $site_options->store();
		}


		#--------------------------------------------------------------------------------
		#region Helper Methods
		#--------------------------------------------------------------------------------

		/**
		 * We don't want to load the map right away since it's not even needed in a non-MS environment.
		 *
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 */
		private static function load_site_options_map() {
			self::$_SITE_OPTIONS_MAP = array(
				'sites'          => true,
				'theme_sites'    => true,
				'unique_id'      => true,
				'active_plugins' => true,
			);
		}

		/**
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param string $option
		 *
		 * @return bool
		 */
		private function is_site_option( $option ) {
			if ( WP_FS__ACCOUNTS_OPTION_NAME != $this->_id ) {
				return false;
			}

			if ( ! isset( self::$_SITE_OPTIONS_MAP ) ) {
				self::load_site_options_map();
			}

			return isset( self::$_SITE_OPTIONS_MAP[ $option ] );
		}

		/**
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param int $blog_id
		 *
		 * @return FS_Option_Manager
		 */
		private function get_site_options( $blog_id = 0 ) {
			if ( 0 == $blog_id || $blog_id == $this->_blog_id ) {
				return $this->_options;
			}

			return FS_Option_Manager::get_manager( $this->_id, true, $blog_id );
		}

		/**
		 * Check if an option should be stored on the MS network storage.
		 *
		 * @author Vova Feldman (@svovaf)
		 * @since  2.0.0
		 *
		 * @param string        $option
		 * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_SITE_LEVEL_PARAMS).
		 *
		 * @return bool
		 */
		private function should_use_network_storage( $option, $network_level_or_blog_id = null ) {
			if ( ! $this->_is_multisite ) {
				// Not a multisite environment.
				return false;
			}

			if ( is_numeric( $network_level_or_blog_id ) ) {
				// Explicitly asked to use a specified blog storage.
				return false;
			}

			if ( is_bool( $network_level_or_blog_id ) ) {
				// Explicitly specified whether should use the network or blog level storage.
				return $network_level_or_blog_id;
			}

			// Determine which storage to use based on the option.
			return ! $this->is_site_option( $option );
		}

		#endregion
	}