<?php
/**
 * Handles iframe embeds for privacy.
 *
 * @author     ThemeFusion
 * @copyright  (c) Copyright by ThemeFusion
 * @link       https://avada.com
 * @package    Avada
 * @subpackage Core
 * @since      5.5.2
 */

// Do not allow directly accessing this file.
if ( ! defined( 'ABSPATH' ) ) {
	exit( 'Direct script access denied.' );
}

/**
 * Handle iframe embeds.
 */
class Avada_Privacy_Embeds {

	/**
	 * An array of options to control privacy embeds.
	 *
	 * @since 5.7
	 * @access private
	 * @var array
	 */
	private $options = [];

	/**
	 * An array of embed types.
	 *
	 * @access public
	 * @var array
	 */
	public $embed_types = [];

	/**
	 * Default embed types.
	 *
	 * @access public
	 * @var array
	 */
	public $embed_defaults = [];

	/**
	 * Cached map of snippet array index => stable consent key.
	 *
	 * @access protected
	 * @var array|null
	 */
	protected $snippet_keys = null;

	/**
	 * An array of consents.
	 *
	 * @access public
	 * @var array
	 */
	public $consents = [];

	/**
	 * An array of default consents.
	 *
	 * @access public
	 * @var array
	 */
	public $default_consents = [];

	/**
	 * Check if consent for all is given.
	 *
	 * @access public
	 * @var bool
	 */
	public $all_consents = false;

	/**
	 * Cookie name.
	 *
	 * @access public
	 * @var array
	 */
	private $cookie_args = [];

	/**
	 * The class constructor.
	 *
	 * @access public
	 */
	public function __construct() {
		$this->set_cookie_args();
		$this->set_embed_types();
		$this->set_consents();

		add_action( 'after_setup_theme', [ $this, 'set_embed_types_names'] );

		add_action( 'init', [ $this, 'init' ] );

	}

	/**
	 * Init.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function init() {

		$this->set_options();
		$this->set_cookie_expiry();
		$this->update_embed_types();
		$this->set_default_consents();

		// Re-run set_options() after the live editor updates Fusion_Settings with preview values.
		add_action( 'fusion_preview_update', [ $this, 'set_options' ], 11 );

		// Only run filter if privacy TO is enabled and we do not already have all consents.
		if ( $this->options['privacy_embeds'] && ! $this->all_consents ) {
			add_filter( 'do_shortcode_tag', [ $this, 'shortcode_replace' ], 20, 4 );
			add_filter( 'the_content', [ $this, 'replace' ], 99999 );
			add_filter( 'privacy_iframe_embed', [ $this, 'replace' ], 20 );
			add_filter( 'privacy_script_embed', [ $this, 'script_block' ], 20, 5 );
			add_filter( 'privacy_image_embed', [ $this, 'image_block' ], 20, 5 );
			add_filter( 'fusion_attr_google-map-shortcode', [ $this, 'hide_google_map' ] );
			add_filter( 'fusion_attr_avada-google-map', [ $this, 'hide_google_map' ] );

			if ( ! is_admin() ) {
				add_filter( 'script_loader_tag', [ $this, 'replace_script_loader_tag' ], 20, 3 );
				add_filter( 'wp_video_shortcode', [ $this, 'video_widget' ], 20, 5 );
			}
		}

		if ( $this->options['privacy_embeds'] ) {
			add_filter( 'avada_dynamic_css_array', [ $this, 'add_styling' ] );
		}

		if ( apply_filters( 'fusion_privacy_bar', '0' !== $this->options['privacy_bar'] ) ) {
			add_filter( 'avada_dynamic_css_array', [ $this, 'add_bar_styling' ] );
			add_action( 'wp_footer', [ $this, 'display_privacy_bar' ], 10 );
			add_action( 'wp_enqueue_scripts', [ $this, 'register_bar_dependencies' ] );
		}

		if ( $this->options['privacy_embeds'] ) {
			add_action( 'wp_head', [ $this, 'output_google_consent_default' ], 1 );
		}

		if ( ! empty( $this->options['privacy_custom_snippets'] ) ) {
			add_action( 'wp_head', [ $this, 'output_custom_snippets' ], 10000 );
			add_action( 'wp_body_open', [ $this, 'output_custom_snippets' ], 20 );
			add_action( 'wp_footer', [ $this, 'output_custom_snippets' ], 10000 );
		}
	}

	/**
	 * Filter video widget for youtube and vimeo videos.
	 *
	 * @access  public
	 * @since   6.0.3
	 * @param   string $output String output.
	 * @param   array  $atts Instance attributes.
	 * @param   string $video The video file.
	 * @param   int    $post_id Post ID.
	 * @param   string $library Media library used for the video shortcode.
	 * @return  string $output
	 */
	public function video_widget( $output, $atts, $video, $post_id, $library ) {
		$consents = [ 'youtube', 'vimeo' ];
		if ( isset( $atts['src'] ) ) {
			foreach ( $consents as $consent ) {
				if ( ! $this->search( $consent, $atts['src'] ) ) {
					continue;
				}
				if ( $this->get_consent( $consent ) ) {
					return $output;
				}

				$output  = '<noscript class="fusion-hidden" data-privacy-video="true" data-privacy-type="' . $consent . '">' . $output . '</noscript>';
				$output .= $this->script_placeholder( $consent, false, false );
				return $output;
			}
		}
		return $output;
	}

