<?php
/**
 *  Common logic for CSS handling class.
 *
 * @package ThemeIsle\GutenbergBlocks
 */

namespace ThemeIsle\GutenbergBlocks;

use Sabberworm\CSS\Parser;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\CSSList\AtRuleBlockList;
use Sabberworm\CSS\CSSList\KeyFrame;

/**
 * Class Base_CSS
 */
class Base_CSS {

	/**
	 * The namespace under which the blocks are registered.
	 *
	 * @var string
	 */
	protected $library_prefix = 'themeisle-blocks';

	/**
	 * Rest route namespace.
	 *
	 * @var string
	 */
	public $namespace = 'otter/';

	/**
	 * Rest route version.
	 *
	 * @var string
	 */
	public $version = 'v1';

	/**
	 * The namespace under which the block classees are saved.
	 *
	 * @var array
	 */
	protected static $blocks_classes = array();

	/**
	 * The namespace under which the fonts are saved.
	 *
	 * @var array
	 */
	protected static $google_fonts = array();

	/**
	 * Base_CSS constructor.
	 *
	 * @since   1.3.0
	 * @access  public
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'autoload_block_classes' ), 99 );
	}

	/**
	 * Autoload classes for each block.
	 *
	 * @since   1.3.0
	 * @access  public
	 */
	public function autoload_block_classes() {
		self::$blocks_classes = array(
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Accordion_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Advanced_Column_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Advanced_Columns_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Advanced_Heading_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Button_Group_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Button_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Circle_Counter_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Countdown_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Core_Image_Plugin_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Font_Awesome_Icons_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Google_Map_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Icon_List_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Icon_List_Item_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Leaflet_Map_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_Input_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_Textarea_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_Multiple_Choice_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Flip_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Progress_Bar_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Popup_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Slider_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Review_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Tabs_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Posts_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Sharing_Icons_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Timeline_CSS',
			'\ThemeIsle\GutenbergBlocks\CSS\Blocks\Timeline_Item_CSS',
		);

		self::$blocks_classes = apply_filters( 'otter_blocks_register_css', self::$blocks_classes );
	}

	/**
	 * Check if string is empty without accepting zero
	 *
	 * @param string $value Var to check.
	 *
	 * @return bool
	 * @since   1.3.1
	 * @access  public
	 */
	public function is_empty( $value ) {
		return empty( $value ) && 0 !== $value;
	}

	/**
	 * Get block attribute value with default
	 *
	 * @param mixed $attr Attributes.
	 * @param mixed $default_value Default value.
	 *
	 * @return mixed
	 * @since   1.3.0
	 * @access  public
	 */
	public function get_attr_value( $attr, $default_value = 'unset' ) {
		if ( ! $this->is_empty( $attr ) ) {
			return $attr;
		} else {
			return $default_value;
		}
	}

	/**
	 * Get Google Fonts
	 *
	 * @param array $attr Attr values.
	 *
	 * @since   1.3.0
	 * @access  public
	 */
	public function get_google_fonts( $attr ) {
		if ( isset( $attr['fontFamily'] ) ) {
			if ( ! array_key_exists( $attr['fontFamily'], self::$google_fonts ) ) {
				self::$google_fonts[ $attr['fontFamily'] ] = array(
					'fontfamily'  => $attr['fontFamily'],
					'fontvariant' => ( isset( $attr['fontVariant'] ) && ! empty( $attr['fontVariant'] ) ? array( $attr['fontVariant'] ) : array() ),
				);
			} elseif ( isset( $attr['fontVariant'] ) && ! empty( $attr['fontVariant'] ) ) {
				$font_variant = $attr['fontVariant'];
				if ( ! in_array( $font_variant, self::$google_fonts[ $attr['fontFamily'] ]['fontvariant'], true ) ) {
					self::$google_fonts[ $attr['fontFamily'] ]['fontvariant'][] = $font_variant;
				}
			}
		}
	}

