<?php
/**
 * Utilities Class file.
 *
 * @package Color_Theme_Manager_For_Divi
 */

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

/**
 * Helper class for color manipulations and calculations.
 *
 * Contains static methods for sanitization, color format conversion (HEX, RGB, HSL),
 * handling Divi's variable format, and calculating contrast ratios.
 *
 * @since 1.0.0
 */
class CTMD_Utilities {

	/**
	 * Sanitizes a color string to ensure it matches allowed formats.
	 *
	 * Supports HEX, RGB, RGBA, HSL, HSLA, 'transparent', and Divi variables.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color The color string to sanitize.
	 * @return string The sanitized color string or empty string if invalid.
	 */
	public static function sanitize_rgba_color( $color ) {
		if ( empty( $color ) || ! is_string( $color ) ) {
			return '';
		}
		$color = trim( $color );
		if ( preg_match( '/^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i', $color ) ) {
			return $color;
		}
		if ( preg_match( '/^(rgba?|hsla?)\s*\([\s*0-9\.\,\s%]+\)$/i', $color ) ) {
			return $color;
		}
		if ( strtolower( $color ) === 'transparent' ) {
			return 'transparent';
		}

		// Check for Divi's variable format, e.g., "$variable({...})$".
		if ( strpos( $color, '$variable(' ) === 0 && substr( $color, -2 ) === ')$' ) {
			return $color;
		}

		return '';
	}

	/**
	 * Recursively resolves a Divi variable string to a concrete RGBA color string.
	 *
	 * Handles nested variables up to a specified depth.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color_string The color string (potential variable).
	 * @param array  $all_colors   Array of all available global colors for reference.
	 * @param int    $max_depth    Maximum recursion depth to prevent infinite loops. Default 10.
	 * @return string The resolved color as an RGBA string or #000000 on failure.
	 */
	public static function resolve_color( $color_string, $all_colors, $max_depth = 10 ) {
		if ( strpos( $color_string, '$variable' ) !== 0 || $max_depth <= 0 ) {
			return $color_string;
		}

		try {
			$json_str = substr( $color_string, 10, -2 );
			$json_str = str_replace( 'u0022', '"', $json_str );
			$data     = json_decode( $json_str, true );

			if ( ! isset( $data['value']['name'] ) ) {
				return '#000000'; // Invalid derivative format.
			}

			$parent_id = $data['value']['name'];
			$settings  = $data['value']['settings'] ?? array();

			if ( ! isset( $all_colors[ $parent_id ] ) ) {
				return '#000000'; // Parent color not found.
			}

			// Recursively resolve the parent color.
			$parent_color_string = self::resolve_color( $all_colors[ $parent_id ]['color'], $all_colors, $max_depth - 1 );

			// Get the RGBA array of the resolved parent color.
			$rgba = self::parse_color_to_rgba_array( $parent_color_string );

			// Apply adjustments.
			self::apply_derivative_settings( $rgba, $settings );

			// Return the final color as an rgba string.
			return sprintf( 'rgba(%d, %d, %d, %s)', $rgba['r'], $rgba['g'], $rgba['b'], rtrim( sprintf( '%.2f', $rgba['a'] ), '0.' ) );

		} catch ( Exception $e ) {
			return '#000000'; // Error during parsing.
		}
	}

	/**
	 * Applies HSL and Opacity adjustments to a color.
	 *
	 * Used for Divi's derivative colors (e.g. darken, lighten, adjust opacity).
	 *
	 * @since 1.0.0
	 *
	 * @param array $rgba     The base color array ['r', 'g', 'b', 'a']. Passed by reference.
	 * @param array $settings The settings array containing adjustments (hue, saturation, lightness, opacity).
	 * @return void
	 */
	private static function apply_derivative_settings( &$rgba, $settings ) {
		if ( empty( $settings ) ) {
			return;
		}

		// Convert RGBA to HSLA to apply adjustments.
		$hsl      = self::rgb_to_hsl_array( $rgba['r'], $rgba['g'], $rgba['b'] );
		$hsl['a'] = $rgba['a'];

		// Apply adjustments.
		if ( isset( $settings['hue'] ) ) {
			$hsl['h'] = ( $hsl['h'] + $settings['hue'] ) % 360;
			if ( $hsl['h'] < 0 ) {
				$hsl['h'] += 360;
			}
		}
		if ( isset( $settings['saturation'] ) ) {
			$hsl['s'] = max( 0, min( 100, $hsl['s'] + $settings['saturation'] ) );
		}
		if ( isset( $settings['lightness'] ) ) {
			$hsl['l'] = max( 0, min( 100, $hsl['l'] + $settings['lightness'] ) );
		}
		if ( isset( $settings['opacity'] ) ) {
			// Divi's opacity setting is a percentage value from 0-100.
			$hsl['a'] = max( 0, min( 100, $settings['opacity'] ) ) / 100.0;
		}

		// Convert back to RGBA.
		$new_rgba  = self::hsl_to_rgb_array( $hsl['h'], $hsl['s'], $hsl['l'] );
		$rgba['r'] = $new_rgba['r'];
		$rgba['g'] = $new_rgba['g'];
		$rgba['b'] = $new_rgba['b'];
		$rgba['a'] = $hsl['a'];
	}

