<?php
/**
 * Customiser Base Class
 *
 * @package errigal
 * @since 1.1.0
 */

namespace Errigal\Customiser;

defined( 'ABSPATH' ) || exit;

use Errigal\Constant\{
	Errigal_Customiser_Hook as Customiser_Hook,
	Errigal_WP_Function as WP_Function,
	Errigal_WP_Hook as WP_Hook,
};
use Errigal\Customiser\Errigal_Custom_Option_Base as Custom_Option;
use Errigal\Shared\Errigal_Validator as Validator;
use WP_Customize_Control;
use WP_Customize_Manager;
use WP_Customize_Partial;
use WP_Customize_Section;
use WP_Customize_Selective_Refresh;
use WP_Customize_Setting;

/**
 * This class is in charge of providing shared customiser functions.
 *
 * @since 1.1.0
 */
abstract class Errigal_Customiser_Base {
	/**
	 * Sections to add.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	protected const SECTIONS_TO_ADD = 'sections_to_add';

	/**
	 * Sections to update.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	protected const SECTIONS_TO_UPDATE = 'sections_to_update';

	/**
	 * Sections to remove.
	 *
	 * @since 1.1.0
	 *
	 * @var string
	 */
	protected const SECTIONS_TO_REMOVE = 'sections_to_remove';

	/**
	 * Section: priority.
	 *
	 * @since 1.1.0
	 *
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_section/__construct/
	 *
	 * @var string
	 */
	protected const SECTION_PRIORITY = 'priority';

	/**
	 * Section: title.
	 *
	 * @since 1.1.0
	 *
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_section/__construct/
	 *
	 * @var string
	 */
	protected const SECTION_TITLE = 'title';

	/**
	 * Section: description.
	 *
	 * @since 1.1.0
	 *
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_section/__construct/
	 *
	 * @var string
	 */
	protected const SECTION_DESCRIPTION = 'description';

	/**
	 * Control: Base class (WP_Customize_Control).
	 *
	 * @since 1.2.0
	 *
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_control/
	 *
	 * @var string
	 */
	private const CONTROL_BASE_CLASS = 'WP_Customize_Control';

	/**
	 * Setting: sanitize_callback: sanitise_checkbox.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const SANITIZE_CALLBACK_SANITISE_CHECKBOX = 'sanitise_checkbox';

	/**
	 * Setting: sanitize_callback: sanitise_choices.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private const SANITIZE_CALLBACK_SANITISE_CHOICES = 'sanitise_choices';

	/**
	 * Theme customiser object.
	 *
	 * @since 1.2.0
	 *
	 * @var WP_Customize_Manager
	 */
	private $wp_customise;

	/**
	 * Constructor.
	 *
	 * @since 1.1.0
	 */
	public function __construct() {
		add_action( WP_Hook::CUSTOMIZE_REGISTER, array( $this, 'register' ) );
	}

	/**
	 * Gets sections args.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	abstract protected function get_sections_args();

	/**
	 * Gets options.
	 *
	 * @since 1.1.0
	 *
	 * @return array
	 */
	abstract protected function get_options();

	/**
	 * Registers customiser options.
	 *
	 * @since 1.1.0
	 *
	 * @param WP_Customize_Manager $wp_customise Theme customiser object.
	 * @return void
	 */
	public function register( $wp_customise ) {
		if ( ! $this->is_wp_customize_manager( $wp_customise ) ) {
			return;
		}

		$this->wp_customise = $wp_customise;

		$this->register_sections();
		$this->register_options();
	}

	/**
	 * Sanitize callback: Sanitises checkbox.
	 *
	 * @since 1.2.0
	 *
	 * @param bool $input Input value to be sanitised.
	 * @return bool
	 */
	public function sanitise_checkbox( $input ) {
		return Validator::is_true( $input );
	}

	/**
	 * Sanitize callback: Sanitises choices.
	 *
	 * @since 1.2.0
	 *
	 * @param string|int           $input   Input value to be sanitised.
	 * @param WP_Customize_Setting $setting Customiser setting object.
	 * @return string
	 */
	public function sanitise_choices( $input, $setting ) {
		if (
			! ( is_string( $input ) || is_int( $input ) ) ||
			! $this->is_wp_customize_setting( $setting )
		) {
			return '';
		}

		$choices = $setting->manager->get_control( $setting->id )->choices;

		if ( ! Validator::has_array_key( $choices, $input ) ) {
			return sanitize_key( $setting->default );
		}

		return sanitize_key( $input );
	}