	/**
	 * Convert HEX to RGBA.
	 *
	 * @param string   $color Color data.
	 * @param bool|int $opacity Opacity status.
	 *
	 * @return mixed
	 * @since   1.3.0
	 * @access  public
	 */
	public static function hex2rgba( $color, $opacity = false ) {
		$default = 'rgb(0,0,0)';

		if ( empty( $color ) ) {
			return $default;
		}

		if ( '#' == $color[0] ) {
			$color = substr( $color, 1 );
		}

		if ( strlen( $color ) == 6 ) {
			$hex = array( $color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5] );
		} elseif ( strlen( $color ) == 3 ) {
			$hex = array( $color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2] );
		} else {
			return $default;
		}

		$rgb = array_map( 'hexdec', $hex );

		if ( $opacity >= 0 ) {
			if ( abs( $opacity ) > 1 ) {
				$opacity = 1.0;
			}
			$output = 'rgba( ' . implode( ',', $rgb ) . ',' . $opacity . ' )';
		} else {
			$output = 'rgb( ' . implode( ',', $rgb ) . ' )';
		}

		return $output;
	}

	/**
	 * Convert a color slug to a CSS variable reference.
	 * WordPress generates CSS variables in the format: --wp--preset--color--{slug}
	 *
	 * @param string|null $slug The color slug.
	 * @return string|null The CSS variable reference.
	 * @since   3.1.5
	 * @access  public
	 */
	public static function get_color_css_variable( $slug ) {
		if ( empty( $slug ) ) {
			return $slug;
		}

		// If it's already a color value or CSS variable, return as-is.
		if (
			strpos( $slug, '#' ) === 0 ||
			strpos( $slug, 'rgb' ) === 0 ||
			strpos( $slug, 'hsl' ) === 0 ||
			strpos( $slug, 'var(' ) === 0
		) {
			return $slug;
		}

		// Sanitize slug: WordPress slugs should only contain lowercase alphanumeric, hyphens, and underscores.
		// This prevents potential CSS injection if slug comes from untrusted sources.
		$sanitized_slug = strtolower( preg_replace( '/[^a-z0-9-_]/', '', $slug ) );

		// Convert slug to CSS variable.
		return 'var(--wp--preset--color--' . $sanitized_slug . ')';
	}

	/**
	 * Resolve a color value which may be a slug from the theme color palette.
	 * This function converts slugs to CSS variables to preserve the connection to theme.json.
	 * If the value is a slug, it returns a CSS variable reference.
	 * Otherwise, returns the value as-is (for hex, rgb, hsl values).
	 *
	 * @param string|null $value The color value or slug.
	 * @return string|null The CSS variable or color value.
	 * @since   3.1.5
	 * @access  public
	 */
	public static function resolve_color_value( $value ) {
		// Use CSS variable conversion for slugs.
		return self::get_color_css_variable( $value );
	}

	/**
	 * Get Blocks CSS
	 *
	 * @param int $post_id Post id.
	 * @return string|void
	 * @since   1.3.0
	 * @access  public
	 */
	public function get_blocks_css( $post_id ) {
		if ( ! function_exists( 'has_blocks' ) ) {
			return;
		}

		$content = get_post_field( 'post_content', $post_id );
		$blocks  = parse_blocks( $content );

		if ( ! is_array( $blocks ) || empty( $blocks ) ) {
			return;
		}

		$animations = boolval( preg_match( '/\banimated\b/', $content ) );

		return $this->cycle_through_static_blocks( $blocks, $animations );
	}

	/**
	 * Get Widgets CSS
	 *
	 * @return string|void
	 * @since   1.7.0
	 * @access  public
	 */
	public function get_widgets_css() {
		if ( ! function_exists( 'has_blocks' ) ) {
			return;
		}

		$content = Registration::get_active_widgets_content();

		$blocks = parse_blocks( $content );

		if ( ! is_array( $blocks ) || empty( $blocks ) ) {
			return '';
		}

		$animations = boolval( preg_match( '/\banimated\b/', $content ) );

		return $this->cycle_through_static_blocks( $blocks, $animations );
	}

	/**
	 * Get Reusable Blocks CSS
	 *
	 * @param int $post_id Post id.
	 * @return string|void
	 * @since   1.3.0
	 * @access  public
	 */
	public function get_reusable_block_css( $post_id ) {
		$reusable_block = get_post( $post_id );

		if ( ! $reusable_block || 'wp_block' !== $reusable_block->post_type ) {
			return;
		}

		if ( 'publish' !== $reusable_block->post_status || ! empty( $reusable_block->post_password ) ) {
			return;
		}

		$blocks     = parse_blocks( $reusable_block->post_content );
		$animations = boolval( preg_match( '/\banimated\b/', $reusable_block->post_content ) );

		return $this->cycle_through_static_blocks( $blocks, $animations );
	}

	/**
	 * Cycle thorugh Static Blocks
	 *
	 * @param array $blocks List of blocks.
	 * @param bool  $animations To check for animations or not.
	 *
	 * @return string Style.
	 * @since   1.3.0
	 * @access  public
	 */
	public function cycle_through_static_blocks( $blocks, $animations = true ) {
		$style = '';

		foreach ( $blocks as $block ) {
			foreach ( self::$blocks_classes as $classname ) {
				$path = new $classname();

				if ( method_exists( $path, 'render_css' ) && isset( $path->block_prefix ) ) {
					if ( ( isset( $path->library_prefix ) ? $path->library_prefix : $this->library_prefix ) . '/' . $path->block_prefix === $block['blockName'] ) {
						$style .= $path->render_css( $block );
					}
				}
			}

			$custom_css = apply_filters( 'otter_blocks_css', $block );

			if ( is_string( $custom_css ) ) {
				$style .= $custom_css;
			}

			if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$style .= $this->cycle_through_static_blocks( $block['innerBlocks'], false );
			}
		}

		if ( true === $animations && class_exists( '\ThemeIsle\GutenbergBlocks\Blocks_Animation' ) && get_option( 'themeisle_blocks_settings_blocks_animation', true ) && get_option( 'themeisle_blocks_settings_optimize_animations_css', true ) ) {
			$style .= $this->get_animation_css( $blocks );
		}

		return $style;
	}

	/**
	 * Get Animation CSS
	 *
	 * @param array $blocks List of blocks.
	 *
	 * @return string CSS.
	 * @since   2.0.10
	 * @access  public
	 */
	public function get_animation_css( $blocks ) {
		$style   = '';
		$classes = $this->get_animation_classes( $blocks );

		if ( 0 === count( $classes ) ) {
			return $style;
		}

		$prepared_classes = array( ':root' );

		foreach ( $classes as $class ) {
			array_push( $prepared_classes, '.' . $class, '.animated.' . $class );
		}

		$classes = $prepared_classes;

		$content = get_transient( 'otter_animations_parsed' );

		if ( false === $content ) {
			$parser = null;
			if ( function_exists( 'wpcom_vip_file_get_contents' ) ) {
				$parser = new Parser( wpcom_vip_file_get_contents( OTTER_BLOCKS_PATH . '/build/animation/index.css' ) );
			} else {
				$parser = new Parser( file_get_contents( OTTER_BLOCKS_PATH . '/build/animation/index.css' ) ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
			}

			$content = $parser->parse()->getContents();

			set_transient( 'otter_animations_parsed', $content, MONTH_IN_SECONDS );
		}

		$format = OutputFormat::createCompact();

		foreach ( $content as $rule ) {
			if ( $rule instanceof DeclarationBlock ) {
				foreach ( $rule->getSelectors() as $selector ) {
					if ( in_array( $selector, $classes ) ) {
						$style .= $rule->render( $format );
					}
				}
				continue;
			}

			/*
			 * This is used to get the reduced-motion animation styles.
			 */
			if ( $rule instanceof AtRuleBlockList ) {
				if ( false === in_array( $rule->atRuleArgs(), array( '(prefers-reduced-motion:reduce),print', 'screen' ) ) ) {
					continue;
				}

				$style .= $rule->render( $format );
				continue;
			}

			/*
			 * This is used to get actual animation which is a @keyframe.
			 */
			if ( $rule instanceof KeyFrame ) {
				if ( in_array( '.' . $rule->getAnimationName(), $classes ) ) {
					$style .= $rule->render( $format );
				}
				continue;
			}
		}

		return $style;
	}

	/**
	 * Get Animation Classes
	 *
	 * @param array $blocks List of blocks.
	 *
	 * @return array CSS classes.
	 * @since   2.0.10
	 * @access  public
	 */
	public function get_animation_classes( $blocks ) {
		$classes = array();

		$allowed_classes = array(
			'animated',
			'bounce',
			'flash',
			'pulse',
			'rubberBand',
			'shakeX',
			'shakeY',
			'headShake',
			'swing',
			'tada',
			'wobble',
			'jello',
			'heartBeat',
			'hinge',
			'jackInTheBox',
			'backInDown',
			'backInLeft',
			'backInRight',
			'backInUp',
			'backOutDown',
			'backOutLeft',
			'backOutRight',
			'backOutUp',
			'bounceIn',
			'bounceInDown',
			'bounceInLeft',
			'bounceInRight',
			'bounceInUp',
			'bounceOut',
			'bounceOutDown',
			'bounceOutLeft',
			'bounceOutRight',
			'bounceOutUp',
			'fadeIn',
			'fadeInDown',
			'fadeInDownBig',
			'fadeInLeft',
			'fadeInLeftBig',
			'fadeInRight',
			'fadeInRightBig',
			'fadeInUp',
			'fadeInTopLeft',
			'fadeInTopRight',
			'fadeInBottomLeft',
			'fadeInBottomRight',
			'fadeOut',
			'fadeOutDown',
			'fadeOutDownBig',
			'fadeOutLeft',
			'fadeOutLeftBig',
			'fadeOutRight',
			'fadeOutRightBig',
			'fadeOutUp',
			'fadeOutUpBig',
			'fadeOutTopLeft',
			'fadeOutTopRight',
			'fadeOutBottomRight',
			'fadeOutBottomLeft',
			'flip',
			'flipInX',
			'flipInY',
			'flipOutX',
			'flipOutY',
			'lightSpeedInRight',
			'lightSpeedInLeft',
			'lightSpeedOutRight',
			'lightSpeedOutLeft',
			'rotateIn',
			'rotateInDownLeft',
			'rotateInDownRight',
			'rotateInUpLeft',
			'rotateInUpRight',
			'rotateOut',
			'rotateOutDownLeft',
			'rotateOutDownRight',
			'rotateOutUpLeft',
			'rotateOutUpRight',
			'slideInDown',
			'slideInLeft',
			'slideInRight',
			'slideInUp',
			'slideOutDown',
			'slideOutLeft',
			'slideOutRight',
			'slideOutUp',
			'zoomIn',
			'zoomInDown',
			'zoomInLeft',
			'zoomInRight',
			'zoomInUp',
			'zoomOut',
			'zoomOutDown',
			'zoomOutLeft',
			'zoomOutRight',
			'zoomOutUp',
			'rollIn',
			'rollOut',
			'backOutDown',
			'backOutLeft',
			'backOutRight',
			'backOutUp',
			'bounceOut',
			'bounceOutDown',
			'bounceOutLeft',
			'bounceOutRight',
			'bounceOutUp',
			'fadeOut',
			'fadeOutDown',
			'fadeOutDownBig',
			'fadeOutLeft',
			'fadeOutLeftBig',
			'fadeOutRight',
			'fadeOutRightBig',
			'fadeOutUp',
			'fadeOutUpBig',
			'fadeOutTopLeft',
			'fadeOutTopRight',
			'fadeOutBottomRight',
			'fadeOutBottomLeft',
			'flipOutX',
			'flipOutY',
			'lightSpeedOutRight',
			'lightSpeedOutLeft',
			'rotateOut',
			'rotateOutDownLeft',
			'rotateOutDownRight',
			'rotateOutUpLeft',
			'rotateOutUpRight',
			'slideOutDown',
			'slideOutLeft',
			'slideOutRight',
			'slideOutUp',
			'zoomOut',
			'zoomOutDown',
			'zoomOutLeft',
			'zoomOutRight',
			'zoomOutUp',
			'rollOut',
			'delay-100ms',
			'delay-200ms',
			'delay-500ms',
			'delay-1s',
			'delay-2s',
			'delay-3s',
			'delay-4s',
			'delay-5s',
			'slow',
			'slower',
			'fast',
			'faster',
		);

		foreach ( $blocks as $block ) {
			if ( isset( $block['attrs']['className'] ) && ! empty( $block['attrs']['className'] ) ) {
				if ( preg_match( '/\banimated\b/', $block['attrs']['className'] ) ) {
					$classes = array_merge( $classes, explode( ' ', trim( $block['attrs']['className'] ) ) );
				}
			}

			if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$classes = array_merge( $classes, $this->get_animation_classes( $block['innerBlocks'] ) );
			}
		}

		$classes = array_intersect( array_unique( $classes ), $allowed_classes );

		return $classes;
	}

	/**
	 * Cycle thorugh Global Styles
	 *
	 * @return string Style.
	 * @since   2.0.0
	 * @access  public
	 */
	public function cycle_through_global_styles() {
		$style = '';
		foreach ( self::$blocks_classes as $classname ) {
			$path = new $classname();

			if ( method_exists( $path, 'render_global_css' ) ) {
				$style .= $path->render_global_css();
			}
		}

		return $style;
	}

	/**
	 * Cycle thorugh Reusable Blocks
	 *
	 * @param array $blocks List of blocks.
	 *
	 * @return string Style.
	 * @since   1.3.0
	 * @access  public
	 */
	public function cycle_through_reusable_blocks( $blocks ) {
		$style = '';

		foreach ( $blocks as $block ) {
			if ( 'core/block' === $block['blockName'] && ! empty( $block['attrs']['ref'] ) ) {
				$style .= $this->get_reusable_block_css( $block['attrs']['ref'] );
			}

			if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$style .= $this->cycle_through_reusable_blocks( $block['innerBlocks'] );
			}
		}

		return $style;
	}

	/**
	 * Check if an url points to an image by checking if the an image extension exists.
	 *
	 * @param string $url The url.
	 *
	 * @return bool
	 * @since   1.4.4
	 * @access  public
	 */
	public static function is_image_url( $url ) {
		return is_string( $url ) && ( preg_match( '/\.(jpeg|jpg|png|gif|svg|bmp|ico|tiff|webp)$/i', $url ) || preg_match( '/\/dynamic\/?.[^"]*/i', $url ) );
	}

	/**
	 * Method to return path to child class in a Reflective Way.
	 *
	 * @return  string
	 * @since   1.3.0
	 * @access  protected
	 */
	protected function get_dir() {
		return __DIR__;
	}

	/**
	 * Throw error on object clone
	 *
	 * The whole idea of the singleton design pattern is that there is a single
	 * object therefore, we don't want the object to be cloned.
	 *
	 * @access  public
	 * @return  void
	 * @since   1.3.0
	 */
	public function __clone() {
		// Cloning instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, 'Cheatin&#8217; huh?', '1.0.0' );
	}

	/**
	 * Disable unserializing of the class
	 *
	 * @access  public
	 * @return  void
	 * @since   1.3.0
	 */
	public function __wakeup() {
		// Unserializing instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, 'Cheatin&#8217; huh?', '1.0.0' );
	}
}
