<?php
/**
 * Sanitize rich-text HTML constrained by field allowedCommand lists.
 *
 * @package JOCBM
 */

namespace JOCBM\Services;

defined( 'ABSPATH' ) || exit;

/**
 * Mirrors src/shared/allowedHtml.js for server-side storage / render.
 */
final class AllowedHtml {

	/**
	 * Default title / header commands.
	 *
	 * @var array<int, string>
	 */
	public const TITLE_COMMANDS = array(
		'heading',
		'fontweight',
		'italic',
		'underline',
		'strikethrough',
		'color',
		'fontfamily',
		'removeformat',
	);

	/**
	 * Tags that are never kept.
	 *
	 * @var array<int, string>
	 */
	private const DROP_TAGS = array(
		'script',
		'iframe',
		'object',
		'embed',
		'img',
		'video',
		'audio',
		'source',
		'picture',
		'svg',
		'canvas',
		'form',
		'input',
		'button',
		'textarea',
		'select',
		'option',
		'link',
		'meta',
		'base',
		'style',
		'noscript',
		'template',
		'math',
		'frame',
		'frameset',
		'applet',
		'param',
		'track',
		'map',
		'area',
		'portal',
		'marquee',
		'plaintext',
		'xmp',
		'listing',
		'keygen',
		'isindex',
		'dialog',
		'webview',
		'foreignobject',
	);

