<?php
/**
 * Execute snippet
 *
 * @package Woody_Code_Snippets
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class WINP_Execute_Snippet {

	/**
	 * @var self
	 */
	private static $instance;

	/**
	 * @var array
	 */
	public $snippets;

	/**
	 * @var WINP_Insertion_Locations
	 */
	public $snippets_locations;

	/**
	 * Current snippet ID being executed.
	 * 
	 * @var int Current snippet ID being executed (for error handling).
	 */
	private static $current_snippet_id = 0;

	/**
	 * Indicates whether shutdown handler has been registered.
	 * 
	 * @var bool Whether shutdown handler has been registered.
	 */
	private static $shutdown_registered = false;

	/**
	 * Tracked executed snippets on current page.
	 * 
	 * @var array<int, array{id: int, name: string, type: string, location: string, scope: string}>
	 */
	private $executed_snippets = [];

	public static function app() {
		if ( self::$instance === null ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Get executed snippets on current page.
	 * 
	 * @return array<int, array{id: int, name: string, type: string, location: string, scope: string}>
	 */
	public function get_executed_snippets() {
		return $this->executed_snippets;
	}

	/**
	 * WINP_Execute_Snippet constructor.
	 */
	public function __construct() {
		self::$instance = $this;
		
		// Check if this is an AJAX validation request and skip the snippet being validated.
		if ( wp_doing_ajax() 
			&& isset( $_POST['action'] ) && 'wbcr_inp_ajax_validate_snippet' === $_POST['action'] 
			&& isset( $_POST['post_id'] ) ) {
			$validating_snippet_id = (int) $_POST['post_id'];
			add_filter(
				'winp_skip_snippet_execution',
				function ( $should_skip, $snippet_id ) use ( $validating_snippet_id ) {
					return $should_skip || $snippet_id === $validating_snippet_id;
				},
				1,
				2
			);
		}

		if ( ! defined( 'WINP_UPLOAD_DIR' ) ) {
			$dir = wp_upload_dir();
			define( 'WINP_UPLOAD_DIR', $dir['basedir'] . '/winp-css-js' );
		}

		if ( ! defined( 'WINP_UPLOAD_URL' ) ) {
			$dir = wp_upload_dir();
			define( 'WINP_UPLOAD_URL', $dir['baseurl'] . '/winp-css-js' );
		}
		global $wpdb;

		// todo: Simplify your request. Seems written ugly
		$sql = "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_content, p2.meta_value as priority
 					FROM {$wpdb->posts}
 					INNER JOIN {$wpdb->postmeta} p1 ON ({$wpdb->posts}.ID = p1.post_id)
 					INNER JOIN {$wpdb->postmeta} p2 ON ({$wpdb->posts}.ID = p2.post_id)
 					INNER JOIN {$wpdb->postmeta} p3 ON ({$wpdb->posts}.ID = p3.post_id)
					WHERE (( p1.meta_key = 'wbcr_inp_snippet_scope' AND p1.meta_value = '%s')
					     AND
					      ( p3.meta_key = 'wbcr_inp_snippet_activate' AND p3.meta_value = '1')
						 AND p2.meta_key = 'wbcr_inp_snippet_priority' ) 
 					AND {$wpdb->posts}.post_type = '" . WINP_SNIPPETS_POST_TYPE . "' 
 					AND ({$wpdb->posts}.post_status = 'publish')
 					ORDER BY CAST(priority AS UNSIGNED) %s";

		$this->snippets['evrywhere'] = $wpdb->get_results( sprintf( $sql, 'evrywhere', 'DESC' ) );
		$this->snippets['auto']      = $wpdb->get_results( sprintf( $sql, 'auto', 'ASC' ) );

		global $winp_snippets_locations;
		$this->snippets_locations = new WINP_Insertion_Locations();
	}

	/**
	 * Register hooks
	 */
	public function register_hooks() {
		add_action( 'init', [ $this, 'execute_everywhere_snippets' ], 1 );

		if ( ! is_admin() ) { // issue PCS-45 fix bug with WPBPage Builder Frontend Editor
			add_action( 'wp_head', [ $this, 'execute_header_snippets' ] );
			add_action( 'wp_footer', [ $this, 'execute_footer_snippets' ] );
			add_action( 'the_post', [ $this, 'executePostSnippets' ], 10, 2 );
			add_filter( 'the_content', [ $this, 'executeContentSnippets' ] );
			add_filter( 'the_excerpt', [ $this, 'executeExcerptSnippets' ] );
			// Бесполезный хук, который вызывается на каждый комментарий. Если их много, увеличивается нагрузка
			// add_filter( 'wp_list_comments_args', [ $this, 'executeListCommentsSnippets' ] );

			// add_action( 'wp_head', [ $this, 'executeWoocommerceSnippets' ] );

			if ( ! empty( $this->snippets_locations->getInsertion( 'custom' ) ) ) {
				add_action( 'wp_head', [ $this, 'executeCustomSnippets' ] );
			}
		}
	}

	/**
	 * Execute the everywhere snippets once the plugins are loaded
	 */
	public function execute_everywhere_snippets() {
		echo $this->execute_active_snippets( 'evrywhere' );
	}

	/**
	 * Execute the snippets in header of page once the plugins are loaded
	 */
	public function execute_header_snippets() {
		echo $this->execute_active_snippets( 'auto', 'header' );
	}

	/**
	 * Execute the snippets in footer of page once the plugins are loaded
	 */
	public function execute_footer_snippets() {
		echo $this->execute_active_snippets( 'auto', 'footer' );
	}

	/**
	 * Execute the snippets before post
	 *
	 * @param WP_Post  $post
	 * @param WP_Query $query
	 */
	public function executePostSnippets( $post, $query ) {
		$content = '';

		$post_type = ! empty( $post ) ? $post->post_type : get_post( $post->ID )->post_type;
		if ( is_singular( [ $post_type ] ) ) {
			if ( did_action( 'get_header' ) ) {
				// Перед заголовком
				$content = $this->execute_active_snippets( 'auto', 'before_post' );
			}
		} elseif ( $query->post_count > 0 ) {
			if ( $query->post_count > 1 && $query->current_post > 0 && $query->post_count > $query->current_post ) {
				// Между записями
				$content = $this->execute_active_snippets( 'auto', 'between_posts' );
			}
				// Перед записью
				$content .= $this->execute_active_snippets( 'auto', 'before_posts', '', $query );

				// После записи
				$content .= $this->execute_active_snippets( 'auto', 'after_posts', '', $query );
		}

		echo $content;
	}

	/**
	 * Handle paragraph content
	 *
	 * @param $content
	 * @param $snippet_content
	 * @param $paragraph_number
	 * @param $type
	 *
	 * @return mixed
	 */
	private function handleParagraphContent( $content, $snippet_content, $paragraph_number, $type = 'before' ) {
		if ( 'before' == $type ) {
			preg_match_all( '/<p(.*?)>/', $content, $matches );
		} else {
			preg_match_all( '/<\/p>/', $content, $matches );
		}
		$paragraphs = $matches[0];

		if ( $paragraph_number == 0 ) {
			$paragraph_number = 1;
		}

		if ( $content && $snippet_content && $paragraphs && $paragraph_number <= count( $paragraphs ) ) {
			$offset = 0;
			foreach ( $paragraphs as $paragraph_key => $paragraph ) {
				$position = strpos( $content, $paragraph, $offset ); // Позиция тега параграфа
				// Если указанный номер параграфа совпадает с текущим
				if ( $paragraph_key + 1 == $paragraph_number ) {
					if ( 'before' == $type ) {
						$content = substr( $content, 0, $position ) . $snippet_content . substr( $content, $position );
					} else {
						$content = substr( $content, 0, $position + 4 ) . $snippet_content . substr( $content, $position + 4 );
					}
					break;
				} else {
					$offset = $position + 1;
				}
			}
		}

		return $content;
	}

	/**
	 * Handle posts content
	 *
	 * @param string  $content
	 * @param string  $snippet_content
	 * @param integer $post_number
	 * @param string  $type
	 * @param object  $query
	 *
	 * @return mixed
	 */
	private function handlePostsContent( $content, $snippet_content, $post_number, $type, $query ) {
		global $winp_after_post_content;
		if ( $query->post_count > 0 ) {
			if ( $post_number == 0 ) {
				$post_number = 1;
			}

			if ( 'before' == $type && $query->current_post + 1 == $post_number ) {
				return $snippet_content;
			} elseif ( 'after' == $type ) {
				// Номер поста совпадает
				if ( $query->current_post == $post_number ) {
					return $snippet_content;
					// Если это последний пост и указанный номер поста больше общего количества постов,
					// то нужно сохранить контент сниппета для вывода в конце данного поста
				} elseif ( $query->current_post + 1 == $query->post_count && $post_number >= $query->post_count ) {
					$winp_after_post_content[ $query->post->ID ] = $snippet_content;
				}
			}
		}

		return $content;
	}

	/**
	 * Execute the snippets page content
	 *
	 * @param $content
	 *
	 * @return mixed
	 */
	public function executeContentSnippets( $content ) {
		global $post, $winp_after_post_content;

		$post_type = ! empty( $post ) ? $post->post_type : false;

		if ( is_category() || is_archive() || is_tag() || is_tax() || is_search() ) {
			// Перед коротким описанием
			$content = $this->execute_active_snippets( 'auto', 'before_excerpt' ) . $content;

			// После короткого описания
			$content .= $this->execute_active_snippets( 'auto', 'after_excerpt' );
		}

		if ( is_singular( [ $post_type ] ) ) {
			// Перед параграфом
			$content = $this->execute_active_snippets( 'auto', 'before_paragraph', $content );

			// После параграфа
			$content = $this->execute_active_snippets( 'auto', 'after_paragraph', $content );

			// После заголовка
			$content = $this->execute_active_snippets( 'auto', 'before_content' ) . $content;

			// После текста
			$content .= $this->execute_active_snippets( 'auto', 'after_content' );

			// После поста
			$content .= $this->execute_active_snippets( 'auto', 'after_post' );

			if ( ! comments_open( $post->ID ) && ! get_comments_number( $post->ID ) ) {
				remove_filter( 'wp_list_comments_args', [ $this, 'executeListCommentsSnippets' ] );
			}
		} elseif ( ! is_null( $post ) && isset( $winp_after_post_content[ $post->ID ] ) ) {
			// После последнего поста в списке
			$content .= $winp_after_post_content[ $post->ID ];
			unset( $winp_after_post_content[ $post->ID ] );
		}

		return $content;
	}

	/**
	 * Execute the snippets page excerpt
	 *
	 * @param $excerpt
	 *
	 * @return mixed
	 */
	public function executeExcerptSnippets( $excerpt ) {
		if ( is_category() || is_archive() || is_tag() || is_tax() || is_search() ) {
			// Перед коротким описанием
			$excerpt = $this->execute_active_snippets( 'auto', 'before_excerpt' ) . $excerpt;

			// После короткого описания
			$excerpt .= $this->execute_active_snippets( 'auto', 'after_excerpt' );
		}

		return $excerpt;
	}

	/**
	 * Execute the list comments filter
	 *
	 * @param $args
	 *
	 * @return mixed
	 */
	public function executeListCommentsSnippets( $args ) {
		global $winp_wp_data;

		$winp_wp_data['winp_comments_saved_end_callback'] = $args['end-callback'];
		$args['end-callback']                             = [ $this, 'executeCommentsSnippets' ];

		return $args;
	}

	/**
	 * Execute the snippets after page comments
	 *
	 * @param $comment
	 * @param $args
	 * @param $depth
	 */
	public function executeCommentsSnippets( $comment, $args, $depth ) {
		global $winp_wp_data, $post;

		if ( ! empty( $winp_wp_data['winp_comments_saved_end_callback'] ) ) {
			echo call_user_func( $winp_wp_data['winp_comments_saved_end_callback'], $comment, $args, $depth );
		}

		$content = '';

		$post_type = ! empty( $post ) ? $post->post_type : false;
		if ( is_singular( [ $post_type ] ) ) {
			// После комментариев
			$content = $this->execute_active_snippets( 'auto', 'after_post' );
		}

		echo $content;
	}

	/**
	 * Execute the custom snippets
	 *
	 * @since 2.4
	 */
	public function executeCustomSnippets() {
		$locations = $this->snippets_locations->getInsertion( 'custom' );
		foreach ( $locations as $location => $data ) {
			$this->execute_active_snippets( 'auto', $location );
		}
	}

	/**
	 * Execute Woocommerce actions/hooks
	 *
	 * @param $location
	 * @param $snippet_content
	 *
	 * @since 2.4
	 */
	public function woocommerce_actions( $location, $snippet_content = '' ) {
		$action = function () use ( $location, $snippet_content ) {
			echo $snippet_content;
		};

		switch ( $location ) {
			case 'woo_before_shop_loop':
				add_filter(
					'woocommerce_product_loop_start',
					function ( $content ) use ( $snippet_content ) {
						return $snippet_content . $content;
					} 
				);
				break;
			case 'woo_after_shop_loop':
				add_filter(
					'woocommerce_product_loop_end',
					function ( $content ) use ( $snippet_content ) {
						return $content . $snippet_content;
					} 
				);
				break;
			case 'woo_before_single_product':
				add_action( 'woocommerce_before_single_product', $action, 10, 2 );
				break;
			case 'woo_after_single_product':
				add_action( 'woocommerce_after_single_product', $action, 10, 2 );
				break;
			case 'woo_before_single_product_summary':
				add_action( 'woocommerce_before_single_product_summary', $action, 10, 2 );
				break;
			case 'woo_after_single_product_summary':
				add_action( 'woocommerce_after_single_product_summary', $action, 10, 2 );
				break;
			case 'woo_single_product_summary_title':
				add_action( 'woocommerce_single_product_summary', $action, 6, 2 );
				break;
			case 'woo_single_product_summary_price':
				add_action( 'woocommerce_single_product_summary', $action, 15, 2 );
				break;
			case 'woo_single_product_summary_excerpt':
				add_action( 'woocommerce_single_product_summary', $action, 25, 2 );
				break;
			default:
				break;
		}
	}

	/**
	 * Execute Woocommerce actions/hooks
	 *
	 * @param $location
	 * @param $snippet_content
	 *
	 * @since 2.4
	 */
	public function custom_actions( $location, $snippet_content = '' ) {
		if ( ! empty( $this->snippets_locations->getLocation( $location ) ) ) {
			/**
			 * Action for a custom location applied in 'wbcr/woody/add_custom_location' filter
			 *
			 * @param array $location Slug of the location.
			 * @param string $snippet_content Rendered snippet content
			 *
			 * @since 2.4
			 */
			do_action( "wbcr/woody/do_custom_location/{$location}", $snippet_content );
		}
	}

	/**
	 * Execute the snippets once the plugins are loaded
	 *
	 * @param string $scope
	 * @param string $location
	 * @param string $content
	 * @param array  $custom_params
	 *
	 * @return string
	 */
	public function execute_active_snippets( $scope = 'evrywhere', $location = '', $content = '', $custom_params = [] ) {
		$snippets = $this->snippets[ $scope ] ?? [];

		if ( ! empty( $snippets ) ) {
			foreach ( (array) $snippets as $snippet ) {
				$id = (int) $snippet->ID;
				
				// Allow filtering to skip specific snippet IDs.
				$should_skip = apply_filters( 'winp_skip_snippet_execution', false, $id );
				if ( $should_skip ) {
					continue;
				}
				
				// $is_active = (int) WINP_Helper::getMetaOption( $id, 'snippet_activate', 0 );
				// Если это сниппет с автовставкой и выбранное место подходит под активный action
				$avail_place = ( 'auto' == $scope ? $location == WINP_Helper::getMetaOption( $id, 'snippet_location', '' ) : true );
				// Если условие отображения сниппета выполняется
				$snippet_type = WINP_Helper::getMetaOption( $id, 'snippet_type', WINP_SNIPPET_TYPE_PHP );
				$is_condition = $snippet_type != WINP_SNIPPET_TYPE_PHP ? $this->checkCondition( $id ) : true;

				if ( $avail_place && $is_condition ) {
					$post_id = (int) WINP_HTTP::post( 'post_ID', 0 );

					if ( ( isset( $_POST['wbcr_inp_snippet_scope'] )
							&& $post_id === $id
							&& WINP_Plugin::app()->current_user_car() ) || WINP_Helper::is_safe_mode() ) {
						return $content;
					}

					// WPML Compatibility
					if ( defined( 'WPML_PLUGIN_FILE' ) ) {
						$wpml_langs = WINP_Helper::getMetaOption( $id, 'snippet_wpml_lang', '' );
						if ( $wpml_langs !== '' && defined( 'ICL_LANGUAGE_CODE' ) ) {
							if ( ! in_array( ICL_LANGUAGE_CODE, explode( ',', $wpml_langs ) ) ) {
								continue;
							}
						}
					}

					$snippet_code = WINP_Helper::get_snippet_code( $snippet );

					/**
					 * Filter snippet code before execute
					 */
					$snippet_code = apply_filters( 'wbcr/inp/execute_snippet/snippet_code', $snippet_code, $id );

					// Track this snippet as executed.
					$this->track_executed_snippet( $id, $scope );

					if ( get_option( 'wbcr_inp_execute_shortcode' ) ) {
						$snippet_code = do_shortcode( $snippet_code );
					}

					if ( $snippet_type === WINP_SNIPPET_TYPE_TEXT || $snippet_type === WINP_SNIPPET_TYPE_AD ) {
						$snippet_content = '<div class="winp-text-snippet-container">' . $snippet_code . '</div>';
					} elseif ( $snippet_type === WINP_SNIPPET_TYPE_CSS || $snippet_type === WINP_SNIPPET_TYPE_JS ) {
						$snippet_content = self::getJsCssSnippetData( $id );
					} elseif ( $snippet_type === WINP_SNIPPET_TYPE_HTML ) {
						$snippet_content = $snippet_code;
					} else {
						$code = $this->prepareCode( $snippet_code, $id );
						ob_start();
						$this->executeSnippet( $code, $id, false );
						$snippet_content = ob_get_contents();
						ob_end_clean();
					}

					// If the user has prohibited the insertion of unfiltered HTML,
					// we prohibit the execution of snippets.
					if ( ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML )
						&& ! in_array(
							$snippet_type,
							[
								WINP_SNIPPET_TYPE_TEXT,
								WINP_SNIPPET_TYPE_AD,
								WINP_SNIPPET_TYPE_CSS,
							] 
						) ) {
						$snippet_content = '';

						if ( is_user_logged_in() && WINP_Plugin::app()->current_user_car() ) {
							$error_text = __( 'This Woody snippet cannot run because unfiltered HTML insertion is disabled.', 'insert-php' );

							switch ( $location ) {
								case 'header':
								case 'footer':
									$snippet_content = '<!-- ' . $error_text . '-->';
									break;
								default:
									$snippet_content = $error_text;
							}
						}
					}

					if ( 'auto' == $scope ) {
						switch ( $location ) {
							case 'before_paragraph': // Перед параграфом
								$location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
								$content         = $this->handleParagraphContent( $content, $snippet_content, $location_number );
								break;
							case 'after_paragraph': // После параграфа
								$location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
								$content         = $this->handleParagraphContent( $content, $snippet_content, $location_number, 'after' );
								break;
							case 'before_posts':   // Перед записью
								$location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
								$content         = $this->handlePostsContent( $content, $snippet_content, $location_number, 'before', $custom_params );
								break;
							case 'after_posts':   // После записи
								$location_number = WINP_Helper::getMetaOption( $id, 'snippet_p_number', 0 );
								$content         = $this->handlePostsContent( $content, $snippet_content, $location_number, 'after', $custom_params );
								break;
							default:
								$content = $snippet_content . $content;
						}

						/**
						 * Action for woo actions
						 *
						 * @param array $location Slug of the location.
						 * @param string $snippet_content Rendered snippet content
						 *
						 * @since 2.4
						 */
						do_action( 'wbcr/woody/do_woocommerce_actions', $location, $snippet_content );

						// $this->woocommerce_actions( $location, $snippet_content );
						$this->custom_actions( $location, $snippet_content );
					} else {
						$content = $snippet_content . $content;
					}
				}
			}
		}

		return $content;
	}

	/**
	 * Track shortcode snippet execution.
	 * 
	 * Public method for shortcode classes to call.
	 * 
	 * @param int $snippet_id Snippet ID.
	 * @return void
	 */
	public function track_shortcode_snippet( $snippet_id ) {
		$this->track_executed_snippet( $snippet_id, 'shortcode' );
	}

	/**
	 * Track executed snippet.
	 * 
	 * @param int    $snippet_id Snippet ID.
	 * @param string $scope Snippet scope.
	 * @return void
	 */
	private function track_executed_snippet( $snippet_id, $scope ) {
		if ( isset( $this->executed_snippets[ $snippet_id ] ) ) {
			return; // Already tracked.
		}

		$snippet = get_post( $snippet_id );
		if ( ! $snippet ) {
			return;
		}

		$snippet_type     = WINP_Helper::getMetaOption( $snippet_id, 'snippet_type', WINP_SNIPPET_TYPE_PHP );
		$snippet_location = WINP_Helper::getMetaOption( $snippet_id, 'snippet_location', '' );

		// Map location to human-readable label.
		$location_label = $this->get_location_label( $scope, $snippet_location );

		$this->executed_snippets[ $snippet_id ] = [
			'id'       => $snippet_id,
			'name'     => $snippet->post_title,
			'type'     => $this->get_type_label( $snippet_type ),
			'location' => $location_label,
			'scope'    => $scope,
		];
	}

	/**
	 * Get human-readable location label.
	 * 
	 * @param string $scope Snippet scope.
	 * @param string $location Snippet location.
	 * @return string
	 */
	private function get_location_label( $scope, $location ) {
		if ( 'evrywhere' === $scope ) {
			return __( 'Everywhere', 'insert-php' );
		}

		if ( 'shortcode' === $scope ) {
			return __( 'Shortcode', 'insert-php' );
		}

		$location_labels = [
			'header'           => __( 'Header', 'insert-php' ),
			'footer'           => __( 'Footer', 'insert-php' ),
			'before_post'      => __( 'Before Post', 'insert-php' ),
			'before_content'   => __( 'Before Content', 'insert-php' ),
			'before_paragraph' => __( 'Before Paragraph', 'insert-php' ),
			'after_paragraph'  => __( 'After Paragraph', 'insert-php' ),
			'after_content'    => __( 'After Content', 'insert-php' ),
			'after_post'       => __( 'After Post', 'insert-php' ),
			'before_excerpt'   => __( 'Before Excerpt', 'insert-php' ),
			'after_excerpt'    => __( 'After Excerpt', 'insert-php' ),
			'before_posts'     => __( 'Before Posts', 'insert-php' ),
			'after_posts'      => __( 'After Posts', 'insert-php' ),
			'after_comments'   => __( 'After Comments', 'insert-php' ),
		];

		return $location_labels[ $location ] ?? ucwords( str_replace( '_', ' ', $location ) );
	}

	/**
	 * Get human-readable type label.
	 * 
	 * @param string $type Snippet type.
	 * @return string
	 */
	private function get_type_label( $type ) {
		$type_labels = [
			WINP_SNIPPET_TYPE_PHP       => 'PHP',
			WINP_SNIPPET_TYPE_UNIVERSAL => 'Universal',
			WINP_SNIPPET_TYPE_HTML      => 'HTML',
			WINP_SNIPPET_TYPE_CSS       => 'CSS',
			WINP_SNIPPET_TYPE_JS        => 'JS',
			WINP_SNIPPET_TYPE_TEXT      => 'TEXT',
			WINP_SNIPPET_TYPE_AD        => 'AD',
		];

		return $type_labels[ $type ] ?? 'PHP';
	}

	/**
	 * Get js or css snippet data
	 *
	 * @param $snippet_id
	 *
	 * @return mixed|string
	 */
	public static function getJsCssSnippetData( $snippet_id ) {
		$snippet_type = WINP_Helper::get_snippet_type( $snippet_id );

		$linking  = WINP_Helper::getMetaOption( $snippet_id, 'snippet_linking' );
		$filetype = WINP_Helper::getMetaOption( $snippet_id, 'filetype', $snippet_type );

		$file_name = $snippet_id . '.' . $filetype;
		$slug      = WINP_Helper::getMetaOption( $snippet_id, 'css_js_slug' );
		if ( ! empty( $slug ) ) {
			$file_name = $slug . '.' . $filetype;
		}

		if ( file_exists( WINP_UPLOAD_DIR . '/' . $file_name ) ) {
			if ( 'inline' == $linking ) {
				return file_get_contents( WINP_UPLOAD_DIR . '/' . $file_name );
			}

			if ( 'external' == $linking ) {
				$file_name .= '?ver=' . WINP_Helper::getMetaOption( $snippet_id, 'css_js_version', time() );

				if ( 'js' == $snippet_type ) {
					return PHP_EOL . "<script type='text/javascript' src='" . WINP_UPLOAD_URL . '/' . $file_name . "'></script>" . PHP_EOL;
				}

				if ( 'css' == $snippet_type ) {
					$short_filename = preg_replace( '@\.css\?ver=.*$@', '', $file_name );

					return PHP_EOL . "<link rel='stylesheet' id='" . $short_filename . "-css'  href='" . WINP_UPLOAD_URL . '/' . $file_name . "' type='text/css' media='all' />" . PHP_EOL;
				}
			}
		}

		return '';
	}

	/**
	 * Execute a snippet
	 *
	 * Code must NOT be escaped, as
	 * it will be executed directly
	 *
	 * @param string $code The snippet code to execute.
	 * @param int    $id The snippet ID.
	 * @param bool   $catch_output Whether to attempt to suppress the output of execution using buffers.
	 *
	 * @return mixed        The result of the code execution
	 */
	public function executeSnippet( $code, $id = 0, $catch_output = true ) {
		$id = (int) $id;

		if ( ! $id || empty( $code ) ) {
			return false;
		}

		if ( $catch_output ) {
			ob_start();
		}

		$snippet = get_post( $id );

		if ( empty( $snippet ) || $snippet->post_type !== WINP_SNIPPETS_POST_TYPE ) {
			return false;
		}

		$snippet_type = WINP_Helper::getMetaOption( $id, 'snippet_type', true );

		// Set current snippet ID for error handling.
		self::$current_snippet_id = $id;

		// Register shutdown function once to catch fatal errors.
		if ( ! self::$shutdown_registered ) {
			register_shutdown_function( [ $this, 'handle_snippet_shutdown' ] );
			self::$shutdown_registered = true;
		}

		if ( $snippet_type == WINP_SNIPPET_TYPE_UNIVERSAL ) {
			$result = eval( '?>' . $code . '<?php ' );
		} elseif ( $snippet_type == WINP_SNIPPET_TYPE_PHP ) {
			$result = eval( $code );
		} else {
			$result = ! empty( $code );
		}

		if ( $catch_output ) {
			ob_end_clean();
		}

		return $result;
	}

	/**
	 * Handle fatal errors during snippet execution.
	 * 
	 * @return void
	 */
	public function handle_snippet_shutdown() {
		$error = error_get_last();

		// If there's a fatal error and a snippet was being executed, check if it's from the snippet.
		if ( self::$current_snippet_id && $error && in_array( $error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ] ) ) {
			// Only log if the error originated from the snippet code itself.
			if ( $this->is_error_from_snippet( $error ) ) {
				$this->log_snippet_error( self::$current_snippet_id, $error );
			}
		}
		
		// Clear the snippet ID after processing.
		self::$current_snippet_id = 0;
	}

	/**
	 * Check if the error originated from the snippet code.
	 * 
	 * @param array<string, mixed> $error Error details.
	 * 
	 * @return bool True if error is from snippet, false otherwise.
	 */
	private function is_error_from_snippet( $error ) {
		if ( ! isset( $error['file'] ) ) {
			return false;
		}

		$error_file = $error['file'];

		// Check if error is from eval'd code (PHP snippets executed via eval).
		if ( strpos( $error_file, "eval()'d code" ) !== false ) {
			return true;
		}

		return false;
	}

	/**
	 * Log snippet error via admin notice
	 *
	 * @param int                  $snippet_id Snippet ID.
	 * @param array<string, mixed> $error Error details.
	 * 
	 * @return void
	 */
	private function log_snippet_error( $snippet_id, $error ) {
		// Validate error array has required keys.
		if ( ! isset( $error['message'], $error['file'], $error['line'] ) ) {
			return;
		}
		
		// Generate unique notice ID based on snippet ID and error details.
		$error_signature = md5( $snippet_id . $error['message'] . $error['file'] . $error['line'] );
		$notice_id       = 'snippet_error_' . $error_signature;

		// Get snippet title for better context.
		$snippet       = get_post( $snippet_id );
		$snippet_title = $snippet ? $snippet->post_title : "ID {$snippet_id}";

		// Extract the main error message (before stack trace).
		$error_message = $error['message'];
		if ( strpos( $error_message, ' Stack trace:' ) !== false ) {
			$error_message = substr( $error_message, 0, strpos( $error_message, ' Stack trace:' ) );
		}
		$error_message = trim( $error_message );

		// Shorten file path for readability.
		$file_path = str_replace( ABSPATH, '', $error['file'] );

		// Get edit link for the snippet.
		$edit_link = admin_url( 'post.php?post=' . $snippet_id . '&action=edit' );

		// Build notice message.
		$message = sprintf(
			'<div class="winp-error-notice-header"><strong>%s</strong> %s <a href="%s" class="winp-snippet-edit-link" target="_blank">%s</a></div><details><summary>%s</summary><div class="winp-error-details"><div class="winp-error-message"><strong>%s:</strong><br><code>%s</code></div><div class="winp-error-location"><strong>%s:</strong><br>%s <span class="winp-line-number">%s %d</span></div></div></details><div class="winp-error-notice-footer"><p class="winp-dismiss-help">%s</p><button type="button" class="button winp-manual-dismiss-btn">%s</button></div>',
			__( 'Snippet Error Detected', 'insert-php' ),
			// Translators: 1: Snippet title.
			sprintf( __( 'Snippet "%s" caused a fatal error.', 'insert-php' ), esc_html( $snippet_title ) ),
			esc_url( $edit_link ),
			__( 'Edit Snippet', 'insert-php' ),
			__( 'Show error details', 'insert-php' ),
			__( 'Error', 'insert-php' ),
			esc_html( $error_message ),
			__( 'Location', 'insert-php' ),
			esc_html( $file_path ),
			__( 'line', 'insert-php' ),
			$error['line'],
			__( 'If you have fixed this error, you can dismiss this notice.', 'insert-php' ),
			__( 'Dismiss', 'insert-php' )
		);

		// Store error notice in option to be displayed on next admin page load.
		$pending_notices = get_option( 'winp_pending_error_notices', [] );
		if ( ! is_array( $pending_notices ) ) {
			$pending_notices = [];
		}

		// Only add if not already present (avoid race conditions).
		if ( ! isset( $pending_notices[ $notice_id ] ) ) {
			$pending_notices[ $notice_id ] = [
				'message' => $message,
				'type'    => 'error',
			];
			
			update_option( 'winp_pending_error_notices', $pending_notices, false );

			// Send email notification if enabled and this is a new error.
			$this->send_error_email( $notice_id, $snippet_id, $snippet_title, $error_message, $file_path, $error['line'], $edit_link );
		}
	}

	/**
	 * Send email notification for snippet error
	 *
	 * @param string $error_signature Unique error identifier.
	 * @param int    $snippet_id The ID of the snippet.
	 * @param string $snippet_title The title of the snippet.
	 * @param string $error_message The error message.
	 * @param string $file_path The file path where error occurred.
	 * @param int    $line_number The line number where error occurred.
	 * @param string $edit_link Link to edit the snippet.
	 *
	 * @return void
	 */
	private function send_error_email( $error_signature, $snippet_id, $snippet_title, $error_message, $file_path, $line_number, $edit_link ) {
		// Check if email notifications are enabled.
		$email_enabled = get_option( 'wbcr_inp_error_email_enabled' );
		if ( ! $email_enabled ) {
			return;
		}

		// Get email address.
		$email_address = get_option( 'wbcr_inp_error_email_address', get_option( 'admin_email' ) );
		if ( empty( $email_address ) || ! is_email( $email_address ) ) {
			return;
		}

		// Check if we've already emailed about this error.
		$emailed_errors = get_option( 'winp_emailed_errors', [] );
		if ( ! is_array( $emailed_errors ) ) {
			$emailed_errors = [];
		}

		// If we've already emailed about this error, skip.
		if ( isset( $emailed_errors[ $error_signature ] ) ) {
			return;
		}

		// Clean up old entries (older than 30 days).
		$thirty_days_ago = time() - ( 30 * DAY_IN_SECONDS );
		foreach ( $emailed_errors as $hash => $timestamp ) {
			if ( $timestamp < $thirty_days_ago ) {
				unset( $emailed_errors[ $hash ] );
			}
		}

		// Build email content.
		$site_name = get_bloginfo( 'name' );
		$admin_url = admin_url( 'edit.php?post_type=' . WINP_SNIPPETS_POST_TYPE );
		$subject   = sprintf( '[%s] Snippet Error Detected: %s', $site_name, $snippet_title );

		$email_body = $this->get_error_email_template( $site_name, $snippet_id, $snippet_title, $error_message, $file_path, $line_number, $edit_link, $admin_url );

		// Set email headers for HTML.
		$headers = [
			'Content-Type: text/html; charset=UTF-8',
		];

		// Send email.
		$sent = wp_mail( $email_address, $subject, $email_body, $headers );

		// If email sent successfully, mark this error as emailed.
		if ( $sent ) {
			$emailed_errors[ $error_signature ] = time();
			update_option( 'winp_emailed_errors', $emailed_errors, false );
		}
	}

	/**
	 * Get HTML email template for error notification
	 *
	 * @param string $site_name Site name.
	 * @param int    $snippet_id Snippet ID.
	 * @param string $snippet_title Snippet title.
	 * @param string $error_message Error message.
	 * @param string $file_path File path.
	 * @param int    $line_number Line number.
	 * @param string $edit_link Edit link.
	 * @param string $admin_url Admin URL.
	 *
	 * @return string HTML email template.
	 */
	private function get_error_email_template( $site_name, $snippet_id, $snippet_title, $error_message, $file_path, $line_number, $edit_link, $admin_url ) {
		ob_start();
		?>
		<!DOCTYPE html>
		<html>
		<head>
			<meta charset="UTF-8">
			<meta name="viewport" content="width=device-width, initial-scale=1.0">
			<title><?php echo esc_html( __( 'Snippet Error Notification', 'insert-php' ) ); ?></title>
		</head>
		<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; background-color: #f0f0f1;">
			<table role="presentation" style="width: 100%; border-collapse: collapse;">
				<tr>
					<td align="center" style="padding: 40px 20px;">
						<table role="presentation" style="width: 100%; max-width: 600px; border-collapse: collapse; background-color: #ffffff; border: 1px solid #c3c4c7; border-radius: 2px;">
							<!-- Header -->
							<tr>
								<td style="padding: 24px 30px; background-color: #2271b1; border-bottom: 1px solid #135e96;">
									<h1 style="margin: 0; color: #ffffff; font-size: 20px; font-weight: 600;">
										⚠️ <?php echo esc_html( __( 'Snippet Error Detected', 'insert-php' ) ); ?>
									</h1>
								</td>
							</tr>

							<!-- Content -->
							<tr>
								<td style="padding: 30px;">
									<p style="margin: 0 0 16px; color: #1d2327; font-size: 14px; line-height: 1.6;">
										<?php echo esc_html( __( 'Hello,', 'insert-php' ) ); ?>
									</p>

									<p style="margin: 0 0 20px; color: #1d2327; font-size: 14px; line-height: 1.6;">
										<?php
										printf(
											// translators: 1: snippet title, 2: site name.
											esc_html( __( 'A fatal error has been detected in the snippet "%1$s" on your site %2$s.', 'insert-php' ) ),
											'<strong>' . esc_html( $snippet_title ) . '</strong>',
											'<strong>' . esc_html( $site_name ) . '</strong>'
										);
										?>
									</p>

									<!-- Error Details Box -->
									<table role="presentation" style="width: 100%; border-collapse: collapse; margin: 24px 0; background-color: #fcf0f1; border-left: 4px solid #d63638; border-radius: 0;">
										<tr>
											<td style="padding: 16px 20px;">
												<p style="margin: 0 0 12px; color: #8c1c13; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
													<?php echo esc_html( __( 'Error Details', 'insert-php' ) ); ?>
												</p>

												<div style="margin-bottom: 12px;">
													<p style="margin: 0 0 6px; color: #3c434a; font-size: 13px; font-weight: 600;">
														<?php echo esc_html( __( 'Error Message:', 'insert-php' ) ); ?>
													</p>
													<p style="margin: 0; padding: 8px 12px; background-color: #ffffff; border: 1px solid #dcdcde; color: #1d2327; font-size: 13px; font-family: Consolas, Monaco, monospace; word-break: break-word; line-height: 1.5;">
														<?php echo esc_html( $error_message ); ?>
													</p>
												</div>

												<div style="margin-bottom: 12px;">
													<p style="margin: 0 0 6px; color: #3c434a; font-size: 13px; font-weight: 600;">
														<?php echo esc_html( __( 'Location:', 'insert-php' ) ); ?>
													</p>
													<p style="margin: 0; padding: 8px 12px; background-color: #ffffff; border: 1px solid #dcdcde; color: #1d2327; font-size: 13px; font-family: Consolas, Monaco, monospace; line-height: 1.5;">
														<?php echo esc_html( $file_path ); ?> <span style="color: #646970;"><?php echo esc_html( __( 'line', 'insert-php' ) ); ?> <?php echo intval( $line_number ); ?></span>
													</p>
												</div>

												<div>
													<p style="margin: 0 0 6px; color: #3c434a; font-size: 13px; font-weight: 600;">
														<?php echo esc_html( __( 'Snippet:', 'insert-php' ) ); ?>
													</p>
													<p style="margin: 0; padding: 8px 12px; background-color: #ffffff; border: 1px solid #dcdcde; color: #1d2327; font-size: 13px; line-height: 1.5;">
														<?php echo esc_html( $snippet_title ); ?> <span style="color: #646970;">(ID: <?php echo intval( $snippet_id ); ?>)</span>
													</p>
												</div>
											</td>
										</tr>
									</table>

									<!-- Action Buttons -->
									<table role="presentation" style="width: 100%; border-collapse: collapse; margin: 24px 0;">
										<tr>
											<td style="padding: 0;">
												<a href="<?php echo esc_url( $edit_link ); ?>" style="display: inline-block; padding: 10px 20px; background-color: #2271b1; color: #ffffff; text-decoration: none; border-radius: 3px; font-weight: 500; font-size: 13px; margin-right: 8px; border: 1px solid #2271b1;">
													<?php echo esc_html( __( 'Edit Snippet', 'insert-php' ) ); ?>
												</a>
												<a href="<?php echo esc_url( $admin_url ); ?>" style="display: inline-block; padding: 10px 20px; background-color: #f6f7f7; color: #2c3338; text-decoration: none; border-radius: 3px; font-weight: 500; font-size: 13px; border: 1px solid #c3c4c7;">
													<?php echo esc_html( __( 'View All Snippets', 'insert-php' ) ); ?>
												</a>
											</td>
										</tr>
									</table>

									<p style="margin: 20px 0 0; color: #646970; font-size: 13px; line-height: 1.6;">
										<?php echo esc_html( __( 'This notification is sent only once per unique error. You can disable these notifications in the plugin settings.', 'insert-php' ) ); ?>
									</p>
								</td>
							</tr>

							<!-- Footer -->
							<tr>
								<td style="padding: 16px 30px; background-color: #f6f7f7; border-top: 1px solid #dcdcde;">
									<p style="margin: 0; color: #646970; font-size: 12px; text-align: center;">
										<?php
										printf(
											// translators: %s: site name.
											esc_html( __( 'This email was sent by Woody Code Snippets on %s', 'insert-php' ) ),
											'<strong>' . esc_html( $site_name ) . '</strong>'
										);
										?>
									</p>
								</td>
							</tr>
						</table>
					</td>
				</tr>
			</table>
		</body>
		</html>
		<?php
		$output = ob_get_clean();
		return false !== $output ? $output : '';
	}

	/**
	 * Get property value
	 *
	 * @param $value
	 * @param $property
	 *
	 * @return null
	 */
	private function getPropertyValue( $value, $property ) {
		if ( is_object( $value ) ) {
			return $value->$property ?? null;
		} elseif ( isset( $value[ $property ] ) ) {
			return $value[ $property ];
		}

		return null;
	}

	/**
	 * Check conditional execution logic for the snippet
	 *
	 * @param $snippet_id
	 *
	 * @return bool
	 */
	public function checkCondition( $snippet_id ) {
		// Итоговый результат условий
		$result = true;
		// Получаем сохранённые параметры условий
		$filters = get_post_meta( $snippet_id, 'wbcr_inp_snippet_filters' );
		// Если условия указаны
		if ( ! ( empty( $filters ) || isset( $filters[0] ) && empty( $filters[0] ) ) ) {
			foreach ( $filters[0] as $filter ) {
				$conditions = $this->getPropertyValue( $filter, 'conditions' );
				// Если условия пусты, то пропускаем цикл
				if ( empty( $conditions ) ) {
					continue;
				}
				// Промежуточный результат AND условий
				$and_conditions = null;
				// Проходим по AND условиям
				foreach ( $conditions as $scope ) {
					$scope_conditions = $this->getPropertyValue( $scope, 'conditions' );
					// Если условия пусты, то пропускаем цикл
					if ( empty( $scope_conditions ) ) {
						continue;
					}
					// Промежуточный результат OR условий
					$or_conditions = null;
					// Проходим по OR условиям
					foreach ( $scope_conditions as $condition ) {
						$method_name = str_replace( '-', '_', $this->getPropertyValue( $condition, 'param' ) );
						$operator    = $this->getPropertyValue( $condition, 'operator' );
						$value       = $this->getPropertyValue( $condition, 'value' );
						// Получаем результат OR условий
						$or_conditions = is_null( $or_conditions ) ? $this->call_method( $method_name, $operator, $value ) : $or_conditions || $this->call_method( $method_name, $operator, $value );
					}
					// Получаем результат AND условий
					$and_conditions = is_null( $and_conditions ) ? $or_conditions : $and_conditions && $or_conditions;
				}
				// Получаем результат блока условий
				$result = $this->getPropertyValue( $filter, 'type' ) == 'showif' ? $and_conditions : ! $and_conditions;
			}
		}

		return $result;
	}

	/**
	 * Call specified method
	 *
	 * @param $method_name
	 * @param $operator
	 * @param $value
	 *
	 * @return bool
	 */
	private function call_method( $method_name, $operator, $value ) {
		if ( method_exists( $this, $method_name ) ) {
			return $this->$method_name( $operator, $value );
		} else {
			return apply_filters( 'wbcr/inp/execute/check_condition', false, $method_name, $operator, $value );
		}
	}

	/**
	 * Retrieve the first error in a snippet's code
	 *
	 * @param int $snippet_id
	 *
	 * @return array|bool
	 */
	public function getSnippetError( $snippet_id ) {
		if ( ! intval( $snippet_id ) ) {
			return false;
		}

		$snippet = get_post( $snippet_id );

		if ( ! $snippet ) {
			return false;
		}

		$snippet_code = WINP_Helper::get_snippet_code( $snippet );
		$snippet_code = $this->prepareCode( $snippet_code, $snippet_id );

		$result = $this->executeSnippet( $snippet_code, $snippet_id );

		if ( false !== $result ) {
			return false;
		}

		$error = error_get_last();

		if ( is_null( $error ) ) {
			return false;
		}

		return $error;
	}

	/**
	 * Prepare the code by removing php tags from beginning and end
	 *
	 * @param string  $code
	 * @param integer $snippet_id
	 *
	 * @return string
	 */
	public function prepareCode( $code, $snippet_id ) {
		$snippet_type = WINP_Helper::get_snippet_type( $snippet_id );

		if ( $snippet_type != WINP_SNIPPET_TYPE_UNIVERSAL
			&& $snippet_type != WINP_SNIPPET_TYPE_CSS
			&& $snippet_type != WINP_SNIPPET_TYPE_JS
			&& $snippet_type != WINP_SNIPPET_TYPE_HTML ) {

			/* Remove <?php and <? from beginning of snippet */
			$code = preg_replace( '|^[\s]*<\?(php)?|', '', $code );

			/* Remove ?> from end of snippet */
			$code = preg_replace( '|\?>[\s]*$|', '', $code );
		}

		return $code;
	}

	/**
	 * Get current URL
	 *
	 * @return string
	 */
	private function getCurrentUrl() {
		$out = '';
		$url = explode( '?', $_SERVER['REQUEST_URI'], 2 );
		if ( isset( $url[0] ) ) {
			$out = trim( $url[0], '/' );
		}

		return $out ? urldecode( $out ) : '/';
	}

	/**
	 * Get referer URL
	 *
	 * @return string
	 */
	private function getRefererUrl() {
		$out = '';
		$url = explode( '?', str_replace( site_url(), '', $_SERVER['HTTP_REFERER'] ), 2 );
		if ( isset( $url[0] ) ) {
			$out = trim( $url[0], '/' );
		}

		return $out ? urldecode( $out ) : '/';
	}

	/**
	 * Check by operator
	 *
	 * @param string $operation Comparison operator.
	 * @param mixed  $first First value.
	 * @param mixed  $second Second value.
	 * @param bool   $third Third value.
	 *
	 * @return bool
	 */
	public function check_by_operator( $operation, $first, $second, $third = false ) {
		switch ( $operation ) {
			case 'equals':
				if ( is_array( $second ) ) {
					return in_array( $first, $second );
				} else {
					return $first === $second;
				}
			case 'notequal':
				if ( is_array( $second ) ) {
					return ! in_array( $first, $second );
				} else {
					return $first !== $second;
				}
			case 'less':
			case 'older':
				return $first > $second;
			case 'greater':
			case 'younger':
				return $first < $second;
			case 'contains':
				return strpos( $first, $second ) !== false;
			case 'notcontain':
				return strpos( $first, $second ) === false;
			case 'between':
				return $first < $second && $second < $third;

			default:
				return $first === $second;
		}
	}

	/**
	 * A role of the user who views your website. The role "guest" is applied for unregistered users.
	 *
	 * @param string $operator
	 * @param string $value
	 *
	 * @return boolean
	 */
	private function user_role( $operator, $value ) {
		if ( ! is_user_logged_in() ) {
			return $this->check_by_operator( $operator, $value, 'guest' );
		} else {
			$current_user = wp_get_current_user();
			if ( ! ( $current_user instanceof WP_User ) ) {
				return false;
			}

			return $this->check_by_operator( $operator, $value, $current_user->roles[0] );
		}
	}

	/**
	 * Get timestamp
	 *
	 * @param string $units Time units.
	 * @param mixed  $count Count of units.
	 *
	 * @return integer
	 */
	private function get_timestamp( $units, $count ) {
		if ( ! is_numeric( $count ) ) {
			return 0;
		}
		
		$count = (int) $count;
		
		switch ( $units ) {
			case 'seconds':
				return $count;
			case 'minutes':
				return $count * MINUTE_IN_SECONDS;
			case 'hours':
				return $count * HOUR_IN_SECONDS;
			case 'days':
				return $count * DAY_IN_SECONDS;
			case 'weeks':
				return $count * WEEK_IN_SECONDS;
			case 'months':
				return $count * MONTH_IN_SECONDS;
			case 'years':
				return $count * YEAR_IN_SECONDS;

			default:
				return $count;
		}
	}

	/**
	 * Get date timestamp
	 *
	 * @param mixed $value Date value.
	 *
	 * @return int|mixed Returns 0 on validation failure, timestamp calculation, or the original value
	 */
	public function get_date_timestamp( $value ) {
		if ( is_object( $value ) ) {
			if ( ! isset( $value->units ) || ! isset( $value->unitsCount ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				return 0;
			}
			$current_timestamp = current_datetime()->getTimestamp();
			return ( $current_timestamp - $this->get_timestamp( $value->units, $value->unitsCount ) ) * 1000; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		} else {
			return $value;
		}
	}

	/**
	 * The date when the user who views your website was registered.
	 * For unregistered users this date always equals to 1 Jan 1970.
	 *
	 * @param string $operator Comparison operator.
	 * @param mixed  $value  Comparison value (object for 'between', mixed otherwise).
	 *
	 * @return boolean
	 */
	private function user_registered( $operator, $value ) {
		if ( ! is_user_logged_in() ) {
			return false;
		} else {
			$user       = wp_get_current_user();
			$registered = strtotime( $user->data->user_registered );
			
			// Validate that we have a valid registration timestamp.
			if ( false === $registered ) {
				return false;
			}
			
			$registered = $registered * 1000;

			if ( 'equals' === $operator || 'notequal' === $operator ) {
				$registered = $registered / 1000;
				$timestamp  = $this->get_date_timestamp( $value );
				
				if ( ! $timestamp || $timestamp <= 0 ) {
					return false;
				}
				
				$timestamp = round( $timestamp / 1000 );

				return $this->check_by_operator( $operator, gmdate( 'Y-m-d', (int) $timestamp ), gmdate( 'Y-m-d', (int) $registered ) );
			} elseif ( 'between' === $operator ) {
				if ( ! is_object( $value ) || ! isset( $value->start ) || ! isset( $value->end ) ) {
					return false;
				}
				$start_timestamp = $this->get_date_timestamp( $value->start );
				$end_timestamp   = $this->get_date_timestamp( $value->end );
				
				if ( ! $start_timestamp || $start_timestamp <= 0 || ! $end_timestamp || $end_timestamp <= 0 ) {
					return false;
				}

				return $this->check_by_operator( $operator, $start_timestamp, $registered, $end_timestamp );
			} else {
				$timestamp = $this->get_date_timestamp( $value );
				
				if ( ! $timestamp || $timestamp <= 0 ) {
					return false;
				}

				return $this->check_by_operator( $operator, $timestamp, $registered );
			}
		}
	}

	/**
	 * Check the user views your website from mobile device or not
	 *
	 * @param string $operator Comparison operator.
	 * @param string $value Comparison value.
	 *
	 * @return boolean
	 *
	 * @link https://stackoverflow.com/a/4117597
	 */
	private function user_mobile( $operator, $value ) {
		$useragent = $_SERVER['HTTP_USER_AGENT'];

		if ( preg_match( '/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i', $useragent ) || preg_match( '/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i', substr( $useragent, 0, 4 ) ) ) {
			return $operator === 'equals' && $value === 'yes' || $operator === 'notequal' && $value === 'no';
		} else {
			return $operator === 'notequal' && $value === 'yes' || $operator === 'equals' && $value === 'no';
		}
	}

	/**
	 * Determines whether the user's browser has a cookie with a given name
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 */
	private function user_cookie_name( $operator, $value ) {
		if ( isset( $_COOKIE[ $value ] ) ) {
			return $operator === 'equals';
		} else {
			return $operator === 'notequal';
		}
	}

	/**
	 * A some selected page
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 */
	private function location_some_page( $operator, $value ) {
		$post_id = ( ! is_404() && ! is_search() && ! is_archive() && ! is_home() ) ? get_the_ID() : false;

		switch ( $value ) {
			case 'base_web':    // Basic - Entire Website
				$result = true;
				break;
			case 'base_sing':   // Basic - All Singulars
				$result = is_singular();
				break;
			case 'base_arch':   // Basic - All Archives
				$result = is_archive();
				break;
			case 'spec_404':    // Special Pages - 404 Page
				$result = is_404();
				break;
			case 'spec_search': // Special Pages - Search Page
				$result = is_search();
				break;
			case 'spec_blog':   // Special Pages - Blog / Posts Page
				$result = is_home();
				break;
			case 'spec_front':  // Special Pages - Front Page
				$result = is_front_page();
				break;
			case 'spec_date':   // Special Pages - Date Archive
				$result = is_date();
				break;
			case 'spec_auth':   // Special Pages - Author Archive
				$result = is_author();
				break;
			case 'post_all':    // Posts - All Posts
			case 'page_all':    // Pages - All Pages
				$result = false;
				if ( false !== $post_id ) {
					$post_type = 'post_all' == $value ? 'post' : 'page';
					$result    = $post_type == get_post_type( $post_id );
				}
				break;
			case 'post_arch':   // Posts - All Posts Archive
			case 'page_arch':   // Pages - All Pages Archive
				$result = false;
				if ( is_archive() ) {
					$post_type = 'post_arch' == $value ? 'post' : 'page';
					$result    = $post_type == get_post_type();
				}
				break;
			case 'post_cat':    // Posts - All Categories Archive
			case 'post_tag':    // Posts - All Tags Archive
				$result = false;
				if ( is_archive() && 'post' == get_post_type() ) {
					$taxonomy = 'post_tag' == $value ? 'post_tag' : 'category';
					$obj      = get_queried_object();

					$current_taxonomy = '';
					if ( '' !== $obj && null !== $obj ) {
						$current_taxonomy = $obj->taxonomy;
					}

					if ( $current_taxonomy == $taxonomy ) {
						$result = true;
					}
				}
				break;

			default:
				$result = false;
		}

		if ( WINP_Helper::is_woo_active() ) {
			switch ( $value ) {
				case 'woo_product':
					$result = is_product();
					break;
				case 'woo_arch':
					$result = is_shop();
					break;
				case 'woo_cart':
					$result = is_cart();
					break;
				case 'woo_checkout':
					$result = is_checkout();
					break;
				case 'woo_checkout_pay':
					$result = is_checkout_pay_page();
					break;
				case 'woo_cat':
					$result = is_product_category();
					break;
				case 'woo_tag':
					$result = is_product_tag();
					break;
			}
		}

		return $this->check_by_operator( $operator, $result, true );
	}

	/**
	 * An URL of the current page where a user who views your website is located
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 */
	private function location_page( $operator, $value ) {
		$url = $this->getCurrentUrl();

		return $url ? $this->check_by_operator( $operator, trim( $url, '/' ), trim( $value, '/' ) ) : false;
	}

	/**
	 * A referrer URL which has brought a user to the current page
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 */
	private function location_referrer( $operator, $value ) {
		$url = $this->getRefererUrl();

		return $url ? $this->check_by_operator( $operator, trim( $url, '/' ), trim( $value, '/' ) ) : false;
	}

	/**
	 * A post type of the current page
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 */
	private function location_post_type( $operator, $value ) {
		if ( is_singular() ) {
			return $this->check_by_operator( $operator, $value, get_post_type() );
		}

		return false;
	}

	/**
	 * A taxonomy page
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 * @since 2.2.8 The bug is fixed, the condition was not checked
	 *              for tachonomies, only posts.
	 */
	private function location_taxonomy( $operator, $value ) {
		$term_id = null;

		if ( is_tax() || is_tag() || is_category() ) {
			$term_id = get_queried_object()->term_id;

			if ( $term_id ) {
				return $this->check_by_operator( $operator, intval( $value ), $term_id );
			}
		}

		return false;
	}

	/**
	 * A taxonomy of the current page
	 *
	 * @param $operator
	 * @param $value
	 *
	 * @return boolean
	 * @since 2.4.0
	 */
	private function page_taxonomy( $operator, $value ) {
		$term_id = null;

		if ( is_singular() ) {
			$post_cat = get_the_category( get_the_ID() );
			if ( is_array( $post_cat ) ) {
				foreach ( $post_cat as $item ) {
					$term_id[] = $item->term_id;
				}
			}
		}

		if ( $term_id ) {
			return $this->check_by_operator( $operator, intval( $value ), $term_id );
		}

		return false;
	}
}