	/**
	 * Normalise a fusionredux repeater value from columnar format to row-based format.
	 * Rows with an empty label are skipped (repeater placeholder entries).
	 *
	 * @access public
	 * @since  7.15.3
	 * @param  mixed $data Raw value from Avada()->settings->get().
	 * @return array       Indexed array of associative row arrays.
	 */
	public static function normalize_repeater_data( $data ) {
		if ( ! is_array( $data ) || ! isset( $data['label'] ) || ! is_array( $data['label'] ) ) {
			return [];
		}
		$rows  = [];
		$count = count( $data['label'] );
		for ( $i = 0; $i < $count; $i++ ) {
			if ( empty( $data['label'][ $i ] ) ) {
				continue;
			}
			$row = [];
			foreach ( $data as $field => $values ) {
				if ( 'fusionredux_repeater_data' === $field || ! is_array( $values ) ) {
					continue;
				}
				$row[ $field ] = isset( $values[ $i ] ) ? $values[ $i ] : '';
			}
			$rows[] = $row;
		}
		return $rows;
	}

	/**
	 * Returns the custom code snippets as a normalised row-based array.
	 *
	 * @access public
	 * @since  7.15.3
	 * @return array
	 */
	public function get_custom_snippets() {
		return self::normalize_repeater_data( Avada()->settings->get( 'custom_code_snippets' ) );
	}

	/**
	 * Gets the options for privacy embeds.
	 *
	 * @access public
	 * @since  5.7
	 * @return array
	 */
	public function get_options() {
		return $this->options;
	}

	/**
	 * Sets the options for privacy embeds.
	 *
	 * @access  public
	 * @since   5.7
	 * @return  void
	 */
	public function set_options() {
		$privacy_bar = Avada()->settings->get( 'privacy_bar' );

		// Check for app full refresh data.
		if ( function_exists( 'fusion_is_preview_frame' ) && fusion_is_preview_frame() && ! empty( Fusion_App()->get_data( 'fusion_options' ) ) ) {
			$privacy_bar = Fusion_App()->get_data( 'fusion_options' )['privacy_bar'];
		}
		$this->options = apply_filters(
			'avada_privacy_options',
			[
				'privacy_embeds'                 => Avada()->settings->get( 'privacy_embeds' ),
				'privacy_bar'                    => $privacy_bar,
				'privacy_expiry'                 => Avada()->settings->get( 'privacy_expiry' ),
				'privacy_embed_types'            => Avada()->settings->get( 'privacy_embed_types' ),
				'privacy_embed_defaults'         => Avada()->settings->get( 'privacy_embed_defaults' ),
				'privacy_bar_content'            => Avada()->settings->get( 'privacy_bar_content' ),
				'privacy_bg_color'               => 'var(--privacy_bg_color)',
				'privacy_color'                  => 'var(--privacy_color)',
				'privacy_bar_bg_color'           => 'var(--privacy_bar_bg_color)',
				'privacy_bar_color'              => 'var(--privacy_bar_color)',
				'privacy_bar_link_color'         => 'var(--privacy_bar_link_color)',
				'privacy_bar_link_hover_color'   => 'var(--privacy_bar_link_hover_color)',
				'privacy_bar_button_save'        => Avada()->settings->get( 'privacy_bar_button_save' ),
				'privacy_bar_text'               => Avada()->settings->get( 'privacy_bar_text' ),
				'privacy_bar_button_text'        => Avada()->settings->get( 'privacy_bar_button_text' ),
				'privacy_bar_more_text'          => Avada()->settings->get( 'privacy_bar_more_text' ),
				'privacy_bar_headings_color'     => 'var(--privacy_bar_headings_color)',
				'privacy_bar_font_size'          => 'var(--privacy_bar_font_size)',
				'privacy_bar_headings_font_size' => 'var(--privacy_bar_headings_font_size)',
				'privacy_custom_snippets'        => self::normalize_repeater_data( Avada()->settings->get( 'custom_code_snippets' ) ),
			]
		);
	}

	/**
	 * Sets the args for the cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function set_cookie_args() {

		// Filterable time for expiration.
		$default_args = [
			'name'     => 'privacy_embeds',
			'days'     => '30',
			'path'     => '/',
			'domain'   => '',
			'secure'   => is_ssl(),
			'samesite' => 'Lax',
		];

		$this->cookie_args = apply_filters( 'fusion_privacy_cookie_args', $default_args );
	}

	/**
	 * Sets the expiry for the cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function set_cookie_expiry() {
		$this->cookie_args['days'] = $this->options['privacy_expiry'];
	}

	/**
	 * Gets the args for the cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  array
	 */
	public function get_cookie_args() {
		return $this->cookie_args;
	}

