<?php
/**
 * Scoped CSS for Visual Builder previews (library embed, popup REST inject, etc.).
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Includes;

if ( ! defined( 'ABSPATH' ) ) {
	die( 'Direct access forbidden.' );
}

/**
 * Mirrors the former private CSS scoping helpers from {@see DSM_Library_Layout_Preview_REST}.
 */
final class DsmCssPreviewScope {

	/**
	 * Scope all rules in a stylesheet under a root selector (@media-aware).
	 *
	 * @param string $css   CSS text.
	 * @param string $scope Root selector e.g. `.foo` or `#bar .baz` (no trailing combinator).
	 * @return string
	 */
	public static function apply( string $css, string $scope ): string {
		if ( '' === trim( $css ) || '' === trim( $scope ) ) {
			return $css;
		}
		return self::dsm_apply_preview_scope_to_css( $css, $scope );
	}

	/**
	 * Index of the matching closing `}` for a block opened at `$open_brace` (string- and comment-aware).
	 *
	 * @param string $css        Full stylesheet.
	 * @param int    $open_brace Index of `{`.
	 * @return int Index of `}` or last index if unbalanced.
	 */
	private static function dsm_css_matching_close_brace( string $css, int $open_brace ): int {
		$len        = strlen( $css );
		$depth      = 0;
		$in_string  = '';
		for ( $i = $open_brace; $i < $len; $i++ ) {
			$c = $css[ $i ];
			if ( '' !== $in_string ) {
				if ( '\\' === $c && $i + 1 < $len ) {
					++$i;
					continue;
				}
				if ( $c === $in_string ) {
					$in_string = '';
				}
				continue;
			}
			if ( '"' === $c || "'" === $c ) {
				$in_string = $c;
				continue;
			}
			if ( '/' === $c && isset( $css[ $i + 1 ] ) && '*' === $css[ $i + 1 ] ) {
				$end = strpos( $css, '*/', $i + 2 );
				if ( false === $end ) {
					return $len - 1;
				}
				$i = $end + 1;
				continue;
			}
			if ( '{' === $c ) {
				++$depth;
			} elseif ( '}' === $c ) {
				--$depth;
				if ( 0 === $depth ) {
					return $i;
				}
			}
		}
		return $len - 1;
	}

	/**
	 * First index of `{` at paren depth 0 outside strings/comments, starting at $start.
	 *
	 * @param string $css   Full stylesheet.
	 * @param int    $start Start offset.
	 * @return int|false
	 */
	private static function dsm_css_first_block_brace( string $css, int $start ) {
		$len       = strlen( $css );
		$depth     = 0;
		$in_string = '';
		for ( $i = $start; $i < $len; $i++ ) {
			$c = $css[ $i ];
			if ( '' !== $in_string ) {
				if ( '\\' === $c && $i + 1 < $len ) {
					++$i;
					continue;
				}
				if ( $c === $in_string ) {
					$in_string = '';
				}
				continue;
			}
			if ( '"' === $c || "'" === $c ) {
				$in_string = $c;
				continue;
			}
			if ( '/' === $c && isset( $css[ $i + 1 ] ) && '*' === $css[ $i + 1 ] ) {
				$end = strpos( $css, '*/', $i + 2 );
				if ( false === $end ) {
					return false;
				}
				$i = $end + 1;
				continue;
			}
			if ( '(' === $c ) {
				++$depth;
			} elseif ( ')' === $c ) {
				$depth = max( 0, $depth - 1 );
			} elseif ( '{' === $c && 0 === $depth ) {
				return $i;
			}
		}
		return false;
	}

	/**
	 * Advance index past whitespace and comments.
	 *
	 * @param string $css Full stylesheet.
	 * @param int    $i   Current index (by ref).
	 * @return void
	 */
	private static function dsm_css_skip_ws_comments( string $css, int &$i ): void {
		$len = strlen( $css );
		while ( $i < $len ) {
			if ( preg_match( '/\s/', $css[ $i ] ) ) {
				++$i;
				continue;
			}
			if ( '/' === $css[ $i ] && isset( $css[ $i + 1 ] ) && '*' === $css[ $i + 1 ] ) {
				$end = strpos( $css, '*/', $i + 2 );
				if ( false === $end ) {
					$i = $len;
					return;
				}
				$i = $end + 2;
				continue;
			}
			break;
		}
	}

