<?php if (!defined('FW')) die('Forbidden');

/**
 * Lets you create easy functions for get/set database option values
 * it will handle all clever logic with default values, multikeys and processing options fw-storage parameter
 * @since 2.5.9
 */
abstract class FW_Db_Options_Model {
	/**
	 * @return string Must not contain '/'
	 */
	abstract protected function get_id();

	/**
	 * @param null|int|string $item_id
	 * @param array $extra_data
	 * @return mixed
	 */
	abstract protected function get_values($item_id, array $extra_data = array());

	/**
	 * @param null|int|string $item_id
	 * @param mixed $values
	 * @param array $extra_data
	 * @return void
	 */
	abstract protected function set_values($item_id, $values, array $extra_data = array());

	/**
	 * @param null|int|string $item_id
	 * @param array $extra_data
	 * @return array
	 */
	abstract protected function get_options($item_id, array $extra_data = array());

	/**
	 * @param null|int|string $item_id
	 * @param array $extra_data
	 * @return array E.g. for post options {'post-id': $item_id}
	 * @see fw_db_option_storage_type()
	 */
	abstract protected function get_fw_storage_params($item_id, array $extra_data = array());

	abstract protected function _init();

	/**
	 * @param null|int|string $item_id
	 * @param null|string $option_id
	 * @param null|string $sub_keys
	 * @param mixed $old_value
	 * @param array $extra_data
	 */
	protected function _after_set($item_id, $option_id, $sub_keys, $old_value, array $extra_data = array()) {}

	/**
	 * Get sub-key. For e.g. if each item must have a separate key or not.
	 * @param string $key
	 * @param null|int|string $item_id
	 * @param array $extra_data
	 * @return null|string
	 */
	protected function _get_cache_key($key, $item_id, array $extra_data = array()) {
		return empty($item_id) ? null : $item_id;
	}

	/**
	 * @var array {'id': mixed}
	 */
	private static $instances = array();

	/**
	 * @param string $id
	 * @return FW_Db_Options_Model
	 * @internal
	 */
	final public static function _get_instance($id) {
		return self::$instances[$id];
	}

	/**
	 * @return string
	 * @since 2.6.7
	 */
	final public function get_main_cache_key() {
		return 'fw-options-model:'. $this->get_id();
	}

	final public function __construct() {
		if (isset(self::$instances[ $this->get_id() ])) {
			trigger_error(__CLASS__ .' with id "'. $this->get_id() .'" was already defined', E_USER_ERROR);
		} else {
			self::$instances[ $this->get_id() ] = $this;
		}

		$this->_init();
	}

	private function get_cache_key($key, $item_id, array $extra_data = array()) {
		$item_key = $this->_get_cache_key($key, $item_id, $extra_data);

		return $this->get_main_cache_key() .'/'. $key . (empty($item_key) ? '' : '/'. $item_key);
	}

	/**
	 * @param null|int|string $item_id
	 * @param null|string $option_id
	 * @param mixed $default_value
	 * @param array $extra_data
	 * @return mixed
	 */
	final public function get( $item_id = null, $option_id = null, $default_value = null, array $extra_data = array() ) {

		if ( is_preview() ) {
			global $wp_query;

			if ( $wp_query->queried_object && ( is_single( $item_id ) || is_page( $item_id ) ) ) {
				$reset_get_rev = wp_get_post_revisions( $item_id );
				$item_id = ( $rewisions = reset( $reset_get_rev ) ) && isset( $rewisions->ID ) ? $rewisions->ID : $item_id;
			}
		}

		if ( empty( $option_id ) ) {
			$sub_keys = null;
		} else {
			$option_id  = explode( '/', $option_id ); // 'option_id/sub/keys'
			$_option_id = array_shift( $option_id ); // 'option_id'
			$sub_keys   = empty( $option_id ) ? null : implode( '/', $option_id ); // 'sub/keys'
			$option_id  = $_option_id;
			unset( $_option_id );
		}

		try {
			// Cached because values are merged with extracted default values
			$values = FW_Cache::get( $cache_key_values = $this->get_cache_key( 'values', $item_id, $extra_data ) );

		} catch ( FW_Cache_Not_Found_Exception $e ) {

			FW_Cache::set(
				$cache_key_values,
				$values = ( is_array( $values = $this->get_values( $item_id, $extra_data ) ) ? $values : array() )
			);
		}

		/**
		 * If db value is not found and default value is provided
		 * return default value before the options file is loaded
		 */
		if ( ! is_null( $default_value ) ) {
			if ( empty( $option_id ) ) {
				if ( empty( $values )
				     && (
					     is_array( $default_value )
					     ||
					     fw_is_callback( is_array( $default_value ) )
				     )
				) {
					return fw_call( $default_value );
				}
			} else {
				if ( is_null( $sub_keys ) ) {
					if ( ! isset( $values[ $option_id ] ) ) {
						return fw_call( $default_value );
					}
				} else {
					if ( ! isset( $values[ $option_id ] ) || is_null( fw_akg( $sub_keys, $values[ $option_id ] ) ) ) {
						return fw_call( $default_value );
					}
				}
			}
		}

		try {
			$options = FW_Cache::get( $cache_key = $this->get_cache_key( 'options', $item_id, $extra_data ) );
		} catch ( FW_Cache_Not_Found_Exception $e ) {
			FW_Cache::set( $cache_key, array() ); // prevent recursion
			FW_Cache::set( $cache_key, $options = fw_extract_only_options( $this->get_options( $item_id, $extra_data ) ) );
		}

		if ( $options ) {
			try {
				FW_Cache::get(
				// fixes https://github.com/ThemeFuse/Unyson/issues/2034
					$cache_key_values_processed = $this->get_cache_key( 'values:processed', $item_id, $extra_data )
				);
			} catch ( FW_Cache_Not_Found_Exception $e ) {
				/**
				 * Set cache value before processing options
				 * Fixes https://github.com/ThemeFuse/Unyson/issues/2034#issuecomment-248571149
				 */
				FW_Cache::set( $cache_key_values_processed, true );

				// Complete missing db values with default values from options array
				{
					try {
						$skip_types_process = FW_Cache::get( $cache_key = 'fw:options-default-values:skip-types' );
					} catch ( FW_Cache_Not_Found_Exception $e ) {
						FW_Cache::set(
							$cache_key,
							$skip_types_process = apply_filters( 'fw:options-default-values:skip-types', array(// 'type' => true
							) )
						);
					}

					foreach ( array_diff_key( fw_extract_only_options( $options ), $values ) as $id => $option ) {
						$values[ $id ] = isset( $skip_types_process[ $option['type'] ] )
							? (
							isset( $option['value'] )
								? $option['value']
								: fw()->backend->option_type( $option['type'] )->get_defaults( 'value' )
							)
							: fw()->backend->option_type( $option['type'] )->get_value_from_input( $option, null );
					}
				}

				foreach ( $options as $id => $option ) {
					$values[ $id ] = fw()->backend->option_type( $option['type'] )->storage_load(
						$id,
						$option,
						isset( $values[ $id ] ) ? $values[ $id ] : null,
						$this->get_fw_storage_params( $item_id, $extra_data )
					);
				}

				FW_Cache::set( $cache_key_values, $values );
			}
		}

		if ( empty( $option_id ) ) {
			return ( empty( $values ) && ( is_array( $default_value ) || fw_is_callback( $default_value ) ) )
				? fw_call( $default_value )
				: $values;
		} else {
			if ( is_null( $sub_keys ) ) {
				return isset( $values[ $option_id ] )
					? $values[ $option_id ]
					: fw_call( $default_value );
			} else {
				return isset( $values[ $option_id ] )
					? fw_akg( $sub_keys, $values[ $option_id ], $default_value )
					: fw_call( $default_value );
			}
		}
	}