	/**
	 * Sets array of embed types.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function set_embed_types() {

		$this->embed_types    = apply_filters(
			'fusion_privacy_embeds',
			[
				'youtube'    => [
					'search'   => 'youtube.com',
					'label'    => '',
					'category' => 'embeds',
				],
				'vimeo'      => [
					'search'   => 'vimeo.com',
					'label'    => '',
					'category' => 'embeds',
				],
				'soundcloud' => [
					'search'   => 'soundcloud.com',
					'label'    => '',
					'category' => 'embeds',
				],
				'facebook'   => [
					'search'   => 'facebook.com',
					'label'    => '',
					'category' => 'embeds',
				],
				'flickr'     => [
					'search'   => 'flickr.com',
					'label'    => '',
					'category' => 'embeds',
				],
				'twitter'    => [
					'search'   => 'twitter.com',
					'label'    => '',
					'category' => 'embeds',
				],
				'gmaps'      => [
					'search'   => [
						'maps.googleapis.com',
						'infobox_packed',
						'google.com/maps/embed',
					],
					'label'    => '',
					'category' => 'embeds',
				],
				'instagram'  => [
					'search'   => [
						'cdninstagram.com',
						'instagram.',
						'fbcdn.net',
					],
					'label'    => '',
					'category' => 'embeds',
				],
				'google_analytics_storage' => [
					'search'   => [],
					'label'    => '',
					'category' => 'statistics',
				],
				'google_ad_storage' => [
					'search'   => [],
					'label'    => '',
					'category' => 'tracking',
				],
				'google_ad_user_data' => [
					'search'   => [],
					'label'    => '',
					'category' => 'tracking',
				],
				'google_ad_personalization' => [
					'search'   => [],
					'label'    => '',
					'category' => 'tracking',
				],
			]
		);
		$this->embed_defaults = $this->embed_types;
	}

	/**
	 * Sets the labels of embed types. Needs to be done on after_setup_theme to avoid text domain PHP notice.
	 *
	 * @access  public
	 * @since   7.11.15
	 * @return  void
	 */	
	public function set_embed_types_names() {
		$types_and_names = [
			'youtube'                   => esc_html__( 'YouTube', 'Avada' ),
			'vimeo'                     => esc_html__( 'Vimeo', 'Avada' ),
			'soundcloud'                => esc_html__( 'SoundCloud', 'Avada' ),
			'facebook'                  => esc_html__( 'Facebook', 'Avada' ),
			'flickr'                    => esc_html__( 'Flickr', 'Avada' ),
			'twitter'                   => esc_html__( 'X', 'Avada' ),
			'gmaps'                     => esc_html__( 'Google Maps', 'Avada' ),
			'instagram'                 => esc_html__( 'Instagram', 'Avada' ),
			'google_analytics_storage'  => esc_html__( 'Google Analytics Storage', 'Avada' ),
			'google_ad_storage'         => esc_html__( 'Google Ad Storage', 'Avada' ),
			'google_ad_user_data'       => esc_html__( 'Google Ad User Data', 'Avada' ),
			'google_ad_personalization' => esc_html__( 'Google Ad Personalization', 'Avada' ),
		];

		foreach( $this->embed_types as $key => $data ) {
			if ( isset( $types_and_names[ $key ] ) ) {
				$this->embed_types[ $key ]['label'] = $types_and_names[ $key ];
			}
		}

		$snippets = self::normalize_repeater_data( Avada()->settings->get( 'custom_code_snippets' ) );
		if ( ! empty( $snippets ) ) {
			foreach ( $snippets as $index => $snippet ) {
				$cat = isset( $snippet['category'] ) ? $snippet['category'] : '';
				if ( 'necessary' === $cat || 'no_group' === $cat ) {
					continue;
				}
				$key = $this->get_snippet_key( $index );
				$this->embed_types[ $key ] = [
					'search'              => [],
					'label'               => $snippet['label'],
					'category'            => $cat,
					'snippet_description' => isset( $snippet['snippet_description'] ) ? $snippet['snippet_description'] : '',
					'lifespan'            => isset( $snippet['lifespan'] ) ? $snippet['lifespan'] : '',
				];
			}
		}

		$this->embed_types    = apply_filters( 'fusion_privacy_embeds', $this->embed_types );
		$this->embed_defaults = $this->embed_types;
	}

	/**
	 * Get embed type.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $name Name of embed type.
	 * @param   string $subkey Name of embed type sub key.
	 * @return  array
	 */
	public function get_embed_type( $name = '', $subkey = false ) {

		$key = esc_attr( strtolower( $name ) );

		if ( ! $subkey && isset( $this->embed_types[ $key ] ) ) {
			return $this->embed_types[ $key ];
		} elseif ( $subkey && isset( $this->embed_types[ $key ] ) && isset( $this->embed_types[ $key ][ $subkey ] ) ) {
			return $this->embed_types[ $key ][ $subkey ];
		}

		return false;
	}

	/**
	 * Get embed default types.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   boolean $simple If you need simplified version.
	 * @return  array
	 */
	public function get_embed_defaults( $simple = false ) {
		if ( $simple && is_array( $this->embed_defaults ) ) {
			$simplified = [];
			foreach ( $this->embed_defaults as $key => $embed ) {
				if ( 0 === strpos( $key, 'awb_snip_' ) ) {
					continue;
				}
				$simplified[ $key ] = $embed['label'];
			}
			return $simplified;
		}
		return $this->embed_defaults;
	}

	/**
	 * Get embed types.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  array
	 */
	public function get_embed_types() {
		return $this->embed_types;
	}

	/**
	 * Returns the resolved description for a single embed type.
	 *
	 * Priority:
	 *  1. Individual option  privacy_{id}_description  (Google types use this)
	 *  2. snippet_description on the embed data        (custom snippets)
	 *  3. Shared privacy_embed_description option      (classic embed types)
	 *
	 * @access public
	 * @param  string $id    Embed type key.
	 * @param  array  $embed Embed type data array.
	 * @return string
	 */
	public function get_embed_description( $id, $embed ) {
		$desc = Avada()->settings->get( 'privacy_' . $id . '_description' );
		if ( ! empty( $desc ) ) {
			return $desc;
		}
		if ( ! empty( $embed['snippet_description'] ) ) {
			return $embed['snippet_description'];
		}
		$category = isset( $embed['category'] ) ? $embed['category'] : 'embeds';
		if ( 'embeds' === $category ) {
			return (string) Avada()->settings->get( 'privacy_embed_description' );
		}
		return '';
	}

	/**
	 * Updates embed types.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function update_embed_types() {
		$defaults = $this->get_embed_defaults();
		$selected = $this->options['privacy_embed_types'];
		$update   = [];

		if ( is_array( $selected ) ) {
			foreach ( $selected as $embed ) {
				if ( isset( $defaults[ $embed ] ) ) {
					$update[ $embed ] = $defaults[ $embed ];
				}
			}
		}

		// Snippet keys are never in privacy_embed_types but must stay in embed_types
		// so that get_consent() returns false (not auto-granted) until the user consents.
		foreach ( $defaults as $key => $embed ) {
			if ( 0 === strpos( $key, 'awb_snip_' ) ) {
				$update[ $key ] = $embed;
			}
		}

		$this->embed_types = $update;
	}

	/**
	 * Set default consents.
	 *
	 * @access  public
	 * @since   5.6
	 * @return  void
	 */
	public function set_default_consents() {
		$this->default_consents = $this->options['privacy_embed_defaults'];

		foreach ( $this->options['privacy_custom_snippets'] as $index => $snippet ) {
			if ( empty( $snippet['prechecked'] ) || '1' !== $snippet['prechecked'] || 'necessary' === $snippet['category'] ) {
				continue;
			}
			$key = $this->get_snippet_key( $index );
			if ( ! in_array( $key, $this->default_consents, true ) ) {
				$this->default_consents[] = $key;
			}
		}
	}