	/**
	 * Split a selector list on commas not inside parentheses or strings.
	 *
	 * @param string $selectors Selector prelude.
	 * @return string[]
	 */
	private static function dsm_css_split_selector_list( string $selectors ): array {
		$selectors = trim( $selectors );
		if ( '' === $selectors ) {
			return array();
		}
		$out       = array();
		$buf       = '';
		$depth     = 0;
		$in_string = '';
		$len       = strlen( $selectors );
		for ( $i = 0; $i < $len; $i++ ) {
			$c = $selectors[ $i ];
			if ( '' !== $in_string ) {
				$buf .= $c;
				if ( '\\' === $c && $i + 1 < $len ) {
					$buf .= $selectors[ ++$i ];
					continue;
				}
				if ( $c === $in_string ) {
					$in_string = '';
				}
				continue;
			}
			if ( '"' === $c || "'" === $c ) {
				$buf .= $c;
				$in_string = $c;
				continue;
			}
			if ( '(' === $c ) {
				++$depth;
				$buf .= $c;
				continue;
			}
			if ( ')' === $c ) {
				$depth = max( 0, $depth - 1 );
				$buf .= $c;
				continue;
			}
			if ( ',' === $c && 0 === $depth ) {
				$t = trim( $buf );
				if ( '' !== $t ) {
					$out[] = $t;
				}
				$buf = '';
				continue;
			}
			$buf .= $c;
		}
		$t = trim( $buf );
		if ( '' !== $t ) {
			$out[] = $t;
		}
		return $out;
	}

	/**
	 * Remove Divi frontend / Theme Builder selector prefixes that are absent from library preview HTML.
	 *
	 * Preview markup is layout fragments inside {@see LibraryLayoutRenderer}; it does not include
	 * `.et-l--post > .et_builder_inner_content` or `.et-db #et-boc .et-l` wrappers, so scoped rules
	 * would never match if those segments stayed in the selector.
	 *
	 * @param string $selector Single selector (no comma list).
	 * @return string
	 */
	private static function dsm_normalize_library_preview_selector_fragment( string $selector ): string {
		$s   = trim( $selector );
		$max = 8;
		while ( $max-- > 0 && '' !== $s ) {
			$before = $s;

			$s = preg_replace(
				'/^\.et-db\s+#et-boc\s+\.et-l\s+\.et_pb_posts\s+\.et_pb_post\s+/i',
				'',
				$s,
				1
			);
			$s = preg_replace( '/^\.et-db\s+#et-boc\s+\.et-l\s+/i', '', $s, 1 );
			$s = preg_replace( '/^body\s+#page-container\s+/i', '', $s, 1 );
			$s = preg_replace( '/^body\.et-db\s+#page-container\s+/i', '', $s, 1 );
			$s = preg_replace(
				'/^\.et-l--(?:post|header|body|footer)(?:--[a-zA-Z0-9_-]+)?\s*>\s*\.et_builder_inner_content\s+/i',
				'',
				$s,
				1
			);
			$s = preg_replace(
				'/^\.et-l--(?:post|header|body|footer)(?:--[a-zA-Z0-9_-]+)?\s+\.et_builder_inner_content\s+/i',
				'',
				$s,
				1
			);
			$s = preg_replace( '/^\.et_builder_inner_content\s*>\s*/i', '', $s, 1 );
			$s = preg_replace( '/^\.et_builder_inner_content\s+/i', '', $s, 1 );

			if ( ! is_string( $s ) ) {
				$s = $before;
				break;
			}
			$s = trim( $s );
			if ( $before === $s ) {
				break;
			}
		}

		return trim( $s );
	}

	/**
	 * Prefix each selector in a list with the library preview root (comma-safe).
	 *
	 * @param string $selectors Selector prelude.
	 * @param string $scope   `.dsm-library-layout-renderer[data-dsm-library-preview="…"]`.
	 * @return string
	 */
	private static function dsm_scope_css_selector_prelude( string $selectors, string $scope ): string {
		$parts = self::dsm_css_split_selector_list( $selectors );
		if ( empty( $parts ) ) {
			return $selectors;
		}
		$prefixed = array();
		foreach ( $parts as $part ) {
			$normalized = self::dsm_normalize_library_preview_selector_fragment( $part );
			if ( '' === $normalized ) {
				$normalized = trim( $part );
			}
			// Mirrors {@see dsm_build_scoped_library_preview_selector()} — avoid `..class` prelude.
			if ( preg_match( '/^\.\.+/', $normalized ) ) {
				$normalized = (string) preg_replace( '/^\.\.+/', '.', $normalized, 1 );
			}
			$prefixed[] = $scope . ' ' . $normalized;
		}
		return implode( ', ', $prefixed );
	}

