<?php

namespace SugarCalendarBookings\Support\Helpers;

/**
 * Color helper.
 *
 * Provides color manipulation and conversion utilities.
 *
 * @since 1.3.0
 */
class Color {

	/**
	 * Available color palette for services.
	 *
	 * These colors match the Sugar Calendar UI color picker options.
	 *
	 * @since 1.3.0
	 *
	 * @var array<string>
	 */
	public const SERVICE_PALETTE = [
		'#C06CB6', // Purple/Magenta.
		'#5B7CB2', // Blue.
		'#F37C4A', // Orange.
		'#4CBBA6', // Teal.
		'#8BC34A', // Green.
		'#FBC252', // Yellow.
	];

	/**
	 * Default service color.
	 *
	 * @since 1.3.0
	 *
	 * @var string
	 */
	public const DEFAULT_SERVICE_COLOR = '#5B7CB2';

	/**
	 * Validate and normalize a hex color.
	 *
	 * @since 1.3.0
	 *
	 * @param string $color         Color value.
	 * @param string $default_color Default color if invalid.
	 *
	 * @return string Valid hex color or default.
	 */
	public static function validate_hex( string $color, string $default_color = self::DEFAULT_SERVICE_COLOR ): string {

		if ( preg_match( '/^#[0-9A-F]{6}$/i', $color ) ) {
			return strtoupper( $color );
		}

		return $default_color;
	}

	/**
	 * Check if a color exists in a palette.
	 *
	 * @since 1.3.0
	 *
	 * @param string        $color   Hex color to check.
	 * @param array<string> $palette Palette to check against.
	 *
	 * @return bool True if color is in palette.
	 */
	public static function is_in_palette( string $color, array $palette ): bool {

		$color_upper = strtoupper( $color );

		foreach ( $palette as $palette_color ) {
			if ( strtoupper( $palette_color ) === $color_upper ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Map a color to the closest palette color.
	 *
	 * @since 1.3.0
	 *
	 * @param string        $color         Hex color value.
	 * @param array<string> $palette       Palette to map to.
	 * @param string        $default_color Default color if invalid.
	 *
	 * @return string Closest palette color.
	 */
	public static function map_to_palette( string $color, array $palette, string $default_color = self::DEFAULT_SERVICE_COLOR ): string {

		// Validate hex format.
		if ( ! preg_match( '/^#[0-9A-F]{6}$/i', $color ) ) {
			return $default_color;
		}

		// Check if color is already in palette.
		if ( self::is_in_palette( $color, $palette ) ) {
			return self::find_in_palette( $color, $palette );
		}

		// Find closest palette color.
		return self::find_closest_color( $color, $palette, $default_color );
	}

	/**
	 * Find the exact color match in palette (case-insensitive).
	 *
	 * @since 1.3.0
	 *
	 * @param string        $color   Hex color to find.
	 * @param array<string> $palette Palette to search.
	 *
	 * @return string|null Palette color or null if not found.
	 */
	public static function find_in_palette( string $color, array $palette ): ?string {

		$color_upper = strtoupper( $color );

		foreach ( $palette as $palette_color ) {
			if ( strtoupper( $palette_color ) === $color_upper ) {
				return $palette_color;
			}
		}

		return null;
	}

	/**
	 * Find the closest color from a list of colors.
	 *
	 * Uses weighted Euclidean distance in RGB space with weights
	 * that approximate human color perception.
	 *
	 * @since 1.3.0
	 *
	 * @param string        $color         Hex color value.
	 * @param array<string> $colors        Colors to compare against.
	 * @param string        $default_color Default if no match found.
	 *
	 * @return string Closest color.
	 */
	public static function find_closest_color( string $color, array $colors, string $default_color = '' ): string {

		if ( empty( $colors ) ) {
			return $default_color;
		}

		$input_rgb     = self::hex_to_rgb( $color );
		$closest_color = $default_color ?: $colors[0];
		$min_distance  = PHP_FLOAT_MAX;

		foreach ( $colors as $compare_color ) {
			$compare_rgb = self::hex_to_rgb( $compare_color );
			$distance    = self::calculate_distance( $input_rgb, $compare_rgb );

			if ( $distance < $min_distance ) {
				$min_distance  = $distance;
				$closest_color = $compare_color;
			}
		}

		return $closest_color;
	}

	/**
	 * Convert hex color to RGB array.
	 *
	 * @since 1.3.0
	 *
	 * @param string $hex Hex color value (e.g., #FF0000).
	 *
	 * @return array{r: int, g: int, b: int}
	 */
	public static function hex_to_rgb( string $hex ): array {

		$hex = ltrim( $hex, '#' );

		return [
			'r' => (int) hexdec( substr( $hex, 0, 2 ) ),
			'g' => (int) hexdec( substr( $hex, 2, 2 ) ),
			'b' => (int) hexdec( substr( $hex, 4, 2 ) ),
		];
	}

	/**
	 * Convert RGB array to hex color.
	 *
	 * @since 1.3.0
	 *
	 * @param array{r: int, g: int, b: int} $rgb RGB color array.
	 *
	 * @return string Hex color value.
	 */
	public static function rgb_to_hex( array $rgb ): string {

		return sprintf(
			'#%02X%02X%02X',
			max( 0, min( 255, $rgb['r'] ) ),
			max( 0, min( 255, $rgb['g'] ) ),
			max( 0, min( 255, $rgb['b'] ) )
		);
	}

	/**
	 * Calculate perceptual color distance between two RGB colors.
	 *
	 * Uses weighted Euclidean distance with weights that approximate
	 * human color perception (humans are more sensitive to green).
	 * Based on the "redmean" color difference formula.
	 *
	 * @since 1.3.0
	 *
	 * @param array{r: int, g: int, b: int} $color1 First RGB color.
	 * @param array{r: int, g: int, b: int} $color2 Second RGB color.
	 *
	 * @return float Color distance.
	 */
	public static function calculate_distance( array $color1, array $color2 ): float {

		$r_diff = $color1['r'] - $color2['r'];
		$g_diff = $color1['g'] - $color2['g'];
		$b_diff = $color1['b'] - $color2['b'];

		// Average red value affects perception weighting.
		$r_mean = ( $color1['r'] + $color2['r'] ) / 2;

		// Weighted Euclidean distance with perceptual adjustments.
		// These weights are based on the "redmean" color difference formula.
		$r_weight = 2 + ( $r_mean / 256 );
		$g_weight = 4;
		$b_weight = 2 + ( ( 255 - $r_mean ) / 256 );

		return sqrt(
			( $r_weight * $r_diff * $r_diff ) +
			( $g_weight * $g_diff * $g_diff ) +
			( $b_weight * $b_diff * $b_diff )
		);
	}
}