	/**
	 * Resolves the parent color of a variable without applying derivative settings.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color_string The variable string.
	 * @param array  $all_colors   Array of available global colors.
	 * @return string The resolved parent color string.
	 */
	public static function get_parent_color( $color_string, $all_colors ) {
		if ( strpos( $color_string, '$variable' ) !== 0 ) {
			return $color_string; // Not a derivative, return original.
		}
		try {
			$json_str = substr( $color_string, 10, -2 );
			$json_str = str_replace( 'u0022', '"', $json_str );
			$data     = json_decode( $json_str, true );

			if ( ! isset( $data['value']['name'] ) ) {
				return '#000000'; // Invalid format.
			}
			$parent_id = $data['value']['name'];

			if ( ! isset( $all_colors[ $parent_id ] ) ) {
				return '#000000'; // Parent not found.
			}

			// Resolve the parent's color recursively.
			return self::resolve_color( $all_colors[ $parent_id ]['color'], $all_colors );
		} catch ( Exception $e ) {
			return '#000000';
		}
	}

	/**
	 * Retrieves the human-readable label of a parent color from a variable string.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color_string The variable string.
	 * @param array  $all_colors   Array of available global colors.
	 * @return string The label of the parent color, or the ID if label not found.
	 */
	public static function get_parent_name( $color_string, $all_colors ) {
		if ( strpos( $color_string, '$variable' ) !== 0 ) {
			return '';
		}
		try {
			$json_str  = substr( $color_string, 10, -2 );
			$json_str  = str_replace( 'u0022', '"', $json_str );
			$data      = json_decode( $json_str, true );
			$parent_id = $data['value']['name'] ?? '';

			if ( isset( $all_colors[ $parent_id ]['label'] ) ) {
				return $all_colors[ $parent_id ]['label'];
			}
			return $parent_id; // Fallback to ID if label not found.
		} catch ( Exception $e ) {
			return '';
		}
	}

	/**
	 * Parses a color string into an associative array of RGBA components.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color The color string to parse.
	 * @return array Associative array ['r', 'g', 'b', 'a'].
	 */
	private static function parse_color_to_rgba_array( $color ) {
		if ( empty( $color ) || ! is_string( $color ) ) {
			return array(
				'r' => 0,
				'g' => 0,
				'b' => 0,
				'a' => 1,
			);
		}
		$color = trim( strtolower( $color ) );

		if ( 'transparent' === $color ) {
			return array(
				'r' => 0,
				'g' => 0,
				'b' => 0,
				'a' => 0,
			);
		}

		if ( strpos( $color, 'rgba' ) === 0 ) {
			sscanf( $color, 'rgba(%d, %d, %d, %f)', $r, $g, $b, $a );
			return array(
				'r' => $r,
				'g' => $g,
				'b' => $b,
				'a' => $a,
			);
		}
		if ( strpos( $color, 'rgb' ) === 0 ) {
			sscanf( $color, 'rgb(%d, %d, %d)', $r, $g, $b );
			return array(
				'r' => $r,
				'g' => $g,
				'b' => $b,
				'a' => 1,
			);
		}
		if ( strpos( $color, 'hsla' ) === 0 ) {
			sscanf( $color, 'hsla(%d, %d%%, %d%%, %f)', $h, $s, $l, $a );
			$rgb      = self::hsl_to_rgb_array( $h, $s, $l );
			$rgb['a'] = $a;
			return $rgb;
		}
		if ( strpos( $color, 'hsl' ) === 0 ) {
			sscanf( $color, 'hsl(%d, %d%%, %d%%)', $h, $s, $l );
			return self::hsl_to_rgb_array( $h, $s, $l );
		}
		if ( strpos( $color, '#' ) === 0 ) {
			$hex = str_replace( '#', '', $color );
			if ( strlen( $hex ) === 3 ) {
				$r = hexdec( str_repeat( substr( $hex, 0, 1 ), 2 ) );
				$g = hexdec( str_repeat( substr( $hex, 1, 1 ), 2 ) );
				$b = hexdec( str_repeat( substr( $hex, 2, 1 ), 2 ) );
				return array(
					'r' => $r,
					'g' => $g,
					'b' => $b,
					'a' => 1,
				);
			}
			if ( strlen( $hex ) === 4 ) {
				$r = hexdec( str_repeat( substr( $hex, 0, 1 ), 2 ) );
				$g = hexdec( str_repeat( substr( $hex, 1, 1 ), 2 ) );
				$b = hexdec( str_repeat( substr( $hex, 2, 1 ), 2 ) );
				$a = hexdec( str_repeat( substr( $hex, 3, 1 ), 2 ) ) / 255;
				return array(
					'r' => $r,
					'g' => $g,
					'b' => $b,
					'a' => $a,
				);
			}
			if ( strlen( $hex ) === 6 ) {
				$r = hexdec( substr( $hex, 0, 2 ) );
				$g = hexdec( substr( $hex, 2, 2 ) );
				$b = hexdec( substr( $hex, 4, 2 ) );
				return array(
					'r' => $r,
					'g' => $g,
					'b' => $b,
					'a' => 1,
				);
			}
			if ( strlen( $hex ) === 8 ) {
				$r = hexdec( substr( $hex, 0, 2 ) );
				$g = hexdec( substr( $hex, 2, 2 ) );
				$b = hexdec( substr( $hex, 4, 2 ) );
				$a = hexdec( substr( $hex, 6, 2 ) ) / 255;
				return array(
					'r' => $r,
					'g' => $g,
					'b' => $b,
					'a' => $a,
				);
			}
		}
		return array(
			'r' => 0,
			'g' => 0,
			'b' => 0,
			'a' => 1,
		);
	}