	/**
	 * Whether an at-keyword should keep its block unmodified (keyframes, font-face, etc.).
	 *
	 * @param string $keyword Lowercased at-keyword without `@`.
	 * @return bool
	 */
	private static function dsm_css_at_rule_is_verbatim_block( string $keyword ): bool {
		static $verbatim = null;
		if ( null === $verbatim ) {
			$verbatim = array(
				'keyframes'        => true,
				'-webkit-keyframes' => true,
				'-moz-keyframes'   => true,
				'-o-keyframes'     => true,
				'font-face'        => true,
				'page'             => true,
			);
		}
		return isset( $verbatim[ $keyword ] );
	}

	/**
	 * Extract lowercased at-keyword after `@` starting at $i.
	 *
	 * @param string $css Full stylesheet.
	 * @param int    $i   Index of `@` (by ref, advanced past keyword).
	 * @return string
	 */
	private static function dsm_css_parse_at_keyword( string $css, int &$i ): string {
		++$i;
		$start = $i;
		$len   = strlen( $css );
		while ( $i < $len && preg_match( '/[a-zA-Z0-9_-]/', $css[ $i ] ) ) {
			++$i;
		}
		return strtolower( substr( $css, $start, $i - $start ) );
	}

	/**
	 * Scope all qualified rules in a CSS string under the preview wrapper (handles @media / @supports).
	 *
	 * @param string $css   Rules region.
	 * @param string $scope `.dsm-library-layout-renderer[data-dsm-library-preview="…"]`.
	 * @return string
	 */
	private static function dsm_apply_preview_scope_to_css( string $css, string $scope ): string {
		$len = strlen( $css );
		$out = '';
		$i   = 0;
		while ( $i < $len ) {
			self::dsm_css_skip_ws_comments( $css, $i );
			if ( $i >= $len ) {
				break;
			}
			if ( '@' === $css[ $i ] ) {
				$at_start = $i;
				$kw       = self::dsm_css_parse_at_keyword( $css, $i );
				self::dsm_css_skip_ws_comments( $css, $i );
				if ( 'import' === $kw || 'charset' === $kw ) {
					while ( $i < $len ) {
						$c = $css[ $i ];
						if ( '\\' === $c && $i + 1 < $len ) {
							$i += 2;
							continue;
						}
						if ( '"' === $c || "'" === $c ) {
							$q = $c;
							++$i;
							while ( $i < $len ) {
								if ( '\\' === $css[ $i ] && $i + 1 < $len ) {
									$i += 2;
									continue;
								}
								if ( $css[ $i ] === $q ) {
									++$i;
									break;
								}
								++$i;
							}
							continue;
						}
						if ( ';' === $c ) {
							$out .= substr( $css, $at_start, $i - $at_start + 1 );
							++$i;
							continue 2;
						}
						++$i;
					}
					$out .= substr( $css, $at_start );
					break;
				}
				$brace_open = self::dsm_css_first_block_brace( $css, $at_start );
				if ( false === $brace_open ) {
					$out .= substr( $css, $at_start );
					break;
				}
				$close = self::dsm_css_matching_close_brace( $css, $brace_open );
				$full  = substr( $css, $at_start, $close - $at_start + 1 );
				if ( self::dsm_css_at_rule_is_verbatim_block( $kw ) ) {
					$out .= $full;
				} else {
					$inner    = substr( $css, $brace_open + 1, $close - $brace_open - 1 );
					$prelude  = trim( substr( $css, $at_start, $brace_open - $at_start ) );
					$out     .= $prelude . '{' . self::dsm_apply_preview_scope_to_css( $inner, $scope ) . '}';
				}
				$i = $close + 1;
				continue;
			}
			$brace_open = self::dsm_css_first_block_brace( $css, $i );
			if ( false === $brace_open ) {
				$out .= substr( $css, $i );
				break;
			}
			$prelude = trim( substr( $css, $i, $brace_open - $i ) );
			$close   = self::dsm_css_matching_close_brace( $css, $brace_open );
			$inner   = substr( $css, $brace_open + 1, $close - $brace_open - 1 );
			if ( '' !== $prelude ) {
				$out .= self::dsm_scope_css_selector_prelude( $prelude, $scope ) . '{' . self::dsm_apply_preview_scope_to_css( $inner, $scope ) . '}';
			} else {
				$out .= '{' . self::dsm_apply_preview_scope_to_css( $inner, $scope ) . '}';
			}
			$i = $close + 1;
		}
		return $out;
	}

}