	/**
	 * Set consents from cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   array $consents Consents which you want to save.
	 * @return  void
	 */
	public function set_consents( $consents = false ) {
		$cookie_name = $this->cookie_args['name'];

		if ( ! $consents ) {
			$consents = [];
			if ( isset( $_COOKIE ) && isset( $_COOKIE[ $cookie_name ] ) ) {
				$consents = wp_unslash( $_COOKIE[ $cookie_name ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			}
		}

		if ( ! is_array( $consents ) ) {
			$consents = explode( ',', $consents );
		}

		$this->consents = $consents;

		$this->set_all_consents();
	}

	/**
	 * Checks if embed type should be selected.
	 *
	 * @access  public
	 * @since   5.6
	 * @param   string $type Name of embed type.
	 * @return  boolean
	 */
	public function is_selected( $type ) {
		$consents = $this->get_consents();
		$defaults = $this->get_default_consents();

		// If consent has been given.
		if ( in_array( $type, $consents ) ) { // phpcs:ignore WordPress.PHP.StrictInArray
			return true;
		}

		// No consent but is within default selection.
		if ( empty( $consents ) && in_array( $type, $defaults ) ) { // phpcs:ignore WordPress.PHP.StrictInArray
			return true;
		}

		return false;
	}

	/**
	 * Get privacy bar content.
	 *
	 * @access  public
	 * @since   5.6
	 * @return  array
	 */
	public function get_privacy_content() {
		$content   = isset( $this->options['privacy_bar_content'] ) ? $this->options['privacy_bar_content'] : Avada()->settings->get( 'privacy_bar_content' );
		$formatted = [];

		if ( isset( $content['title'] ) && is_array( $content['title'] ) ) {
			$custom_index = 0;
			foreach ( $content['title'] as $idx => $content_id ) {
				$type = isset( $content['type'][ $idx ] ) ? $content['type'][ $idx ] : 'custom';

				if ( 'custom' === $type ) {
					$key = 'custom_' . $custom_index;
					$custom_index++;
				} else {
					$key = $type;
				}

				$formatted[] = [
					'type'        => $type,
					'key'         => $key,
					'title'       => isset( $content['title'][ $idx ] ) ? $content['title'][ $idx ] : '',
					'description' => do_shortcode( isset( $content['description'][ $idx ] ) ? $content['description'][ $idx ] : '' ),
				];
			}
		}
		return $formatted;
	}

	/**
	 * Get default WordPress cookies.
	 *
	 * @access  public
	 * @since   7.12
	 * @return  array
	 */
	public function get_default_wp_cookies() {
		return [
			'wordpress_[hash]'           => [
				'description' => esc_html__( 'Used to store your authentication details.', 'Avada' ),
				'lifespan'    => esc_html__( 'Session', 'Avada' ),
			],
			'wordpress_logged_in_[hash]' => [
				'description' => esc_html__( 'Used to store your authentication details.', 'Avada' ),
				'lifespan'    => esc_html__( 'Session', 'Avada' ),
			],
			'wordpress_test_cookie'      => [
				'description' => esc_html__( 'Used to check if your browser has cookies enabled.', 'Avada' ),
				'lifespan'    => esc_html__( 'Session', 'Avada' ),
			],
			'wp-settings-[user_id]'      => [
				'description' => esc_html__( 'Used to persist your user configuration in the admin dashboard.', 'Avada' ),
				'lifespan'    => esc_html__( '1 year', 'Avada' ),
			],
			'wp-settings-time-[user_id]' => [
				'description' => esc_html__( 'The time at which wp-settings-[user_id] was set.', 'Avada' ),
				'lifespan'    => esc_html__( '1 year', 'Avada' ),
			],
		];
	}

	/**
	 * Returns necessary cookies set by popular plugins when active.
	 * Shown below the WordPress cookies when the "List WordPress Cookies" option is enabled.
	 *
	 * @access public
	 * @since  7.12
	 * @return array
	 */
	public function get_plugin_necessary_cookies() {
		$cookies = [];

		// WooCommerce.
		if ( class_exists( 'WooCommerce' ) ) {
			$cookies['woocommerce_cart_hash'] = [
				'description' => esc_html__( 'Stores a hash of the cart contents so WooCommerce can detect when cart contents change.', 'Avada' ),
				'lifespan'    => esc_html__( 'Session', 'Avada' ),
			];
			$cookies['woocommerce_items_in_cart'] = [
				'description' => esc_html__( 'Helps WooCommerce determine when cart contents/data changes.', 'Avada' ),
				'lifespan'    => esc_html__( 'Session', 'Avada' ),
			];
			$cookies['wp_woocommerce_session_[hash]'] = [
				'description' => esc_html__( 'Contains a unique session token to maintain your shopping session and cart data.', 'Avada' ),
				'lifespan'    => esc_html__( '2 days', 'Avada' ),
			];
		}

		// The Events Calendar.
		if ( class_exists( 'Tribe__Events__Main' ) ) {
			$cookies['tribe_events_last_view'] = [
				'description' => esc_html__( 'Remembers your preferred calendar view (month, list, day, etc.).', 'Avada' ),
				'lifespan'    => esc_html__( '1 year', 'Avada' ),
			];
		}

		// Advanced Custom Fields Pro — no necessary frontend cookies.

		return $cookies;
	}

	/**
	 * Set consents from cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function set_all_consents() {
		$embeds   = $this->get_embed_types();
		$consents = $this->get_consents();

		foreach ( $embeds as $key => $embed ) {
			if ( ! $this->get_consent( $key ) ) {
				$this->all_consents = false;
				return;
			}
		}

		$this->all_consents = true;
	}

	/**
	 * Get consents.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  array
	 */
	public function get_consents() {
		return $this->consents;
	}

	/**
	 * Get default consents.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  array
	 */
	public function get_default_consents() {
		return $this->default_consents;
	}

	/**
	 * Get specific consent.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $name Name of embed type.
	 * @return  array
	 */
	public function get_consent( $name = '' ) {
		$key = esc_attr( strtolower( $name ) );

		if ( ! array_key_exists( $key, $this->embed_types ) && 'consent' !== $key ) {
			return true;
		}
		return in_array( $key, $this->consents ); // phpcs:ignore WordPress.PHP.StrictInArray
	}

	/**
	 * Save consent.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $name Name of embed type.
	 * @return  void
	 */
	public function add_consent( $name = '' ) {
		$consents   = $this->consents;
		$consents[] = strtolower( esc_attr( $name ) );
		$consents   = array_unique( $consents );

		$this->consents = $consents;
		$this->save_cookie();
	}

	/**
	 * Remove specific consent.
	 *
	 * @access public
	 * @since 5.5.2
	 * @param string $name Name of embed type.
	 * @return void
	 */
	public function remove_consent( $name = '' ) {
		$consents = $this->consents;
		$key      = esc_attr( strtolower( $name ) );

		if ( '' !== $name && isset( $consents[ $name ] ) ) {
			unset( $consents[ $name ] );
		}

		$this->set_consents( $consents );
		$this->save_cookie();
	}

	/**
	 * Save cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   array $consents Consents which you want to save.
	 * @return  void
	 */
	public function save_cookie( $consents = false ) {
		$cookie_args = $this->cookie_args;

		// If passing on consents, set them first.
		if ( $consents ) {
			$this->set_consents( $consents );
		}

		$consents = $this->consents;
		if ( is_array( $consents ) ) {
			$consents = implode( ',', $consents );
		}

		$time = strtotime( '+' . $cookie_args['days'] . ' days' );

		$options = [
			'expires'  => $time,
			'path'     => $cookie_args['path'],
			'domain'   => isset( $cookie_args['domain'] ) ? $cookie_args['domain'] : '',
			'secure'   => isset( $cookie_args['secure'] ) ? (bool) $cookie_args['secure'] : is_ssl(),
			'httponly' => false,
			'samesite' => isset( $cookie_args['samesite'] ) ? $cookie_args['samesite'] : 'Lax',
		];

		setcookie( $cookie_args['name'], $consents, $options );
	}

	/**
	 * Clears the saved cookie.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @return  void
	 */
	public function clear_cookie() {
		$cookie_name = $this->cookie_args['name'];

		if ( isset( $_COOKIE ) && isset( $_COOKIE[ $cookie_name ] ) ) {
			unset( $_COOKIE[ $cookie_name ] );
			$cookie_args = $this->cookie_args;
			$options     = [
				'expires'  => time() - 3600,
				'path'     => $cookie_args['path'],
				'domain'   => isset( $cookie_args['domain'] ) ? $cookie_args['domain'] : '',
				'secure'   => isset( $cookie_args['secure'] ) ? (bool) $cookie_args['secure'] : is_ssl(),
				'httponly' => false,
				'samesite' => isset( $cookie_args['samesite'] ) ? $cookie_args['samesite'] : 'Lax',
			];
			setcookie( $cookie_name, '', $options );
			$this->consents = [];
		}

	}

	/**
	 * Search string.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $type Embed type.
	 * @param   string $src Url src for embed.
	 * @return  string
	 */
	public function search( $type, $src = '' ) {
		$embed = $this->get_embed_type( $type );

		if ( ! $embed ) {
			return false;
		}

		if ( isset( $embed['search'] ) && is_string( $embed['search'] ) ) {
			return ( strpos( $src, $embed['search'] ) );
		}

		if ( isset( $embed['search'] ) && is_array( $embed['search'] ) ) {
			foreach ( $embed['search'] as $search ) {
				if ( strpos( $src, $search ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Replaces iframe src with temporary.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $src Url src for embed.
	 * @return  string
	 */
	public function get_src_type( $src = '' ) {
		$embed_types = (array) $this->embed_types;
		foreach ( $embed_types as $name => $embed ) {
			if ( $this->search( $name, $src ) ) {
				return $name;
			}
		}
		return false;
	}

	/**
	 * Replace in shortcodes.
	 *
	 * @access public
	 * @since 5.5.2
	 * @param string       $output Shortcode output.
	 * @param string       $tag    Shortcode name.
	 * @param array|string $attr   Shortcode attributes array or empty string.
	 * @param array        $m      Regular expression match array.
	 * @return string
	 */
	public function shortcode_replace( $output, $tag, $attr, $m ) {
		return $this->replace( $output );
	}

	/**
	 * Replaces iframe src with temporary.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $content HTML content to filter.
	 * @return  string
	 */
	public function replace( $content ) {
		global $fusion_library;

		// Iframe replacements.
		preg_match_all( '/<iframe.*src=\"(.*)\".*>\s*<\/iframe>/isU', $content, $iframes );
		if ( array_key_exists( 1, $iframes ) ) {
			foreach ( $iframes[0] as $key => $frame ) {

				$src  = $iframes[1][ $key ];
				$orig = $frame;

				// Its already been filtered.
				if ( strpos( $frame, 'data-privacy-src' ) ) {
					continue;
				}

				// Remove whitespace and line breaks between attributes, needed e.g. for TEC.
				$frame = preg_replace( "/\n\s*\t*/", ' ', $frame );

				// If "Avada" lazy-loading method is applied, then get the original src from 'data-orig-src'.
				if ( $fusion_library->get_images_obj()->is_avada_iframe_lazy_load_enabled() && is_string( $frame ) && preg_match( '/<iframe.*data-orig-src=\"(.*)\".*><\/iframe>/isU', $frame, $lazy_loading_matches ) ) {

					// Also replace the src with the lazy-loading src.
					// This will make privacy script and lazy-loading script work no matter they order of execution.
					$frame = str_replace( ' src="' . $src . '"', ' src="' . $lazy_loading_matches[1] . '"', $frame );
					$src   = $lazy_loading_matches[1];
				}

				// Check the iframe type and continue if not one of ours.
				$type = $this->get_src_type( $src );
				if ( ! $type ) {
					continue;
				}

				// Check if we already have consent.
				if ( $this->get_consent( $type ) ) {
					continue;
				}

				// Replace src with data attribute.
				$frame = str_replace( $src, '$$temp$$', $frame );
				$frame = str_replace( ' src=', ' data-privacy-src=', $frame );
				$frame = str_replace( '$$temp$$', $src, $frame );
				$frame = str_replace( '<iframe ', '<iframe data-privacy-type="' . $type . '" src="" ', $frame );

				if ( strpos( $frame, 'class="' ) || strpos( $frame, "class='" ) ) {
					$frame = str_replace( [ 'class="', "class='" ], 'class="fusion-hidden ', $frame );
				} else {
					$frame = str_replace( '<iframe ', '<iframe class="fusion-hidden" ', $frame );
				}

				$frame_width  = false;
				$frame_height = false;

				// Get dimensions if set.
				preg_match( '/width="(.*?)"/', $frame, $width );
				if ( isset( $width[1] ) ) {
					preg_match( '/height="(.*?)"/', $frame, $height );
					if ( isset( $height[1] ) ) {
						$frame_width  = $width[1];
						$frame_height = $height[1];
					}
				}

				// Add placeholder.
				$placeholder = '';
				if ( ! strpos( $frame, 'data-fusion-no-placeholder' ) ) {
					$placeholder = $this->script_placeholder( $type, $frame_width, $frame_height );

					// Allow custom placeholder additions.
					$placeholder = apply_filters( 'avada_privacy_placeholder', $placeholder, $type, $frame_width, $frame_height, $src );
				}

				// Replace iframe.
				$content = str_replace( $orig, $frame . $placeholder, $content );
			}

			preg_match_all( '/<(lite-youtube|lite-vimeo) .*>.*<\/(lite-youtube|lite-vimeo)>/', $content, $facades );
			if ( array_key_exists( 1, $facades ) ) {
				foreach ( $facades[0] as $key => $facade ) {

					$orig = $facade;
					$src  = $facades[1][ $key ];

					// Its already been filtered.
					if ( strpos( $facade, 'data-privacy-src' ) ) {
						continue;
					}

					// Check the iframe type and continue if not one of ours.
					$type = $this->get_src_type( $src . '.com' );

					if ( ! $type ) {
						continue;
					}

					// Check if we already have consent.
					if ( $this->get_consent( $type ) ) {
						continue;
					}

					$loading_class = false !== strpos( $src, 'youtube' ) ? ' lty-load' : '';
					$facade = str_replace( '<' . $src, '<priv-fac-' . $src . ' class="fusion-hidden' . $loading_class . '" data-privacy-type="' . $type . '"', $facade );
					$facade = str_replace( '</' . $src, '</priv-fac-' . $src, $facade );

					$facade_width  = false;
					$facade_height = false;

					// Get dimensions if set.
					preg_match( '/width="(.*?)"/', $facade, $width );
					if ( isset( $width[1] ) ) {
						preg_match( '/height="(.*?)"/', $facade, $height );
						if ( isset( $height[1] ) ) {
							$facade_width  = $width[1];
							$facade_height = $height[1];
						}
					}

					// Add placeholder.
					$placeholder = '';
					if ( ! strpos( $facade, 'data-fusion-no-placeholder' ) ) {
						$placeholder = $this->script_placeholder( $type, $facade_width, $facade_height );

						// Allow custom placeholder additions.
						$placeholder = apply_filters( 'avada_privacy_placeholder', $placeholder, $type, false, false, $src );
					}

					// Replace facade.
					$content = str_replace( $orig, $facade . $placeholder, $content );

				}
			}
		}

		return $content;
	}

	/**
	 * Replaces all script tags with spans.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $content Content you want to replace script tags..
	 * @param   string $type Type of embed.
	 * @return  string
	 */
	public function script_replace( $content, $type ) {

		if ( ! $this->get_consent( $type ) ) {
			preg_match_all( '/<script(.*?)>(.*?)<\/script>/is', $content, $scripts );
			if ( array_key_exists( 1, $scripts ) ) {
				foreach ( $scripts[0] as $key => $script ) {

					$orig = $script;

					// Replace src with data attribute.
					$script = str_replace( 'src=', 'data-privacy-src=', $script );
					$script = str_replace( '<script', '<noscript class="fusion-hidden" data-privacy-script="true" data-privacy-type="' . $type . '"', $script );
					$script = str_replace( '</script>', '</noscript>', $script );

					// Replace script.
					$content = str_replace( $orig, $script, $content );
				}
			}
		}
		return $content;
	}

	/**
	 * Filters enqueued JS files.
	 *
	 * @access public
	 * @since 5.5.2
	 * @param string $tag    The <script> tag for the enqueued script.
	 * @param string $handle The script's registered handle.
	 * @param string $src    The script's source URL.
	 * @return string
	 */
	public function replace_script_loader_tag( $tag, $handle, $src ) {
		$embeds   = $this->get_embed_types();
		$consents = $this->get_consents();

		foreach ( $embeds as $key => $embed ) {
			if ( $this->search( $key, $src ) ) {
				return $this->script_replace( $tag, $key );
			}
		}
		return $tag;
	}

	/**
	 * Replaces image src with temporary.
	 *
	 * @access  public
	 * @since   5.6.1
	 * @param   string $content Content you want to replace script tags..
	 * @param   string $type Type of embed.
	 * @param   bool   $placeholder Whether to have a placeholder or not.
	 * @param   string $width Width of iframe if set .
	 * @param   string $height Height of iframe if set.
	 * @return  string
	 */
	public function image_block( $content, $type, $placeholder = true, $width = false, $height = false ) {
		$content = $this->image_replace( $content, $type );

		if ( $placeholder ) {
			$placeholder = $this->script_placeholder( $type, $width, $height );
		}

		return $placeholder . $content;
	}

	/**
	 * Replaces all images src,
	 *
	 * @access  public
	 * @since   5.6.1
	 * @param   string $content Content you want to replace script tags..
	 * @param   string $type Type of embed.
	 * @return  string
	 */
	public function image_replace( $content, $type ) {

		if ( ! $this->get_consent( $type ) ) {
			preg_match_all( '/<img\s+[^>]*src="([^"]*)"[^>]*>/isU', $content, $images );
			if ( array_key_exists( 1, $images ) ) {
				foreach ( $images[0] as $key => $image ) {

					$orig = $image;

					// Replace src with data attribute.
					$image = str_replace( 'src=', 'data-privacy-src=', $image );
					$image = str_replace( '<img', '<img class="fusion-hidden" data-privacy-script="true" data-privacy-type="' . $type . '"', $image );

					// Replace script.
					$content = str_replace( $orig, $image, $content );
				}
			}
		}
		return $content;
	}

	/**
	 * Replaces scripts and adds a placeholder.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $content Content you want to replace script tags..
	 * @param   string $type Type of embed.
	 * @param   bool   $placeholder Whether to have a placeholder or not.
	 * @param   string $width Width of iframe if set .
	 * @param   string $height Height of iframe if set.
	 * @return  string
	 */
	public function script_block( $content, $type, $placeholder = true, $width = false, $height = false ) {

		$content = $this->script_replace( $content, $type );

		if ( $placeholder ) {
			$placeholder = $this->script_placeholder( $type, $width, $height );
		}

		return $placeholder . $content;
	}

	/**
	 * Returns a placeholder iframe.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   string $type Name of embed type.
	 * @param   string $width Width of iframe if set .
	 * @param   string $height Height of iframe if set.
	 * @return  string
	 */
	public function script_placeholder( $type, $width = false, $height = false ) {

		if ( ! $this->get_consent( $type ) ) {
			$style = '';
			$label = esc_html( $this->get_embed_type( $type, 'label' ) );

			if ( $width && $height ) {
				$width  = Fusion_Sanitize::get_value_with_unit( $width );
				$height = Fusion_Sanitize::get_value_with_unit( $height );
				$style  = 'style="width:' . $width . '; height:' . $height . ';"';
			}
			$html = '<div class="fusion-privacy-placeholder" ' . $style . ' data-privacy-type="' . $type . '"><div class="fusion-privacy-placeholder-content">';

			/* translators: The placeholder label (embed-type). */
			$content = sprintf( esc_html__( 'For privacy reasons %s needs your permission to be loaded.', 'Avada' ), $label );

			if ( function_exists( 'get_the_privacy_policy_link' ) ) {
				$privacy_link = get_the_privacy_policy_link();
				if ( ! empty( $privacy_link ) ) {
					/* translators: The link to the privacy page (embed-type). */
					$content .= ' ' . sprintf( esc_html__( 'For more details, please see our %s.', 'Avada' ), $privacy_link );
				}
			}

			$content = '<div class="fusion-privacy-label">' . $content . '</div>';

			$html .= apply_filters( 'avada_embeds_consent_text', $content, $label, $type );
			$html .= '<button data-privacy-type="' . $type . '" class="fusion-button button-default fusion-button-default-size button fusion-privacy-consent">' . esc_html__( 'I Accept', 'Avada' ) . '</button>';
			$html .= '</div></div>';

			return $html;
		}
		return '';
	}

	/**
	 * Returns a placeholder iframe.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   array $css CSS dynamic array.
	 * @return  array
	 */
	public function add_styling( $css ) {

		$css['global']['.fusion-privacy-placeholder']['background'] = $this->options['privacy_bg_color'];
		$css['global']['.fusion-privacy-placeholder']['color']      = $this->options['privacy_color'];

		return $css;
	}

	/**
	 * Hides the container for google map.
	 *
	 * @access  public
	 * @since   5.5.2
	 * @param   array $attributes Attributes to filter.
	 * @return  string
	 */
	public function hide_google_map( $attributes ) {
		if ( ! $this->get_consent( 'gmaps' ) && isset( $attributes['class'] ) && ! strpos( $attributes['class'], 'fusion-maps-embed-type' ) ) {
			$attributes['class'] .= ' fusion-hidden';
		}
		return $attributes;
	}

	/**
	 * Displays the privacy bar.
	 *
	 * @access  public
	 * @since   5.6
	 * @return  void
	 */
	public function display_privacy_bar() {
		if ( ! $this->get_consent( 'consent' ) ) {
			get_template_part( 'templates/privacy-bar' );
		}
	}

	/**
	 * Registers Tabs element assets when the privacy bar uses the tabs layout,
	 * so the dependency loads even when the Tabs element is disabled in builder options.
	 *
	 * @access  public
	 * @since   7.15.3
	 * @return  void
	 */
	public function register_bar_dependencies() {
		if ( 'tabs' === Avada()->settings->get( 'privacy_bar_content_layout' ) && function_exists( 'awb_register_tabs_js_files' ) && ! fusion_is_element_enabled( 'fusion_tabs' ) ) {
			awb_register_tabs_js_files();
			awb_register_tabs_css_files();
		}
	}

	/**
	 * Adds bar dynamic styling.
	 *
	 * @access  public
	 * @since   5.6
	 * @param   array $css CSS dynamic array.
	 * @return  array
	 */
	public function add_bar_styling( $css ) {

		$css['global']['.fusion-privacy-bar']['background']                          = $this->options['privacy_bar_bg_color'];
		$css['global']['.fusion-privacy-bar']['color']                               = $this->options['privacy_bar_color'];
		$css['global']['.fusion-privacy-bar a:not(.fusion-button)']['color']         = $this->options['privacy_bar_link_color'];
		$css['global']['.fusion-privacy-bar a:not(.fusion-button):hover']['color']   = $this->options['privacy_bar_link_hover_color'];
		$css['global']['.fusion-privacy-bar-full .column-title']['color']            = $this->options['privacy_bar_headings_color'];
		$css['global']['.fusion-privacy-bar, .fusion-privacy-bar-full']['font-size'] = $this->options['privacy_bar_font_size'];
		$css['global']['.fusion-privacy-bar-full .column-title']['font-size']        = $this->options['privacy_bar_headings_font_size'];
		$css['global']['.fusion-privacy-bar-full .column-title']['line-height']      = $this->options['privacy_bar_headings_font_size'];

		return $css;
	}

	/**
	 * Output default Google Consent Mode script if any related types are enabled.
	 *
	 * @access  public
	 * @since   7.12.0
	 * @return  void
	 */
	public function output_google_consent_default() {
		$google_types = [
			'google_ad_storage'         => 'ad_storage',
			'google_analytics_storage'  => 'analytics_storage',
			'google_ad_user_data'       => 'ad_user_data',
			'google_ad_personalization' => 'ad_personalization',
		];

		$active_types = [];
		foreach ( $google_types as $type => $google_param ) {
			if ( is_array( $this->options['privacy_embed_types'] ) && in_array( $type, $this->options['privacy_embed_types'], true ) ) {
				$active_types[ $type ] = $google_param;
			}
		}

		if ( empty( $active_types ) ) {
			return;
		}

		$consent_params = [];
		foreach ( $active_types as $type => $google_param ) {
			$consent_params[ $google_param ] = $this->get_consent( $type ) ? 'granted' : 'denied';
		}

		echo "<script>\n";
		echo "window.dataLayer = window.dataLayer || [];\n";
		echo "function gtag(){dataLayer.push(arguments);}\n";
		echo "gtag('consent', 'default', " . wp_json_encode( $consent_params ) . ");\n";
		echo "</script>\n";
	}

	/**
	 * Output custom snippets.
	 *
	 * @access  public
	 * @since   7.15.3
	 * @return  void
	 */
	/**
	 * Build and cache a stable index → key map for all custom snippets.
	 * Keys are derived from the slugified label so they survive reordering/deletion.
	 * Duplicate labels get a numeric suffix (-2, -3 …).
	 */
	protected function build_snippet_keys() {
		if ( null !== $this->snippet_keys ) {
			return;
		}
		$this->snippet_keys = [];
		$snippets           = self::normalize_repeater_data( Avada()->settings->get( 'custom_code_snippets' ) );
		if ( empty( $snippets ) ) {
			return;
		}
		$seen = [];
		foreach ( $snippets as $index => $snippet ) {
			$base = ! empty( $snippet['label'] )
				? 'awb_snip_' . substr( sanitize_title( $snippet['label'] ), 0, 32 )
				: 'awb_snip_' . $index;
			$key  = $base;
			$i    = 2;
			while ( in_array( $key, $seen, true ) ) {
				$key = $base . '-' . $i++;
			}
			$seen[]                       = $key;
			$this->snippet_keys[ $index ] = $key;
		}
	}

	/**
	 * Return the stable consent key for a snippet by its array index.
	 *
	 * @param int $index Snippet array index.
	 * @return string
	 */
	public function get_snippet_key( $index ) {
		$this->build_snippet_keys();
		return isset( $this->snippet_keys[ $index ] ) ? $this->snippet_keys[ $index ] : 'awb_snip_' . $index;
	}

	public function output_custom_snippets() {
		$snippets = $this->options['privacy_custom_snippets'];
		if ( ! is_array( $snippets ) ) {
			return;
		}

		$current_hook = current_action();
		$placement    = 'head';

		if ( 'wp_body_open' === $current_hook ) {
			$placement = 'body_open';
		} elseif ( 'wp_footer' === $current_hook ) {
			$placement = 'body_close';
		}

		foreach ( $snippets as $index => $snippet ) {
			if ( empty( $snippet['code'] ) || $placement !== $snippet['placement'] ) {
				continue;
			}

			$category = $snippet['category'];
			$code     = $snippet['code'];
			$id       = $this->get_snippet_key( $index );

			if ( 'necessary' === $category ) {
				echo $code; // phpcs:ignore WordPress.Security.EscapeOutput
			} else {
				$consent    = $this->get_consent( $id );
				$block_type = isset( $snippet['block_type'] ) ? $snippet['block_type'] : 'hard_block';

				if ( $consent || 'soft_block' === $block_type ) {
					echo $code; // phpcs:ignore WordPress.Security.EscapeOutput
				} else {
					echo $this->script_replace( $code, $id );
				}
			}
		}
	}
}