	/**
	 * Normalizes a color string to HEX format.
	 *
	 * Converts various color formats to #RRGGBB or #RRGGBBAA.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color The color string to convert.
	 * @return string The normalized HEX string.
	 */
	public static function normalize_to_hex( $color ) {
		$c = self::parse_color_to_rgba_array( $color );
		// Use an epsilon for float comparison to avoid precision issues.
		if ( abs( $c['a'] - 1.0 ) > 0.001 ) {
			return sprintf( '#%02x%02x%02x%02x', $c['r'], $c['g'], $c['b'], round( $c['a'] * 255 ) );
		}
		return sprintf( '#%02x%02x%02x', $c['r'], $c['g'], $c['b'] );
	}

	/**
	 * Converts a color to RGB or RGBA string format.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color The color string to convert.
	 * @return string The formatted 'rgb(...)' or 'rgba(...)' string.
	 */
	public static function hex_to_rgb( $color ) {
		$c = self::parse_color_to_rgba_array( $color );
		// Use an epsilon for float comparison to avoid precision issues.
		if ( abs( $c['a'] - 1.0 ) > 0.001 ) {
			$alpha_val = (float) rtrim( sprintf( '%.2f', $c['a'] ), '0.' );
			return sprintf( 'rgba(%d, %d, %d, %s)', $c['r'], $c['g'], $c['b'], $alpha_val );
		}
		return sprintf( 'rgb(%d, %d, %d)', $c['r'], $c['g'], $c['b'] );
	}

	/**
	 * Converts a color to HSL or HSLA string format.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color The color string to convert.
	 * @return string The formatted 'hsl(...)' or 'hsla(...)' string.
	 */
	public static function hex_to_hsl( $color ) {
		$c   = self::parse_color_to_rgba_array( $color );
		$hsl = self::rgb_to_hsl_array( $c['r'], $c['g'], $c['b'] );
		// Use an epsilon for float comparison to avoid precision issues.
		if ( abs( $c['a'] - 1.0 ) > 0.001 ) {
			$alpha_val = (float) rtrim( sprintf( '%.2f', $c['a'] ), '0.' );
			return sprintf( 'hsla(%d, %d%%, %d%%, %s)', $hsl['h'], $hsl['s'], $hsl['l'], $alpha_val );
		}
		return sprintf( 'hsl(%d, %d%%, %d%%)', $hsl['h'], $hsl['s'], $hsl['l'] );
	}

	/**
	 * Helper function to convert HSL values to an RGB array.
	 *
	 * @since 1.0.0
	 *
	 * @param int $h Hue (0-360).
	 * @param int $s Saturation (0-100).
	 * @param int $l Lightness (0-100).
	 * @return array Associative array ['r', 'g', 'b', 'a'].
	 */
	private static function hsl_to_rgb_array( $h, $s, $l ) {
		$h /= 360;
		$s /= 100;
		$l /= 100;
		if ( 0 === $s ) {
			$r = $l;
			$g = $l;
			$b = $l;
		} else {
			$q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s;
			$p = 2 * $l - $q;
			$r = self::hue_to_rgb_component( $p, $q, $h + 1 / 3 );
			$g = self::hue_to_rgb_component( $p, $q, $h );
			$b = self::hue_to_rgb_component( $p, $q, $h - 1 / 3 );
		}
		return array(
			'r' => round( $r * 255 ),
			'g' => round( $g * 255 ),
			'b' => round( $b * 255 ),
			'a' => 1,
		);
	}

