<?php
/**
 * Class Settings manages common settings for modules.
 *
 * @package Hummingbird\Core
 * @since 1.8
 */

namespace Hummingbird\Core;

use Hummingbird\Core\SafeMode;

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

/**
 * Class Settings
 */
class Settings {

	/**
	 * Plugin instance.
	 *
	 * @var Settings
	 */
	private static $instance;

	/**
	 * List of available modules.
	 *
	 * @since 1.8
	 *
	 * @var array
	 */
	private static $available_modules = array(
		'minify',
		'page_cache',
		'performance',
		'uptime',
		'gravatar',
		'caching',
		'cloudflare',
		'advanced',
		'rss',
		'settings',
		'redis',
		'database',
	);

	/**
	 * List of network modules that have settings for each sub-site.
	 *
	 * @since 1.8
	 *
	 * @var array
	 */
	private static $network_modules = array( 'caching', 'minify', 'page_cache', 'performance', 'advanced', 'cloudflare' );

	/**
	 * Cached default settings.
	 *
	 * @var array|null
	 */
	private static $default_settings = null;

	/**
	 * Return the plugin instance.
	 *
	 * @return Settings
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Settings constructor.
	 */
	private function __construct() {
	}

	/**
	 * Return the plugin default settings.
	 *
	 * @return array  Default Hummingbird settings.
	 */
	public static function get_default_settings() {
		$defaults = array(
			'minify'      => array(
				'enabled'                               => false,
				'use_cdn'                               => true,
				'delay_js'                              => false,
				'critical_css'                          => false,
				'critical_css_type'                     => 'remove',
				'critical_css_remove_type'              => 'user_interaction_with_remove',
				'critical_css_mode'                     => '',
				'critical_page_types'                   => array(),
				'critical_skipped_custom_post_types'    => array(),
				'above_fold_load_stylesheet_method'     => 'load_stylesheet_on_user_interaction',
				'critical_css_files_exclusion'          => array(),
				'critical_css_post_urls_exclusion'      => array(),
				'critical_css_plugins_themes_exclusion' => array(),
				'critical_css_keywords'                 => array(),
				'font_optimization'                     => false,
				'preload_fonts'                         => '',
				'font_swap'                             => false,
				'font_display_value'                    => 'swap',
				'preload_fonts_mode'                    => 'automatic',
				'log'                                   => false,
				'file_path'                             => '',
				// Only for multisites. Toggles minification in a subsite
				// By default is true as if 'minify'-'enabled' is set to false, this option has no meaning.
				'minify_blog'                           => false,
				'view'                                  => 'basic', // Accepts: 'basic' or 'advanced'.
				'type'                                  => 'speedy', // Accepts: 'speedy' or 'basic'.
				'compress'                              => true,
				'combine'                               => true,
				'do_assets'                             => array( // Assets to optimize.
					'styles'  => true,
					'scripts' => true,
					'fonts'   => true,
				),
				// Only for multisite.
				'block'                                 => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'dont_minify'                           => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'dont_combine'                          => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'position'                              => array( // Move to footer.
					'scripts' => array(),
					'styles'  => array(),
				),
				'defer'                                 => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'inline'                                => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'nocdn'                                 => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'delay_js_exclusions'                   => '',
				'delay_js_exclusion_list'               => false,
				'delay_js_timeout'                      => 20,
				'fonts'                                 => array(),
				'delay_js_files_exclusion'              => array(),
				'delay_js_post_types_exclusion'         => array(),
				'delay_js_post_urls_exclusion'          => array(),
				'delay_js_plugins_themes_exclusion'     => array(),
				'delay_js_ads_tracker_exclusion'        => array(),
				'delay_js_exclude_inline_js'            => false,
				'delay_js_keywords_advanced_view'       => false,
				'preload'                               => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'async'                                 => array(
					'scripts' => array(),
					'styles'  => array(),
				),
				'ao_completed_time'                     => '',
			),
			'uptime'      => array(
				'enabled'       => false,
				'notifications' => array(
					'enabled' => false,
				),
				'reports'       => array(
					'enabled' => false,
				),
			),
			'gravatar'    => array(
				'enabled' => false,
			),
			'page_cache'  => array(
				'enabled'      => false,
				// Only for multisites. Toggles page caching in a subsite
				// By default is true as if 'page_cache'-'enabled' is set to false, this option has no meaning.
				'cache_blog'   => true,
				'detection'    => 'auto', // Accepts: manual, auto and none.
				'pages_cached' => 0,
				'integrations' => array(
					'varnish' => false,
					'opcache' => false,
				),
				'preload'      => true,
				'preload_type' => array(
					'home_page' => true,
					'on_clear'  => false,
				),
			),
			'caching'     => array(
				// Always enabled, so no 'enabled' option.
				'expiry_css'        => '1y/A31536000',
				'expiry_javascript' => '1y/A31536000',
				'expiry_media'      => '1y/A31536000',
				'expiry_images'     => '1y/A31536000',
			),
			'cloudflare'  => array(
				'enabled'      => false,
				'connected'    => false,
				'last_check'   => false,
				'email'        => '',
				'api_key'      => '',
				'account_id'   => '',
				'zone'         => '',
				'zone_name'    => '',
				'plan'         => false,
				'page_rules'   => array(),
				'cache_expiry' => 31536000,
				'apo_paid'     => false,
				'apo'          => array(),
			),
			'performance' => array(
				'reports'       => array(
					'enabled' => false,
				),
				'subsite_tests' => true,
				'dismissed'     => false,
			),
			'advanced'    => array(
				'query_string'         => false,
				'query_strings_global' => false, // If true, will force query_string on all subsites.
				'emoji'                => false,
				'post_revisions'       => false,
				'viewport_meta'        => true,
				'emoji_global'         => false, // If true, will force emoji on all subsites.
				'prefetch'             => array(),
				'preconnect'           => array(),
				'cart_fragments'       => false,
				'lazy_load'            => array(
					'enabled'   => false,
					'method'    => 'click',
					'button'    => array(
						'dimensions' => array(
							'height' => 0,
							'width'  => 0,
							'radius' => 0,
						),
						'color'      => array(
							'background' => '',
							'border'     => '',
							'hover'      => '',
						),
						'alignment'  => array(
							'align'      => 'center',
							'full_width' => 'on',
							'left'       => 0,
							'right'      => 0,
							'top'        => 0,
							'bottom'     => 0,
						),
					),
					'threshold' => 10,
					'preload'   => false,
				),
			),
			'rss'         => array(
				'enabled'  => true,
				'duration' => 3600,
			),
			'settings'    => array(
				'accessible_colors' => false,
				'remove_settings'   => false,
				'remove_data'       => false,
				'tracking'          => false,
				'control'           => false, // Cache control in admin bar.
			),
			'redis'       => array(
				'enabled' => false,
			),
			'database'    => array(
				'reports' => array(
					'enabled' => false,
				),
			),
		);

		/**
		 * Filter the default settings.
		 * Useful when adding new settings to the plugin
		 */
		return apply_filters( 'wp_hummingbird_default_options', $defaults );
	}