	/**
	 * Determines whether $wp_customise is instance of WP_Customize_Manager.
	 *
	 * @since 1.2.0
	 *
	 * @param WP_Customize_Manager $wp_customise Theme customiser object.
	 * @return bool
	 */
	private function is_wp_customize_manager( $wp_customise ) {
		return $wp_customise instanceof WP_Customize_Manager;
	}

	/**
	 * Registers sections.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	private function register_sections() {
		$sections_args = $this->get_sections_args();

		if ( ! Validator::is_associative_array( $sections_args ) ) {
			return;
		}

		$sections_args = wp_parse_args(
			$sections_args,
			array(
				self::SECTIONS_TO_ADD    => array(),
				self::SECTIONS_TO_UPDATE => array(),
				self::SECTIONS_TO_REMOVE => array(),
			)
		);

		$this->add_sections( $sections_args[ self::SECTIONS_TO_ADD ] );
		$this->update_sections( $sections_args[ self::SECTIONS_TO_UPDATE ] );
		$this->remove_sections( $sections_args[ self::SECTIONS_TO_REMOVE ] );
	}

	/**
	 * Adds sections.
	 *
	 * @since 1.1.0
	 *
	 * @param array $sections_args Associative array of arguments for sections.
	 * @return void
	 */
	private function add_sections( $sections_args ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_associative_array( $sections_args )
		) {
			return;
		}