	final public function set( $item_id = null, $option_id = null, $value = '', array $extra_data = array() ) {
		FW_Cache::del($cache_key_values = $this->get_cache_key('values', $item_id, $extra_data));
		FW_Cache::del($cache_key_values_processed = $this->get_cache_key('values:processed', $item_id, $extra_data));

		try {
			$options = FW_Cache::get($cache_key = $this->get_cache_key('options', $item_id, $extra_data));
		} catch (FW_Cache_Not_Found_Exception $e) {
			FW_Cache::set($cache_key, array()); // prevent recursion
			FW_Cache::set($cache_key, $options = fw_extract_only_options($this->get_options($item_id, $extra_data)));
		}

		$sub_keys = null;

		if ($option_id) {
			$option_id = explode('/', $option_id); // 'option_id/sub/keys'
			$_option_id = array_shift($option_id); // 'option_id'
			$sub_keys = empty($option_id) ? null : implode('/', $option_id); // 'sub/keys'
			$option_id = $_option_id;
			unset($_option_id);

			$old_values = is_array($old_values = $this->get_values($item_id, $extra_data)) ? $old_values : array();
			$old_value = isset($old_values[$option_id]) ? $old_values[$option_id] : null;

			if ($sub_keys) { // update sub_key in old_value and use the entire value
				$new_value = $old_value;
				fw_aks($sub_keys, $value, $new_value);
				$value = $new_value;
				unset($new_value);

				$old_value = fw_akg($sub_keys, $old_value);
			}

			if (isset($options[$option_id])) {
				$value = fw()->backend->option_type($options[$option_id]['type'])->storage_save(
					$option_id,
					$options[$option_id],
					$value,
					$this->get_fw_storage_params($item_id, $extra_data)
				);
			}

			$old_values[$option_id] = $value;

			$this->set_values($item_id, $old_values, $extra_data);

			unset($old_values);
		} else {
			$old_value = is_array($old_values = $this->get_values($item_id, $extra_data)) ? $old_values : array();

			if ( ! is_array($value) ) {
				$value = array();
			}

			if (empty($value)) {
				// All options reset. Reset all fw-storage values too
				// Fixes https://github.com/ThemeFuse/Unyson/issues/2179
				foreach ($options as $_option_id => $_option) {
					fw()->backend->option_type($options[$_option_id]['type'])->storage_save(
						$_option_id,
						$_option,
						fw()->backend->option_type($options[$_option_id]['type'])->get_defaults('value'),
						$this->get_fw_storage_params($item_id, $extra_data)
					);
				}
			} else {
				foreach ($value as $_option_id => $_option_value) {
					if (isset($options[$_option_id])) {
						$value[$_option_id] = fw()->backend->option_type($options[$_option_id]['type'])->storage_save(
							$_option_id,
							$options[$_option_id],
							$_option_value,
							$this->get_fw_storage_params($item_id, $extra_data)
						);
					}
				}
			}

			$this->set_values($item_id, $value, $extra_data);
		}

		FW_Cache::del($cache_key_values); // fixes https://github.com/ThemeFuse/Unyson/issues/1538
		FW_Cache::del($cache_key_values_processed);

		$this->_after_set($item_id, $option_id, $sub_keys, $old_value, $extra_data);
	}
}