	/**
	 * Array of settings per sub-site.
	 *
	 * @access private
	 *
	 * @param string $module  Module for to get sub site setting fields for.
	 *
	 * @return array
	 */
	private static function get_blog_option_names( $module ) {
		if ( ! in_array( $module, self::$network_modules, true ) ) {
			return array();
		}

		$options = array(
			'caching'     => array( 'expiry_css', 'expiry_javascript', 'expiry_media', 'expiry_images' ),
			'minify'      => array( 'minify_blog', 'view', 'type', 'combine', 'compress', 'do_assets', 'block', 'dont_minify', 'dont_combine', 'position', 'defer', 'inline', 'nocdn', 'fonts', 'preload', 'async', 'ao_completed_time', 'delay_js', 'delay_js_exclusions', 'delay_js_exclusion_list', 'delay_js_timeout', 'delay_js_files_exclusion', 'delay_js_post_types_exclusion', 'delay_js_post_urls_exclusion', 'delay_js_plugins_themes_exclusion', 'delay_js_ads_tracker_exclusion', 'delay_js_keywords_advanced_view', 'delay_js_exclude_inline_js', 'critical_css', 'critical_css_type', 'critical_css_remove_type', 'critical_css_mode', 'critical_page_types', 'critical_skipped_custom_post_types', 'font_optimization', 'above_fold_load_stylesheet_method', 'critical_css_files_exclusion', 'critical_css_post_urls_exclusion', 'critical_css_plugins_themes_exclusion', 'critical_css_keywords', 'preload_fonts', 'font_swap', 'font_display_value', 'preload_fonts_mode' ),
			'page_cache'  => array( 'cache_blog' ),
			'performance' => array( 'dismissed', 'reports' ),
			'advanced'    => array( 'query_string', 'viewport_meta', 'emoji', 'post_revisions', 'prefetch', 'preconnect', 'cart_fragments' ),
			'cloudflare'  => array( 'enabled', 'connected', 'last_check', 'email', 'api_key', 'account_id', 'zone', 'zone_name', 'plan', 'page_rules', 'cache_expiry', 'apo_paid', 'apo' ),
		);

		return $options[ $module ];
	}