		foreach ( $sections_args as $id => $args ) {
			if (
				! Validator::is_non_empty_string( $id ) ||
				! Validator::is_associative_array( $args )
			) {
				continue;
			}

			$this->wp_customise->add_section( $id, $args );
		}
	}

	/**
	 * Updates sections.
	 *
	 * @since 1.1.0
	 *
	 * @param array $sections_args Associative array of arguments for sections to be updated.
	 * @return void
	 */
	private function update_sections( $sections_args ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_associative_array( $sections_args )
		) {
			return;
		}

		foreach ( $sections_args as $id => $args ) {
			if (
				! Validator::is_non_empty_string( $id ) ||
				! Validator::is_associative_array( $args )
			) {
				continue;
			}

			$section = $this->wp_customise->get_section( $id );

			if ( ! $this->is_wp_customize_section( $section ) ) {
				continue;
			}

			foreach ( $args as $property => $value ) {
				if ( ! property_exists( $section, $property ) ) {
					continue;
				}

				$section->{$property} = $value;
			}
		}
	}

	/**
	 * Determines whether $section is instance of WP_Customize_Section.
	 *
	 * @since 1.1.0
	 *
	 * @param WP_Customize_Section $section Customiser section object.
	 * @return bool
	 */
	private function is_wp_customize_section( $section ) {
		return $section instanceof WP_Customize_Section;
	}

	/**
	 * Removes sections.
	 *
	 * @since 1.1.0
	 *
	 * @param string[] $ids IDs for sections to be removed.
	 * @return void
	 */
	private function remove_sections( $ids ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_string_list( $ids )
		) {
			return;
		}

		foreach ( $ids as $id ) {
			if ( ! Validator::is_non_empty_string( $id ) ) {
				continue;
			}

			$this->wp_customise->remove_section( $id );
		}
	}

	/**
	 * Determines whether $setting is instance of WP_Customize_Setting.
	 *
	 * @since 1.2.0
	 *
	 * @param WP_Customize_Setting $setting Customiser setting object.
	 * @return bool
	 */
	private function is_wp_customize_setting( $setting ) {
		return $setting instanceof WP_Customize_Setting;
	}

	/**
	 * Registers options.
	 *
	 * @since 1.2.0
	 *
	 * @return void
	 */
	private function register_options() {
		$options = $this->get_options();

		if ( ! Validator::is_associative_array( $options ) ) {
			return;
		}

		foreach ( $options as $section => $custom_options ) {
			if (
				! is_string( $section ) ||
				! Validator::is_non_empty_array( $custom_options )
			) {
				continue;
			}

			foreach ( $custom_options as $custom_option ) {
				if ( ! $this->is_custom_option( $custom_option ) ) {
					continue;
				}

				$controls_args = $custom_option->get_controls_args();

				if ( ! Validator::is_associative_array( $controls_args ) ) {
					continue;
				}

				$controls_args = wp_parse_args(
					$controls_args,
					array(
						Custom_Option::CONTROLS_TO_ADD    => array(),
						Custom_Option::CONTROLS_TO_UPDATE => array(),
						Custom_Option::CONTROLS_TO_REMOVE => array(),
					)
				);

				$controls_args = $this->add_section_to_controls_args(
					$section,
					$controls_args,
				);

				$this->add_controls( $controls_args[ Custom_Option::CONTROLS_TO_ADD ] );
				$this->update_controls( $controls_args[ Custom_Option::CONTROLS_TO_UPDATE ] );
				$this->remove_controls( $controls_args[ Custom_Option::CONTROLS_TO_REMOVE ] );
			}
		}
	}

	/**
	 * Determines whether $custom_option is instance of subclass of Custom_Option class
	 *
	 * @since 1.2.0
	 *
	 * @param Custom_Option $custom_option Instance of subclass of Custom_Option class.
	 * @return bool
	 */
	private function is_custom_option( $custom_option ) {
		return is_object( $custom_option ) &&
			$custom_option instanceof Custom_Option &&
			Custom_Option::class !== get_class( $custom_option ) &&
			method_exists( $custom_option, 'get_controls_args' );
	}

	/**
	 * Adds section to controls args.
	 *
	 * @since 1.2.0
	 *
	 * @param string $section       Section.
	 * @param array  $controls_args Associative array of arguments for controls.
	 * @return array
	 */
	private function add_section_to_controls_args( $section, $controls_args ) {
		if (
			! Validator::is_non_empty_string( $section ) ||
			! Validator::is_associative_array( $controls_args )
		) {
				return $controls_args;
		}

		$controls_keys = array(
			Custom_Option::CONTROLS_TO_ADD,
			Custom_Option::CONTROLS_TO_UPDATE,
		);

		foreach ( $controls_keys as $controls_key ) {
			if (
				! Validator::is_non_empty_string( $controls_key ) ||
				! isset( $controls_args[ $controls_key ] ) ||
				! Validator::is_associative_array( $controls_args[ $controls_key ] )
			) {
				continue;
			}

			foreach ( $controls_args[ $controls_key ] as $id => $customiser_args ) {
				if (
					! isset( $customiser_args[ Custom_Option::CUSTOMISE_ARGS_CONTROL ] ) ||
					! Validator::is_associative_array( $customiser_args[ Custom_Option::CUSTOMISE_ARGS_CONTROL ] )
				) {
						continue;
				}

				if ( ! isset( $controls_args[ $controls_key ][ $id ][ Custom_Option::CUSTOMISE_ARGS_CONTROL ][ Custom_Option::CONTROL_SECTION ] ) ) {
					$controls_args[ $controls_key ][ $id ][ Custom_Option::CUSTOMISE_ARGS_CONTROL ][ Custom_Option::CONTROL_SECTION ] = sanitize_key( $section );
				}
			}
		}

		return $controls_args;
	}

	/**
	 * Adds controls.
	 *
	 * @since 1.2.0
	 *
	 * @param array $controls_args Associative array of arguments for controls.
	 * @return void
	 */
	private function add_controls( $controls_args ) {
		if ( ! Validator::is_associative_array( $controls_args ) ) {
			return;
		}

		foreach ( $controls_args as $id => $args ) {
			if (
				! Validator::is_non_empty_string( $id ) ||
				! Validator::is_associative_array( $args )
			) {
				continue;
			}

			$this->add_control( $id, $args );
		}
	}

	/**
	 * Adds customise control.
	 *
	 * @since 1.2.0
	 *
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_setting/__construct/
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_control/__construct/
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_partial/__construct/
	 *
	 * @param string $id Customise ID.
	 * @param array  $args {
	 *     Associative array of arguments for control.
	 *
	 *     @type array $setting Optional. Associative array of arguments for setting object.
	 *                          See WP_Customize_Setting::__construct() for information
	 *                          on accepted arguments. Default empty array.
	 *     @type array $control Associative array of arguments for control object.
	 *                          See WP_Customize_Control::__construct() for information
	 *                          on accepted arguments. Default empty array.
	 *     @type array $partial Optional. Associative array of arguments for partials object.
	 *                          See WP_Customize_Partial::__construct() for information
	 *                          on accepted arguments. Default empty array.
	 * }
	 * @return void
	 */
	private function add_control( $id, $args ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_non_empty_string( $id ) ||
			! Validator::is_associative_array( $args )
		) {
			return;
		}

		$args = wp_parse_args(
			$args,
			array(
				Custom_Option::CUSTOMISE_ARGS_SETTING => array(),
				Custom_Option::CUSTOMISE_ARGS_CONTROL => array(),
				Custom_Option::CUSTOMISE_ARGS_PARTIAL => array(),
			)
		);

		$control_args = wp_parse_args(
			$args[ Custom_Option::CUSTOMISE_ARGS_CONTROL ],
			array(
				Custom_Option::CONTROL_TYPE    => '',
				Custom_Option::CONTROL_SECTION => '',
			)
		);

		if ( ! Validator::is_non_empty_string( $control_args[ Custom_Option::CONTROL_SECTION ] ) ) {
			return;
		}

		$control_type = $control_args[ Custom_Option::CONTROL_TYPE ];

		if ( ! $this->is_valid_control_type( $control_type ) ) {
			return;
		}

		$setting_args = wp_parse_args(
			$args[ Custom_Option::CUSTOMISE_ARGS_SETTING ],
			array(
				Custom_Option::SETTING_SANITIZE_CALLBACK => $this->get_sanitize_callback( $control_type ),
			)
		);

		if ( ! is_callable( $setting_args[ Custom_Option::SETTING_SANITIZE_CALLBACK ] ) ) {
			return;
		}

		$this->wp_customise->add_setting(
			$id,
			$setting_args
		);

		if ( $this->is_extended_control( $control_type ) ) {
			unset( $control_args[ Custom_Option::CONTROL_TYPE ] );
			$this->wp_customise->add_control(
				new $control_type(
					$this->wp_customise,
					$id,
					$control_args
				)
			);
		} else {
			$this->wp_customise->add_control(
				$id,
				$control_args
			);
		}

		$partial_args = $args[ Custom_Option::CUSTOMISE_ARGS_PARTIAL ];

		if ( ! Validator::is_associative_array( $partial_args ) ) {
			return;
		}

		$this->wp_customise->selective_refresh->add_partial( $id, $partial_args );
	}

	/**
	 * Determines whether control type is valid.
	 *
	 * @since 1.2.0
	 *
	 * @param string $control_type Control type.
	 * @return bool
	 */
	private function is_valid_control_type( $control_type ) {
		return Validator::is_non_empty_string( $control_type ) &&
			( $this->is_wp_control( $control_type ) || $this->is_extended_control( $control_type ) );
	}

	/**
	 * Determines whether control is supported in WordPress.
	 *
	 * @since 1.2.0
	 *
	 * @param string $control_type Control type.
	 * @return bool
	 */
	private function is_wp_control( $control_type ) {
		return Validator::has_string_list_item(
			$this->get_supported_wp_control_types(),
			$control_type
		);
	}

	/**
	 * Gets supported WordPress customiser control types.
	 *
	 * @since 1.2.0
	 *
	 * @return string[]
	 */
	private function get_supported_wp_control_types() {
		/**
		 * Filters supported WP control types.
		 *
		 * @since 1.2.0
		 *
		 * @param string[] $supported_wp_control_types Supported WP control types.
		 */
		$supported_wp_control_types = apply_filters(
			Customiser_Hook::SUPPORTED_WP_CONTROL_TYPES,
			$this->get_default_supported_wp_control_types()
		);

		if ( ! Validator::is_string_list( $supported_wp_control_types ) ) {
			return $this->get_default_supported_wp_control_types();
		}

		return $supported_wp_control_types;
	}

	/**
	 * Gets default supported WordPress customiser control types.
	 *
	 * @since 1.2.0
	 *
	 * @return string[]
	 */
	private function get_default_supported_wp_control_types() {
		return array(
			Custom_Option::CONTROL_TYPE_CHECKBOX,
			Custom_Option::CONTROL_TYPE_TEXTAREA,
			Custom_Option::CONTROL_TYPE_RADIO,
			Custom_Option::CONTROL_TYPE_SELECT,
		);
	}

	/**
	 * Gets sanitize callback for control.
	 *
	 * @since 1.2.0
	 *
	 * @param string $control_type Control type.
	 * @return callable|string
	 */
	private function get_sanitize_callback( $control_type ) {
		if ( ! Validator::is_non_empty_string( $control_type ) ) {
			return '';
		}

		$sanitize_callbacks = $this->get_sanitize_callbacks();

		if (
			! Validator::has_array_key( $sanitize_callbacks, $control_type ) ||
			! is_callable( $sanitize_callbacks[ $control_type ] )
		) {
			return '';
		}

		return $sanitize_callbacks[ $control_type ];
	}

	/**
	 * Gets sanitize callbacks.
	 *
	 * @since 1.2.0
	 *
	 * @return array
	 */
	private function get_sanitize_callbacks() {
		/**
		 * Filters sanitize callbacks.
		 *
		 * @since 1.2.0
		 *
		 * @param array $sanitize_callbacks Associative array of sanitize callbacks.
		 */
		$sanitize_callbacks = apply_filters(
			Customiser_Hook::SANITIZE_CALLBACKS,
			$this->get_default_sanitize_callbacks()
		);

		if ( ! Validator::is_associative_array( $sanitize_callbacks ) ) {
			return $this->get_default_sanitize_callbacks();
		}

		return $sanitize_callbacks;
	}

	/**
	 * Gets default sanitize callbacks.
	 *
	 * @since 1.2.0
	 *
	 * @return array
	 */
	private function get_default_sanitize_callbacks() {
		return array(
			Custom_Option::CONTROL_TYPE_CHECKBOX     => array( $this, self::SANITIZE_CALLBACK_SANITISE_CHECKBOX ),
			Custom_Option::CONTROL_TYPE_TEXTAREA     => WP_Function::WP_FILTER_NOHTML_KSES,
			Custom_Option::CONTROL_TYPE_RADIO        => array( $this, self::SANITIZE_CALLBACK_SANITISE_CHOICES ),
			Custom_Option::CONTROL_TYPE_SELECT       => array( $this, self::SANITIZE_CALLBACK_SANITISE_CHOICES ),
			Custom_Option::CONTROL_TYPE_LABEL        => WP_Function::SANITIZE_TEXT_FIELD,
			Custom_Option::CONTROL_TYPE_PRO_FEATURES => WP_Function::WP_FILTER_NOHTML_KSES,
		);
	}

	/**
	 * Determines whether control is extended control.
	 *
	 * @since 1.2.0
	 *
	 * @param string $control_type Control type.
	 * @return bool
	 */
	private function is_extended_control( $control_type ) {
		return Validator::is_non_empty_string( $control_type ) &&
			class_exists( $control_type ) &&
			is_subclass_of( $control_type, self::CONTROL_BASE_CLASS );
	}

	/**
	 * Updates controls.
	 *
	 * @since 1.2.0
	 *
	 * @param array $controls_args Associative array of arguments for controls to be updated.
	 * @return void
	 */
	private function update_controls( $controls_args ) {
		if ( ! Validator::is_associative_array( $controls_args ) ) {
			return;
		}

		foreach ( $controls_args as $id => $args ) {
			if (
				! Validator::is_non_empty_string( $id ) ||
				! Validator::is_associative_array( $args )
			) {
				continue;
			}

			$this->update_control( $id, $args );
		}
	}

	/**
	 * Updates customise control.
	 *
	 * @since 1.2.0
	 *
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_setting/__construct/
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_control/__construct/
	 * @link https://developer.wordpress.org/reference/classes/wp_customize_partial/__construct/
	 *
	 * @param string $id Customise ID.
	 * @param array  $args {
	 *     Associative array of arguments for control to be updated.
	 *
	 *     @type array $setting Optional. Associative array of arguments for setting object.
	 *                          See WP_Customize_Setting::__construct() for information
	 *                          on accepted arguments. Default empty array.
	 *     @type array $control Optional. Associative array of arguments for control object.
	 *                          See WP_Customize_Control::__construct() for information
	 *                          on accepted arguments. Default empty array.
	 *     @type array $partial Optional. Associative array of arguments for partials object.
	 *                          See WP_Customize_Partial::__construct() for information
	 *                          on accepted arguments. Default empty array.
	 * }
	 * @return void
	 */
	private function update_control( $id, $args ) {
		if (
			! Validator::is_non_empty_string( $id ) ||
			! Validator::is_associative_array( $args )
		) {
			return;
		}

		$args = wp_parse_args(
			$args,
			array(
				Custom_Option::CUSTOMISE_ARGS_SETTING => array(),
				Custom_Option::CUSTOMISE_ARGS_CONTROL => array(),
				Custom_Option::CUSTOMISE_ARGS_PARTIAL => array(),
			)
		);

		$this->update_customise_setting( $id, $args[ Custom_Option::CUSTOMISE_ARGS_SETTING ] );
		$this->update_customise_control( $id, $args[ Custom_Option::CUSTOMISE_ARGS_CONTROL ] );
		$this->update_customise_partial( $id, $args[ Custom_Option::CUSTOMISE_ARGS_PARTIAL ] );
	}

	/**
	 * Updates customise setting.
	 *
	 * @since 1.2.0
	 *
	 * @param string $id   Customise ID.
	 * @param array  $args Associative array of arguments for setting object.
	 *                     See WP_Customize_Setting::__construct() for information
	 *                     on accepted arguments. Default empty array.
	 * @return void
	 */
	private function update_customise_setting( $id, $args ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_non_empty_string( $id ) ||
			! Validator::is_associative_array( $args )
		) {
			return;
		}

		$setting = $this->wp_customise->get_setting( $id );

		if ( ! $this->is_wp_customize_setting( $setting ) ) {
			return;
		}

		foreach ( $args as $property => $value ) {
			if ( ! property_exists( $setting, $property ) ) {
				continue;
			}

			$setting->{$property} = $value;
		}
	}

	/**
	 * Updates customise control.
	 *
	 * @since 1.2.0
	 *
	 * @param string $id   Customise ID.
	 * @param array  $args Associative array of arguments for control object.
	 *                     See WP_Customize_Control::__construct() for information
	 *                     on accepted arguments. Default empty array.
	 * @return void
	 */
	private function update_customise_control( $id, $args ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_non_empty_string( $id ) ||
			! Validator::is_associative_array( $args )
		) {
			return;
		}

		$control = $this->wp_customise->get_control( $id );

		if ( ! $this->is_wp_customize_control( $control ) ) {
			return;
		}

		foreach ( $args as $property => $value ) {
			if ( ! property_exists( $control, $property ) ) {
				continue;
			}

			$control->{$property} = $value;
		}
	}

	/**
	 * Determines whether $control is instance of WP_Customize_Control.
	 *
	 * @since 1.2.0
	 *
	 * @param WP_Customize_Control $control Customiser control object.
	 * @return bool
	 */
	private function is_wp_customize_control( $control ) {
		return $control instanceof WP_Customize_Control;
	}

	/**
	 * Updates customise partial.
	 *
	 * @since 1.2.0
	 *
	 * @param string $id   Customise ID.
	 * @param array  $args Associative array of arguments for partials object.
	 *                     See WP_Customize_Partial::__construct() for information
	 *                     on accepted arguments. Default empty array.
	 * @return void
	 */
	private function update_customise_partial( $id, $args ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_non_empty_string( $id ) ||
			! Validator::is_associative_array( $args )
		) {
			return;
		}

		$selective_refresh = $this->wp_customise->selective_refresh;

		if ( ! $this->is_wp_customize_selective_refresh( $selective_refresh ) ) {
			return;
		}

		$partial = $selective_refresh->get_partial( $id );

		if ( ! $this->is_wp_customize_partial( $partial ) ) {
			$selective_refresh->add_partial(
				$id,
				$args
			);

			return;
		}

		foreach ( $args as $property => $value ) {
			if ( ! property_exists( $partial, $property ) ) {
				continue;
			}

			$partial->{$property} = $value;
		}
	}

	/**
	 * Determines whether $selective_refresh is instance of WP_Customize_Selective_Refresh.
	 *
	 * @since 1.2.0
	 *
	 * @param WP_Customize_Selective_Refresh $selective_refresh Customiser selective refresh object.
	 * @return bool
	 */
	private function is_wp_customize_selective_refresh( $selective_refresh ) {
		return $selective_refresh instanceof WP_Customize_Selective_Refresh;
	}

	/**
	 * Determines whether $partial is instance of WP_Customize_Partial.
	 *
	 * @since 1.2.0
	 *
	 * @param WP_Customize_Partial $partial Customiser partial object.
	 * @return bool
	 */
	private function is_wp_customize_partial( $partial ) {
		return $partial instanceof WP_Customize_Partial;
	}

	/**
	 * Removes controls.
	 *
	 * @since 1.2.0
	 *
	 * @param string[] $ids IDs for controls to be removed.
	 * @return void
	 */
	private function remove_controls( $ids ) {
		if (
			! $this->is_wp_customize_manager( $this->wp_customise ) ||
			! Validator::is_string_list( $ids )
		) {
			return;
		}

		foreach ( $ids as $id ) {
			if ( ! Validator::is_non_empty_string( $id ) ) {
				continue;
			}

			$this->wp_customise->remove_control( $id );
		}
	}
}