	/**
	 * Helper function to convert RGB values to an HSL array.
	 *
	 * @since 1.0.0
	 *
	 * @param int $r Red (0-255).
	 * @param int $g Green (0-255).
	 * @param int $b Blue (0-255).
	 * @return array Associative array ['h', 's', 'l'].
	 */
	private static function rgb_to_hsl_array( $r, $g, $b ) {
		$r  /= 255;
		$g  /= 255;
		$b  /= 255;
		$max = max( $r, $g, $b );
		$min = min( $r, $g, $b );
		$h   = ( $max + $min ) / 2;
		$s   = ( $max + $min ) / 2;
		$l   = ( $max + $min ) / 2;

		if ( $max === $min ) {
			$h = 0;
			$s = 0;
		} else {
			$d = $max - $min;
			$s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min );
			switch ( $max ) {
				case $r:
					$h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
					break;
				case $g:
					$h = ( $b - $r ) / $d + 2;
					break;
				case $b:
					$h = ( $r - $g ) / $d + 4;
					break;
			}
			$h /= 6;
		}
		return array(
			'h' => round( $h * 360 ),
			's' => round( $s * 100 ),
			'l' => round( $l * 100 ),
		);
	}

	/**
	 * Helper function to calculate a single RGB component from HSL.
	 *
	 * @since 1.0.0
	 *
	 * @param float $p Intermediate value 1.
	 * @param float $q Intermediate value 2.
	 * @param float $t Normalized hue component.
	 * @return float The calculated RGB component (0-1).
	 */
	private static function hue_to_rgb_component( $p, $q, $t ) {
		if ( $t < 0 ) {
			++$t;
		}
		if ( $t > 1 ) {
			--$t;
		}
		if ( $t < 1 / 6 ) {
			return $p + ( $q - $p ) * 6 * $t;
		}
		if ( $t < 1 / 2 ) {
			return $q;
		}
		if ( $t < 2 / 3 ) {
			return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6;
		}
		return $p;
	}

	/**
	 * Calculates the relative luminance of a color.
	 *
	 * Follows WCAG 2.0 definitions.
	 *
	 * @since 1.0.0
	 *
	 * @param array $rgba Associative array ['r', 'g', 'b', 'a'].
	 * @return float The relative luminance value (0-1).
	 */
	private static function get_relative_luminance( $rgba ) {
		$r = $rgba['r'] / 255;
		$g = $rgba['g'] / 255;
		$b = $rgba['b'] / 255;

		$r = ( $r <= 0.03928 ) ? $r / 12.92 : pow( ( ( $r + 0.055 ) / 1.055 ), 2.4 );
		$g = ( $g <= 0.03928 ) ? $g / 12.92 : pow( ( ( $g + 0.055 ) / 1.055 ), 2.4 );
		$b = ( $b <= 0.03928 ) ? $b / 12.92 : pow( ( ( $b + 0.055 ) / 1.055 ), 2.4 );

		return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
	}

	/**
	 * Calculates the contrast ratio between two colors.
	 *
	 * Handles alpha blending by assuming the first color acts as the foreground
	 * over the second color.
	 *
	 * @since 1.0.0
	 *
	 * @param string $color1 The foreground color string.
	 * @param string $color2 The background color string.
	 * @return float The contrast ratio (1-21).
	 */
	public static function get_contrast_ratio( $color1, $color2 ) {
		$rgba1 = self::parse_color_to_rgba_array( $color1 );
		$rgba2 = self::parse_color_to_rgba_array( $color2 );

		// Blend the foreground color onto the background color if it has alpha transparency.
		if ( $rgba1['a'] < 1 ) {
			$r     = $rgba1['r'] * $rgba1['a'] + $rgba2['r'] * ( 1 - $rgba1['a'] );
			$g     = $rgba1['g'] * $rgba1['a'] + $rgba2['g'] * ( 1 - $rgba1['a'] );
			$b     = $rgba1['b'] * $rgba1['a'] + $rgba2['b'] * ( 1 - $rgba1['a'] );
			$rgba1 = array(
				'r' => $r,
				'g' => $g,
				'b' => $b,
				'a' => 1,
			);
		}

		$lum1 = self::get_relative_luminance( $rgba1 );
		$lum2 = self::get_relative_luminance( $rgba2 );

		$lighter = max( $lum1, $lum2 );
		$darker  = min( $lum1, $lum2 );

		return ( $lighter + 0.05 ) / ( $darker + 0.05 );
	}
}