	/**
	 * Filter out sub site options from network options on multisite.
	 *
	 * @access private
	 *
	 * @param array $options  Options array.
	 *
	 * @return array
	 */
	private static function filter_multisite_options( $options ) {
		$network_options = array();
		$blog_options    = array();

		foreach ( $options as $module => $setting ) {
			/*
			 * Skip if module is not registered.
			 * Only needed in case an update to 1.8 manually by replacing the files.
			 */
			if ( ! in_array( $module, self::$available_modules, true ) ) {
				continue;
			}

			$data = array_fill_keys( self::get_blog_option_names( $module ), self::get_blog_option_names( $module ) );

			$network_options[ $module ] = array_diff_key( $setting, $data );
			$blog_options[ $module ]    = array_intersect_key( $setting, $data );
		}

		// array_filter will remove all empty values.
		return array(
			'network' => $network_options,
			'blog'    => array_filter( $blog_options ),
		);
	}

	/**
	 * Reset database to default settings. Will overwrite all current settings.
	 * This can be moved out to update_settings, because it's almost identical.
	 */
	public static function reset_to_defaults() {
		Utils::get_module( 'redis' )->disable();

		$defaults = self::get_default_settings();

		if ( ! is_multisite() ) {
			update_option( self::get_setting_name(), $defaults );
		} else {
			$options = self::filter_multisite_options( $defaults );
			update_site_option( self::get_setting_name(), $options['network'] );
			update_option( self::get_setting_name(), $options['blog'] );
		}
	}

	/**
	 * Return the plugin settings.
	 *
	 * @param bool|string $for_module  Module to fetch options for.
	 *
	 * @return array  Hummingbird settings.
	 * @param bool|string $force_setting_name  Force a specific setting name.
	 */
	public static function get_settings( $for_module = false, $force_setting_name = false ) {
		$setting_name = $force_setting_name ? $force_setting_name : self::get_setting_name();
		if ( ! is_multisite() ) {
			$options = get_option( $setting_name, array() );
		} else {
			$blog_options    = get_option( $setting_name, array() );
			$network_options = get_site_option( $setting_name, array() );
			$options         = array_merge_recursive( $blog_options, $network_options );
		}

		$defaults = self::get_default_settings();

		// We need to parse each module individually.
		foreach ( $defaults as $module => $option ) {
			// If there is nothing set in the current option, we use the default set.
			if ( ! isset( $options[ $module ] ) ) {
				$options[ $module ] = $option;
				continue;
			}
			// Else we combine defaults with current options.
			$options[ $module ] = wp_parse_args( $options[ $module ], $option );
		}

		if ( $for_module ) {
			return apply_filters( "wphb_get_settings_for_module_$for_module", $options[ $for_module ] );
		}

		return $options;
	}

	/**
	 * Update the plugin settings.
	 *
	 * @param array       $new_settings  New settings.
	 * @param bool|string $for_module    Module to update settings for.
	 * @param bool|string $force_setting_name  Force a specific setting name.
	 */
	public static function update_settings( $new_settings, $for_module = false, $force_setting_name = false ) {
		$setting_name = $force_setting_name ? $force_setting_name : self::get_setting_name();

		if ( $for_module ) {
			$options                = self::get_settings( false, $setting_name );
			$options[ $for_module ] = $new_settings;
			$new_settings           = $options;
		}

		if ( 'wphb_safe_mode_settings' === $setting_name ) {
			$safemode_settings = SafeMode::filter_options( $new_settings );
			foreach ( $safemode_settings as $module => $settings ) {
				$new_settings[ $module ] = wp_parse_args( $settings, $new_settings[ $module ] );
			}
		}

		if ( ! is_multisite() ) {
			update_option( $setting_name, $new_settings );
		} else {
			$options = self::filter_multisite_options( $new_settings );
			update_site_option( $setting_name, $options['network'] );
			update_option( $setting_name, $options['blog'] );
		}
	}