	/**
	 * Normalize allowedCommand from field schema.
	 *
	 * @param mixed $raw Raw allowedCommand / allowedCommands.
	 * @return array<int, string>|null Null = full rich (still no media/scripts).
	 */
	public static function normalize_commands( $raw ): ?array {
		if ( null === $raw ) {
			return null;
		}
		if ( is_string( $raw ) ) {
			$raw = preg_split( '/[\s,|]+/', $raw ) ?: array();
		}
		if ( ! is_array( $raw ) ) {
			return null;
		}
		$out  = array();
		$seen = array();
		foreach ( $raw as $item ) {
			$cmd = strtolower( trim( (string) $item ) );
			$cmd = preg_replace( '/^mce_/', '', $cmd ) ?? $cmd;
			if ( '' === $cmd ) {
				continue;
			}
			if ( 'strike' === $cmd ) {
				$cmd = 'strikethrough';
			} elseif ( in_array( $cmd, array( 'forecolor', 'textcolor' ), true ) ) {
				$cmd = 'color';
			} elseif ( in_array( $cmd, array( 'font', 'font-family', 'fontfamily' ), true ) ) {
				$cmd = 'fontfamily';
			} elseif ( in_array( $cmd, array( 'font-size', 'fontsize', 'size' ), true ) ) {
				$cmd = 'fontsize';
			} elseif ( in_array( $cmd, array( 'font-weight', 'fontweight', 'weight', 'bold' ), true ) ) {
				$cmd = 'fontweight';
			} elseif ( in_array( $cmd, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'headings', 'formatblock' ), true ) ) {
				$cmd = 'heading';
			} elseif ( 'ul' === $cmd ) {
				$cmd = 'bullist';
			} elseif ( 'ol' === $cmd ) {
				$cmd = 'numlist';
			} elseif ( in_array( $cmd, array( 'listitem', 'list-item', 'li' ), true ) ) {
				$cmd = 'listitem';
			}
			if ( isset( $seen[ $cmd ] ) ) {
				continue;
			}
			$seen[ $cmd ] = true;
			$out[]          = $cmd;
		}
		return $out;
	}

	/**
	 * Read allowedCommand from a field definition.
	 *
	 * @param array<string, mixed> $field Field schema.
	 * @return array<int, string>|null
	 */
	public static function commands_from_field( array $field ): ?array {
		$raw = $field['allowedCommand'] ?? $field['allowedCommands'] ?? $field['allowed_commands'] ?? null;
		return self::normalize_commands( $raw );
	}

	/**
	 * Tag allowlist for commands.
	 *
	 * @param array<int, string>|null $commands Commands.
	 * @return array{tags: array<string, true>, color: bool, link: bool, align: bool, fontfamily: bool, fontsize: bool, fontweight: bool}
	 */
	public static function tags_from_commands( ?array $commands ): array {
		$map = array(
			'bold'          => array( 'strong', 'b' ),
			'italic'        => array( 'em', 'i' ),
			'underline'     => array( 'u' ),
			'strikethrough' => array( 's', 'strike', 'del' ),
			'color'         => array( 'span' ),
			'heading'       => array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ),
			'fontfamily'    => array( 'span' ),
			'fontsize'      => array( 'span' ),
			'fontweight'    => array( 'span', 'strong', 'b' ),
			'bullist'       => array( 'ul', 'li' ),
			'numlist'       => array( 'ol', 'li' ),
			'listitem'      => array( 'li' ),
			'blockquote'    => array( 'blockquote' ),
			'link'          => array( 'a' ),
			'subscript'     => array( 'sub' ),
			'superscript'   => array( 'sup' ),
			'hr'            => array( 'hr' ),
			// Align does not add tags; text-align may be kept on span.
			'alignleft'     => array(),
			'aligncenter'   => array(),
			'alignright'    => array(),
		);

		$cmds = null === $commands
			? array_keys( $map )
			: $commands;

		$tags       = array( 'br' => true );
		$color      = false;
		$link       = false;
		$align      = false;
		$fontfamily = false;
		$fontsize   = false;
		$fontweight = false;
		$lineheight = false;
		$letterspacing = false;

		foreach ( $cmds as $cmd ) {
			foreach ( $map[ $cmd ] ?? array() as $tag ) {
				$tags[ $tag ] = true;
			}
			if ( 'color' === $cmd ) {
				$color = true;
			}
			if ( 'fontfamily' === $cmd ) {
				$fontfamily   = true;
				$tags['span'] = true;
			}
			if ( 'fontsize' === $cmd ) {
				$fontsize     = true;
				$tags['span'] = true;
			}
			if ( 'fontweight' === $cmd ) {
				$fontweight   = true;
				$tags['span'] = true;
			}
			if ( 'link' === $cmd ) {
				$link = true;
			}
			if ( in_array( $cmd, array( 'alignleft', 'aligncenter', 'alignright' ), true ) ) {
				$align = true;
			}
		}

		// Do not allow <p>/<div> — match JS sanitizer (unwrap to <br> client-side).
		if ( null === $commands ) {
			$fontfamily    = true;
			$fontsize      = true;
			$fontweight    = true;
			$lineheight    = true;
			$letterspacing = true;
			$tags['span']  = true;
		}

		if ( $fontfamily || $fontsize || $fontweight ) {
			$lineheight    = true;
			$letterspacing = true;
			$tags['span']  = true;
		}

		return array(
			'tags'          => $tags,
			'color'         => $color,
			'link'          => $link,
			'align'         => $align,
			'fontfamily'    => $fontfamily,
			'fontsize'      => $fontsize,
			'fontweight'    => $fontweight,
			'lineheight'    => $lineheight,
			'letterspacing' => $letterspacing,
		);
	}

	/**
	 * Sanitize HTML for storage / output.
	 *
	 * @param string                  $html                Raw HTML.
	 * @param array<int, string>|null $commands            Allowed commands (null = rich default).
	 * @param bool                    $allow_inline_styles When false, never emit style attributes (paste).
	 * @return string
	 */
	public static function sanitize( string $html, ?array $commands = null, bool $allow_inline_styles = true ): string {
		$html = trim( $html );
		if ( '' === $html ) {
			return '';
		}

		$html = str_replace( "\0", '', $html );

		// Strip comments and CDATA early.
		$html = preg_replace( '/<!--.*?-->/s', '', $html ) ?? $html;
		$html = preg_replace( '/<!\[CDATA\[.*?\]\]>/s', '', $html ) ?? $html;
		$html = preg_replace( '/<!\[if[\s\S]*?<!\[endif\]>/i', '', $html ) ?? $html;

		$cfg = self::tags_from_commands( $commands );

		// Color may be authored on strong/em/etc. (library samples); keep style so import matches preview.
		$color_style_tags = array( 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'del' );

		$allowed = array();
		foreach ( array_keys( $cfg['tags'] ) as $tag ) {
			if ( in_array( $tag, self::DROP_TAGS, true ) ) {
				continue;
			}
			if ( 'br' === $tag || 'hr' === $tag ) {
				$allowed[ $tag ] = array();
				continue;
			}
			if (
				$allow_inline_styles &&
				'span' === $tag &&
				(
					! empty( $cfg['color'] ) ||
					! empty( $cfg['fontfamily'] ) ||
					! empty( $cfg['fontsize'] ) ||
					! empty( $cfg['fontweight'] ) ||
					! empty( $cfg['lineheight'] ) ||
					! empty( $cfg['letterspacing'] ) ||
					! empty( $cfg['align'] )
				)
			) {
				$allowed['span'] = array( 'style' => true );
				continue;
			}
			if (
				$allow_inline_styles &&
				! empty( $cfg['color'] ) &&
				in_array( $tag, $color_style_tags, true )
			) {
				$allowed[ $tag ] = array( 'style' => true );
				continue;
			}
			if (
				$allow_inline_styles &&
				in_array( $tag, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ), true ) &&
				( ! empty( $cfg['lineheight'] ) || ! empty( $cfg['letterspacing'] ) )
			) {
				$allowed[ $tag ] = array( 'style' => true );
				continue;
			}
			if ( 'a' === $tag && $cfg['link'] ) {
				$allowed['a'] = array(
					'href'   => true,
					'target' => true,
					'rel'    => true,
				);
				continue;
			}
			$allowed[ $tag ] = array();
		}

		$filtered = wp_kses( $html, $allowed );
		$filtered = self::scrub_attributes( $filtered, $cfg, $allow_inline_styles );
		$filtered = self::scrub_dangerous_uris( $filtered );

		return $filtered;
	}

	/**
	 * Paste sanitizer: allowed tags only, no style attributes.
	 *
	 * @param string                  $html     Raw HTML.
	 * @param array<int, string>|null $commands Allowed commands.
	 * @return string
	 */
	public static function sanitize_paste( string $html, ?array $commands = null ): string {
		return self::sanitize( $html, $commands, false );
	}

	/**
	 * Post-kses: keep only color / text-align styles; drop other attrs leftovers.
	 *
	 * @param string               $html                Sanitized HTML.
	 * @param array<string, mixed> $cfg                 Tag config.
	 * @param bool                 $allow_inline_styles Whether style attrs are kept.
	 * @return string
	 */
	private static function scrub_attributes( string $html, array $cfg, bool $allow_inline_styles = true ): string {
		if ( '' === $html ) {
			return '';
		}

		if ( ! $allow_inline_styles ) {
			// Strip every style attribute on paste.
			$html = preg_replace( '/\sstyle\s*=\s*("|\').*?\1/i', '', $html ) ?? $html;
			$html = preg_replace( '/<\/?font\b[^>]*>/i', '', $html ) ?? $html;
			$html = preg_replace( '/\son[a-z]+\s*=\s*("|\').*?\1/i', '', $html ) ?? $html;
			$html = preg_replace( '/\s(?:javascript|data|vbscript|blob|file):/i', '', $html ) ?? $html;
			return $html;
		}

		// font tags → span with color when allowed.
		if ( ! empty( $cfg['color'] ) ) {
			$html = preg_replace_callback(
				'/<font\b([^>]*)>(.*?)<\/font>/is',
				static function ( $m ) {
					$attrs = $m[1];
					$inner = $m[2];
					$color = '';
					if ( preg_match( '/\bcolor\s*=\s*["\']?([^"\'\s>]+)/i', $attrs, $cm ) ) {
						$color = self::safe_color( $cm[1] );
					}
					if ( preg_match( '/\bstyle\s*=\s*["\']([^"\']*)["\']/i', $attrs, $sm ) ) {
						$from_style = self::safe_color_from_style( $sm[1] );
						if ( $from_style ) {
							$color = $from_style;
						}
					}
					if ( $color ) {
						return '<span style="color: ' . esc_attr( $color ) . '">' . $inner . '</span>';
					}
					return $inner;
				},
				$html
			) ?? $html;
		} else {
			$html = preg_replace( '/<\/?font\b[^>]*>/i', '', $html ) ?? $html;
		}

		$html = preg_replace_callback(
			'/<span\b([^>]*)>/i',
			static function ( $m ) use ( $cfg ) {
				$styles = array();
				$style  = '';
				if ( preg_match( '/\bstyle\s*=\s*["\']([^"\']*)["\']/i', $m[1], $sm ) ) {
					$style = $sm[1];
				}
				if ( self::style_is_dangerous( $style ) ) {
					return '<span>';
				}
				if ( ! empty( $cfg['color'] ) ) {
					$color = self::safe_color_from_style( $style );
					if ( $color ) {
						$styles[] = 'color: ' . $color;
					}
				}
				if ( ! empty( $cfg['fontfamily'] ) ) {
					$family = self::safe_font_family_from_style( $style );
					if ( $family ) {
						$styles[] = 'font-family: ' . $family;
					}
				}
				if ( ! empty( $cfg['fontsize'] ) ) {
					$size = self::safe_font_size_from_style( $style );
					if ( $size ) {
						$styles[] = 'font-size: ' . $size;
					}
				}
				if ( ! empty( $cfg['fontweight'] ) ) {
					$weight = self::safe_font_weight_from_style( $style );
					if ( $weight ) {
						$styles[] = 'font-weight: ' . $weight;
					}
				}
				if ( ! empty( $cfg['lineheight'] ) ) {
					$lh = self::safe_line_height_from_style( $style );
					if ( $lh ) {
						$styles[] = 'line-height: ' . $lh;
						$styles[] = 'display: block';
					}
				}
				if ( ! empty( $cfg['letterspacing'] ) ) {
					$ls = self::safe_letter_spacing_from_style( $style );
					if ( $ls ) {
						$styles[] = 'letter-spacing: ' . $ls;
					}
				}
				if ( ! empty( $cfg['align'] ) ) {
					if ( preg_match( '/(?:^|;)\s*text-align\s*:\s*(left|center|right|justify)\b/i', $style, $am ) ) {
						$styles[] = 'text-align: ' . strtolower( $am[1] );
					}
				}
				if ( $styles ) {
					return '<span style="' . esc_attr( implode( '; ', $styles ) ) . '">';
				}
				return '<span>';
			},
			$html
		) ?? $html;

		// Keep safe color styles authored on bold/italic wrappers (not only span).
		if ( ! empty( $cfg['color'] ) ) {
			$html = preg_replace_callback(
				'/<(strong|b|em|i|u|s|strike|del)\b([^>]*)>/i',
				static function ( $m ) {
					$tag   = strtolower( $m[1] );
					$style = '';
					if ( preg_match( '/\bstyle\s*=\s*["\']([^"\']*)["\']/i', $m[2], $sm ) ) {
						$style = $sm[1];
					}
					if ( self::style_is_dangerous( $style ) ) {
						return '<' . $tag . '>';
					}
					$color = self::safe_color_from_style( $style );
					if ( $color ) {
						return '<' . $tag . ' style="color: ' . esc_attr( $color ) . '">';
					}
					return '<' . $tag . '>';
				},
				$html
			) ?? $html;
		}

		if ( ! empty( $cfg['lineheight'] ) || ! empty( $cfg['letterspacing'] ) ) {
			$html = preg_replace_callback(
				'/<h([1-6])\b([^>]*)>/i',
				static function ( $m ) use ( $cfg ) {
					$tag = 'h' . $m[1];
					$styles = array();
					$style  = '';
					if ( preg_match( '/\bstyle\s*=\s*["\']([^"\']*)["\']/i', $m[2], $sm ) ) {
						$style = $sm[1];
					}
					if ( self::style_is_dangerous( $style ) ) {
						return '<' . $tag . '>';
					}
					if ( ! empty( $cfg['lineheight'] ) ) {
						$lh = self::safe_line_height_from_style( $style );
						if ( $lh ) {
							$styles[] = 'line-height: ' . $lh;
						}
					}
					if ( ! empty( $cfg['letterspacing'] ) ) {
						$ls = self::safe_letter_spacing_from_style( $style );
						if ( $ls ) {
							$styles[] = 'letter-spacing: ' . $ls;
						}
					}
					if ( $styles ) {
						return '<' . $tag . ' style="' . esc_attr( implode( '; ', $styles ) ) . '">';
					}
					return '<' . $tag . '>';
				},
				$html
			) ?? $html;
		}

		// Strip leftover event handlers if any slipped through.
		$html = preg_replace( '/\son[a-z]+\s*=\s*("|\').*?\1/i', '', $html ) ?? $html;
		$html = preg_replace( '/\s(?:javascript|data|vbscript|blob|file):/i', '', $html ) ?? $html;

		return $html;
	}

	/**
	 * Reject CSS that could execute code or load remote resources.
	 *
	 * @param string $style Style attribute value.
	 * @return bool
	 */
	private static function style_is_dangerous( string $style ): bool {
		return (bool) preg_match(
			'/(expression\s*\(|url\s*\(|-moz-binding|behavior\s*:|@import)/i',
			$style
		);
	}

	/**
	 * Strip dangerous href schemes from anchors after kses.
	 *
	 * @param string $html HTML.
	 * @return string
	 */
	private static function scrub_dangerous_uris( string $html ): string {
		return preg_replace_callback(
			'/<a\b([^>]*)>/i',
			static function ( $m ) {
				$attrs = $m[1];
				if ( ! preg_match( '/\bhref\s*=\s*("|\'|)([^"\'>\s]*)\1/i', $attrs, $hm ) ) {
					return '<a>';
				}
				$href = self::safe_href( $hm[2] );
				if ( ! $href ) {
					return '<a>';
				}
				$out = '<a href="' . esc_attr( $href ) . '"';
				if ( preg_match( '/\btarget\s*=\s*("|\'|)_blank\1/i', $attrs ) ) {
					$out .= ' target="_blank" rel="noopener noreferrer"';
				}
				$out .= '>';
				return $out;
			},
			$html
		) ?? $html;
	}

	/**
	 * @param string $href Raw href.
	 * @return string Empty when unsafe.
	 */
	private static function safe_href( string $href ): string {
		$original = trim( str_replace( "\0", '', $href ) );
		if ( '' === $original ) {
			return '';
		}
		$probe = strtolower( $original );
		$probe = str_replace(
			array( '&colon;', '&#58;', '&#x3a;' ),
			':',
			$probe
		);
		$probe = preg_replace( '/[\x00-\x1F\x7F\s]+/', '', $probe ) ?? $probe;
		if ( preg_match( '/^(javascript|data|vbscript|blob|file):/i', $probe ) ) {
			return '';
		}
		if (
			preg_match( '#^https?://#i', $original ) ||
			0 === strpos( $original, '/' ) ||
			0 === strpos( $original, '#' ) ||
			preg_match( '/^mailto:/i', $original ) ||
			preg_match( '/^tel:/i', $original )
		) {
			return substr( $original, 0, 2048 );
		}
		if ( ! preg_match( '/^[a-z][a-z0-9+.-]*:/i', $original ) ) {
			return substr( $original, 0, 2048 );
		}
		return '';
	}

	/**
	 * @param string $style Style attribute value.
	 * @return string Empty or safe color.
	 */
	private static function safe_color_from_style( string $style ): string {
		if ( preg_match( '/(?:^|;)\s*color\s*:\s*([^;]+)/i', $style, $m ) ) {
			return self::safe_color( $m[1] );
		}
		return '';
	}

	/**
	 * @param string $style Style attribute value.
	 * @return string Empty or safe font-family.
	 */
	private static function safe_font_family_from_style( string $style ): string {
		if ( self::style_is_dangerous( $style ) ) {
			return '';
		}
		if ( ! preg_match( '/(?:^|;)\s*font-family\s*:\s*([^;]+)/i', $style, $m ) ) {
			return '';
		}
		$family = trim( (string) $m[1] );
		$family = str_replace( array( '"', "'" ), '', $family );
		$family = preg_replace( '/[<>;{}()]/', '', $family ) ?? '';
		$family = substr( trim( $family ), 0, 120 );
		if ( preg_match( '/url|expression|javascript/i', $family ) ) {
			return '';
		}
		return $family;
	}

	/**
	 * @param string $style Style attribute value.
	 * @return string Empty or safe font-size.
	 */
	private static function safe_font_size_from_style( string $style ): string {
		if ( ! preg_match( '/(?:^|;)\s*font-size\s*:\s*([^;]+)/i', $style, $m ) ) {
			return '';
		}
		$raw = strtolower( trim( (string) $m[1] ) );
		$raw = str_replace( array( '"', "'" ), '', $raw );
		if ( ! preg_match( '/^[\d.]+\s*(px|rem|em|%)?$/i', $raw ) ) {
			return '';
		}
		$compact = preg_replace( '/\s+/', '', $raw ) ?? '';
		if ( ! preg_match( '/[a-z%]$/i', $compact ) ) {
			$compact = (string) floatval( $compact ) . 'px';
		}
		return substr( $compact, 0, 24 );
	}

	/**
	 * @param string $style Style attribute value.
	 * @return string Empty or numeric weight 100–900.
	 */
	private static function safe_font_weight_from_style( string $style ): string {
		if ( ! preg_match( '/(?:^|;)\s*font-weight\s*:\s*([^;]+)/i', $style, $m ) ) {
			return '';
		}
		$raw = strtolower( trim( (string) $m[1] ) );
		$raw = str_replace( array( '"', "'" ), '', $raw );
		if ( 'bold' === $raw ) {
			return '700';
		}
		if ( 'normal' === $raw ) {
			return '400';
		}
		if ( preg_match( '/^(100|200|300|400|500|600|700|800|900)$/', $raw ) ) {
			return $raw;
		}
		return '';
	}

	/**
	 * @param string $style Style attribute value.
	 * @return string Empty or safe line-height.
	 */
	private static function safe_line_height_from_style( string $style ): string {
		if ( ! preg_match( '/(?:^|;)\s*line-height\s*:\s*([^;]+)/i', $style, $m ) ) {
			return '';
		}
		$raw = strtolower( trim( (string) $m[1] ) );
		$raw = str_replace( array( '"', "'" ), '', $raw );
		if ( '' === $raw || 'normal' === $raw || 'inherit' === $raw ) {
			return '';
		}
		if ( preg_match( '/^[\d.]+$/', $raw ) ) {
			$n = (float) $raw;
			return ( $n > 0 && $n <= 10 ) ? (string) $n : '';
		}
		if ( ! preg_match( '/^[\d.]+\s*(px|rem|%)$/i', $raw ) ) {
			return '';
		}
		$compact = preg_replace( '/\s+/', '', $raw ) ?? '';
		$n       = (float) $compact;
		if ( $n <= 0 ) {
			return '';
		}
		if ( str_ends_with( $compact, '%' ) && $n > 1000 ) {
			return '';
		}
		if ( ( str_ends_with( $compact, 'px' ) || str_ends_with( $compact, 'rem' ) ) && $n > 500 ) {
			return '';
		}
		return substr( $compact, 0, 24 );
	}

	/**
	 * @param string $style Style attribute value.
	 * @return string Empty or safe letter-spacing.
	 */
	private static function safe_letter_spacing_from_style( string $style ): string {
		if ( ! preg_match( '/(?:^|;)\s*letter-spacing\s*:\s*([^;]+)/i', $style, $m ) ) {
			return '';
		}
		$raw = strtolower( trim( (string) $m[1] ) );
		$raw = str_replace( array( '"', "'" ), '', $raw );
		if ( '' === $raw || 'normal' === $raw || 'inherit' === $raw ) {
			return '';
		}
		if ( '0' === $raw ) {
			return '0';
		}
		if ( ! preg_match( '/^-?[\d.]+\s*(px|rem|em)?$/i', $raw ) ) {
			return '';
		}
		$compact = preg_replace( '/\s+/', '', $raw ) ?? '';
		if ( ! preg_match( '/[a-z]$/i', $compact ) ) {
			$compact = (string) floatval( $compact ) . 'px';
		}
		return substr( $compact, 0, 24 );
	}

	/**
	 * @param string $raw Color candidate.
	 * @return string Empty or #rrggbb / #rrggbbaa / rgba().
	 */
	private static function safe_color( string $raw ): string {
		$c = trim( $raw );
		$c = preg_replace( '/\s*!important\s*$/i', '', $c ) ?? $c;
		$c = trim( $c );
		if ( preg_match( '/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i', $c ) ) {
			if ( 4 === strlen( $c ) ) {
				return '#' . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
			}
			return strtolower( $c );
		}
		if ( preg_match( '/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([\d.]+)\s*)?\)$/i', $c, $m ) ) {
			$r = min( 255, (int) $m[1] );
			$g = min( 255, (int) $m[2] );
			$b = min( 255, (int) $m[3] );
			if ( isset( $m[4] ) && '' !== $m[4] ) {
				$a = max( 0, min( 1, (float) $m[4] ) );
				if ( $a < 1 ) {
					return sprintf( 'rgba(%d, %d, %d, %s)', $r, $g, $b, rtrim( rtrim( number_format( $a, 3, '.', '' ), '0' ), '.' ) );
				}
			}
			return sprintf( '#%02x%02x%02x', $r, $g, $b );
		}
		return '';
	}
}