	/**
	 * Get setting.
	 *
	 * @param string      $option_name  Return a single WP Hummingbird setting.
	 * @param bool|string $for_module   Module to fetch options for.
	 *
	 * @return mixed
	 */
	public static function get_setting( $option_name, $for_module = false ) {
		$options = self::get_settings( $for_module );

		if ( ! isset( $options[ $option_name ] ) ) {
			return '';
		}

		/**
		 * Failsafe for when options are stored incorrectly.
		 */
		$defaults = self::get_default_settings();
		if ( $for_module ) {
			$defaults = $defaults[ $for_module ];
		}

		if ( self::is_exception( $for_module, $options, $option_name ) ) {
			return $options[ $option_name ];
		}

		if ( gettype( $defaults[ $option_name ] ) !== gettype( $options[ $option_name ] ) ) {
			self::update_setting( $option_name, $defaults[ $option_name ], $for_module );
			return $defaults[ $option_name ];
		}

		return $options[ $option_name ];
	}

	/**
	 * Check if setting has an exception.
	 *
	 * In get_settings we compare the values to defaults (including value type).
	 * Two options can be bool/string: minify -> enabled and page_cache -> enabled.
	 *
	 * @since 1.8.1
	 *
	 * @param string $module       Module.
	 * @param array  $options      Options.
	 * @param string $option_name  Option name.
	 *
	 * @return bool
	 */
	private static function is_exception( $module, $options, $option_name ) {
		$exceptions = array(
			'minify'      => 'super-admins',
			'page_cache'  => 'blog-admins',
			'performance' => 'super-admins',
		);

		// Cache control in Settings can be an array or boolean.
		if ( 'settings' === $module && 'control' === $option_name && is_array( $options[ $option_name ] ) ) {
			return true;
		}

		if ( isset( $exceptions[ $module ] ) && $exceptions[ $module ] === $options[ $option_name ] ) {
			return true;
		}

		return false;
	}

	/**
	 * Update selected plugin setting.
	 *
	 * @param string      $option_name  Setting name.
	 * @param mixed       $value        Setting value.
	 * @param bool|string $for_module   Module to update settings for.
	 */
	public static function update_setting( $option_name, $value, $for_module = false ) {
		$options = self::get_settings( $for_module );

		$options[ $option_name ] = $value;

		self::update_settings( $options, $for_module );
	}

	/**
	 * Return a single WP Hummingbird option.
	 *
	 * @param string $option   Option.
	 * @param mixed  $default  Optional. Default value to return if the option does not exist.
	 *
	 * @return mixed
	 */
	public static function get( $option, $default = false ) {
		if ( ! is_main_site() ) {
			$value = get_option( $option, $default );
		} else {
			$value = get_site_option( $option, $default );
		}

		return $value;
	}

	/**
	 * Delete a single WP Hummingbird option.
	 *
	 * @param string $option  Option.
	 */
	public static function delete( $option ) {
		if ( ! is_main_site() ) {
			delete_option( $option );
		} else {
			delete_site_option( $option );
		}
	}

	/**
	 * Update option.
	 *
	 * @param string      $option   WP Hummingbird option name.
	 * @param mixed       $value    WP Hummingbird option value.
	 * @param string|bool $autoload Optional. Whether to load the option when WordPress starts up. For existing options,
	 *                              `$autoload` can only be updated using `update_option()` if `$value` is also changed.
	 *                              Accepts 'yes'|true to enable or 'no'|false to disable. For non-existent options,
	 *                              the default value is 'yes'. Default null.
	 */
	public static function update( $option, $value, $autoload = null ) {
		if ( ! is_main_site() ) {
			update_option( $option, $value, $autoload );
		} else {
			update_site_option( $option, $value );
		}
	}

	/**
	 * Get setting name based on safe mode and context.
	 *
	 * @return string The appropriate settings option name.
	 */
	public static function get_setting_name(): string {
		if ( class_exists( SafeMode::class ) && SafeMode::instance()->is_safemode_call() ) {
			return 'wphb_safe_mode_settings';
		}

		return 'wphb_settings';
	}
}
