<?php

/**
 * JCOGS Image Pro - Colour Management Service
 * ===========================================
 * Native implementation of legacy colour management
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      2.0.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service;

use Imagine\Image\Palette\Color\ColorInterface;
use Imagine\Image\Palette;
use JCOGSDesign\JCOGSImagePro\Contracts\SettingsInterface;

/**
 * Colour Management Service
 * 
 * This service provides all colour manipulation and validation functionality
 * from the legacy ColourManagementTrait with 100% backward compatibility.
 * 
 * Key Methods:
 * - validate_colour_string() - Main colour validation (used by cache key generation)
 * - convert_rgba_to_GdImage_format() - GD format conversion
 * - convert_rgba_to_Imagine_RGB() - Imagine palette conversion
 * - hslToRgb() / rgbToHsl() - Colour space conversions
 */
class ColourManagementService 
{
    private SettingsInterface $settings;
    
    /**
     * Constructor
     * 
     * Initialize with settings service for accessing colour defaults
     */
    public function __construct(SettingsInterface $settings) 
    {
        $this->settings = $settings;
    }
    
    /**
     * Convert RGBA/RGB string to GdImage format
     * 
     * Converts rgb or rgba into GdImage three / four digit colour array
     * 
     * @param string $colour_string Color string in rgb() or rgba() format
     * @return array|bool Color values array or false if invalid
     */
    public function convert_rgba_to_GdImage_format(string $colour_string): array|bool
    {
        if (preg_match('/rgba\((.*)\)/', $colour_string, $matches)) {
            $values = explode(',', $matches[1]);
            $values[3] = (int) ((1 - $values[3]) * 127);
            return $values;
        }
        elseif (preg_match('/rgb\((.*)\)/', $colour_string, $matches)) {
            $values = explode(',', $matches[1]);
            // set opacity value to 1 to imply opaque and so return four values
            $values[3] = 127;
            return $values;
        }
        else {
            return false;
        }
    }

    /**
     * Convert RGBA/RGB string to Imagine RGB Color
     * 
     * Converts rgb or rgba into Imagine Color Palette object
     * 
     * @param string $colour_string Color string in rgb() or rgba() format  
     * @return ColorInterface|bool Imagine color object or false if invalid
     */
    public function convert_rgba_to_Imagine_RGB(string $colour_string): bool|ColorInterface
    {
        if (preg_match('/rgba\((.*)\)/', $colour_string, $matches)) {
            $values = explode(',', $matches[1]);
            $values[3] = (int) ($values[3] * 100);
        }
        elseif (preg_match('/rgb\((.*)\)/', $colour_string, $matches)) {
            $values = explode(',', $matches[1]);
            // set opacity value to 1 to imply opaque and so return four values
            $values[3] = 100;
        }
        else {
            return false;
        }
        $colors = [[(int) $values[0], (int) $values[1], (int) $values[2]], (int) $values[3]];

        return (new Palette\RGB())->color(...$colors);
    }

    /**
     * Convert HSL to RGB
     * 
     * Convert an HSL value to an RGB format
     * From https://gist.github.com/brandonheyer/5254516
     * 
     * @param float $h H value in HSL (0-360)
     * @param float $s S value in HSL (0-1)
     * @param float $l L value in HSL (0-1)
     * @return array RGB values [r, g, b]
     */
    public function hsl_to_rgb(float $h, float $s, float $l): array
    {
        $h = min(max($h, 0), 360);
        $s = min(max($s, 0), 1);
        $l = min(max($l, 0), 1);

        $r = 0;
        $g = 0;
        $b = 0;

        $c = (1 - abs(2 * $l - 1)) * $s;
        $x = $c * (1 - abs(fmod(($h / 60), 2) - 1));
        $m = $l - ($c / 2);

        if ($h < 60) {
            $r = $c;
            $g = $x;
            $b = 0;
        }
        else if ($h < 120) {
            $r = $x;
            $g = $c;
            $b = 0;
        }
        else if ($h < 180) {
            $r = 0;
            $g = $c;
            $b = $x;
        }
        else if ($h < 240) {
            $r = 0;
            $g = $x;
            $b = $c;
        }
        else if ($h < 300) {
            $r = $x;
            $g = 0;
            $b = $c;
        }
        else {
            $r = $c;
            $g = 0;
            $b = $x;
        }

        $r = ($r + $m) * 255;
        $g = ($g + $m) * 255;
        $b = ($b + $m) * 255;

        return array(floor($r), floor($g), floor($b));
    }
    
    /**
     * Normalize RGB value to valid range
     * 
     * @param int $value RGB value to normalize
     * @return int Normalized value (0-255)
     */
    public function normalize_rgb_value(int $value): int
    {
        return max(0, min(255, $value));
    }

    /**
     * Convert RGB to HSL
     * 
     * Convert an RGB value to an HSL format
     * From https://gist.github.com/brandonheyer/5254516
     * 
     * @param int $r R value in RGB (0-255)
     * @param int $g G value in RGB (0-255) 
     * @param int $b B value in RGB (0-255)
     * @return array HSL values [h, s, l]
     */
    public function rgb_to_hsl(int $r, int $g, int $b): array
    {
        // Normalize RGB values to the range 0-255
        $r = min(max($r, 0), 255) / 255;
        $g = min(max($g, 0), 255) / 255;
        $b = min(max($b, 0), 255) / 255;
    
        $max = max($r, $g, $b);
        $min = min($r, $g, $b);
    
        $h = 0;
        $s = 0;
        $l = ($max + $min) / 2;
        $d = $max - $min;
    
        if ($d != 0) {
            $s = $d / (1 - abs(2 * $l - 1));
    
            switch ($max) {
                case $r:
                    $h = 60 * fmod((($g - $b) / $d), 6);
                    if ($b > $g) {
                        $h += 360;
                    }
                    break;
    
                case $g:
                    $h = 60 * (($b - $r) / $d + 2);
                    break;
    
                case $b:
                    $h = 60 * (($r - $g) / $d + 4);
                    break;
            }
        }
    
        return [round($h, 3), round($s, 3), round($l, 3)];
    }
    
    /**
     * Main colour validation method
     * 
     * Normalises colour strings / rgba forms to Imagine RGB colour palette.
     * If colour not valid or not set returns default background colour.
     * 
     * This is the exact same algorithm as legacy validate_colour_string()
     * 
     * @param string|null|ColorInterface $colour Color string (hex, rgb, rgba formats) or existing ColorInterface
     * @param float $opacity Opacity value (0-1)
     * @return ColorInterface Validated Imagine colour object
     */
    public function validate_colour_string(string|null|ColorInterface $colour = null, float $opacity = 1): ColorInterface
    {
        // Check to see if we have been here before... 
        if ($colour instanceof ColorInterface) {
            return $colour;
        }

        $settings = $this->settings->all();
        
        // Use default if no color provided - check legacy parameter first
        $default = isset($settings['bg_color']) && $settings['bg_color'] != '' 
            ? $settings['bg_color'] 
            : $settings['img_cp_default_bg_color'];
            
        $colour = $colour ?? $default;

        // Check for CSS named colors first (case-insensitive)
        $colour = $this->_convertNamedColorToHex($colour);

        // Sanitize $colour if "#" is provided 
        // Need to check for hex and rgb and rgba forms.
        if (stripos($colour, '#') !== false) {
            $colour = substr($colour, 1);
        }

        // Check to see if colour is in rgb(a) format
        // If opacity given use that rather than any opacity given in call
        if (strtolower(substr($colour, 0, 4)) == 'rgba') {
            if (preg_match('/rgba\((.*)\)/', $colour, $matches) == 0) {
                // Invalid rgba format, fall back to default
                return $this->validate_colour_string($default);
            }
            $rgb = explode(',', $matches[1]);
            if (count($rgb) == 4) {
                $opacity = (float) trim(array_pop($rgb));
            } else {
                $opacity = $opacity ?? 1;
            }
        }
        elseif (strtolower(substr($colour, 0, 3)) == 'rgb') {
            // Check to see if colour is in rgb format
            if (preg_match('/rgb\((.*)\)/', $colour, $matches) == 0) {
                // Invalid rgb format, fall back to default
                return $this->validate_colour_string($default);
            }
            $rgb = explode(',', $matches[1]);
        }
        elseif (strlen($colour) == 8) {
            // Check if colour is in hexadecimal format and has 8, 6, 4 or 3 characters and get values
            // If colour has 8 or 4 then use that in preference to any opacity value given in call
            $rgb = array_map('hexdec', array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]));
            $opacity = round(hexdec($colour[6] . $colour[7]) / 255, 2);
        }
        elseif (strlen($colour) == 6) {
            $rgb = array_map('hexdec', array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]));
        }
        elseif (strlen($colour) == 4) {
            $rgb = array_map('hexdec', array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]));
            $opacity = round(hexdec($colour[3] . $colour[3]) / 255, 2);
        }
        elseif (strlen($colour) == 3) {
            $rgb = array_map('hexdec', array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]));
        }
        else {
            return $this->validate_colour_string($default);
        }

        // Normalize rgb values - convert to int and clamp
        $rgb = array_map(function($value) { 
            return $this->normalize_rgb_value((int) trim($value)); 
        }, $rgb);

                // Normalize opacity value
        $opacity = max(0, min(1, $opacity));

        // Now scale opacity to 0-100 range to suit Imagine library
        $opacity = (int) round($opacity * 100, 0);
        return (new Palette\RGB())->color([$rgb[0], $rgb[1], $rgb[2]], $opacity);
    }

    /**
     * Convert CSS named colors to hex values
     * Supports standard CSS color keywords for enhanced user experience (#61)
     * 
     * @param string $colour Color name or existing color value
     * @return string Hex color value or original input if not a named color
     */
    private function _convertNamedColorToHex(string $colour): string
    {
        // Common CSS named colors map (subset of most useful colors)
        // Full spec: https://www.w3.org/TR/css-color-3/#svg-color
        $namedColors = [
            // Basic colors
            'black' => '000000',
            'white' => 'FFFFFF',
            'red' => 'FF0000',
            'green' => '008000',
            'blue' => '0000FF',
            'yellow' => 'FFFF00',
            'cyan' => '00FFFF',
            'magenta' => 'FF00FF',
            'gray' => '808080',
            'grey' => '808080',
            
            // Extended colors
            'maroon' => '800000',
            'darkred' => '8B0000',
            'brown' => 'A52A2A',
            'firebrick' => 'B22222',
            'crimson' => 'DC143C',
            'tomato' => 'FF6347',
            'coral' => 'FF7F50',
            'indianred' => 'CD5C5C',
            'lightcoral' => 'F08080',
            'darksalmon' => 'E9967A',
            'salmon' => 'FA8072',
            'lightsalmon' => 'FFA07A',
            'orangered' => 'FF4500',
            'darkorange' => 'FF8C00',
            'orange' => 'FFA500',
            'gold' => 'FFD700',
            'darkgoldenrod' => 'B8860B',
            'goldenrod' => 'DAA520',
            'palegoldenrod' => 'EEE8AA',
            'darkkhaki' => 'BDB76B',
            'khaki' => 'F0E68C',
            'olive' => '808000',
            'yellowgreen' => '9ACD32',
            'darkolivegreen' => '556B2F',
            'olivedrab' => '6B8E23',
            'lawngreen' => '7CFC00',
            'chartreuse' => '7FFF00',
            'greenyellow' => 'ADFF2F',
            'darkgreen' => '006400',
            'forestgreen' => '228B22',
            'lime' => '00FF00',
            'limegreen' => '32CD32',
            'lightgreen' => '90EE90',
            'palegreen' => '98FB98',
            'darkseagreen' => '8FBC8F',
            'mediumspringgreen' => '00FA9A',
            'springgreen' => '00FF7F',
            'seagreen' => '2E8B57',
            'mediumaquamarine' => '66CDAA',
            'mediumseagreen' => '3CB371',
            'lightseagreen' => '20B2AA',
            'darkslategray' => '2F4F4F',
            'darkslategrey' => '2F4F4F',
            'teal' => '008080',
            'darkcyan' => '008B8B',
            'aqua' => '00FFFF',
            'lightcyan' => 'E0FFFF',
            'darkturquoise' => '00CED1',
            'turquoise' => '40E0D0',
            'mediumturquoise' => '48D1CC',
            'paleturquoise' => 'AFEEEE',
            'aquamarine' => '7FFFD4',
            'powderblue' => 'B0E0E6',
            'cadetblue' => '5F9EA0',
            'steelblue' => '4682B4',
            'cornflowerblue' => '6495ED',
            'deepskyblue' => '00BFFF',
            'dodgerblue' => '1E90FF',
            'lightblue' => 'ADD8E6',
            'skyblue' => '87CEEB',
            'lightskyblue' => '87CEFA',
            'midnightblue' => '191970',
            'navy' => '000080',
            'darkblue' => '00008B',
            'mediumblue' => '0000CD',
            'royalblue' => '4169E1',
            'blueviolet' => '8A2BE2',
            'indigo' => '4B0082',
            'darkslateblue' => '483D8B',
            'slateblue' => '6A5ACD',
            'mediumslateblue' => '7B68EE',
            'mediumpurple' => '9370DB',
            'darkmagenta' => '8B008B',
            'darkviolet' => '9400D3',
            'darkorchid' => '9932CC',
            'mediumorchid' => 'BA55D3',
            'purple' => '800080',
            'thistle' => 'D8BFD8',
            'plum' => 'DDA0DD',
            'violet' => 'EE82EE',
            'fuchsia' => 'FF00FF',
            'orchid' => 'DA70D6',
            'mediumvioletred' => 'C71585',
            'palevioletred' => 'DB7093',
            'deeppink' => 'FF1493',
            'hotpink' => 'FF69B4',
            'lightpink' => 'FFB6C1',
            'pink' => 'FFC0CB',
            'antiquewhite' => 'FAEBD7',
            'beige' => 'F5F5DC',
            'bisque' => 'FFE4C4',
            'blanchedalmond' => 'FFEBCD',
            'wheat' => 'F5DEB3',
            'cornsilk' => 'FFF8DC',
            'lemonchiffon' => 'FFFACD',
            'lightgoldenrodyellow' => 'FAFAD2',
            'lightyellow' => 'FFFFE0',
            'saddlebrown' => '8B4513',
            'sienna' => 'A0522D',
            'chocolate' => 'D2691E',
            'peru' => 'CD853F',
            'sandybrown' => 'F4A460',
            'burlywood' => 'DEB887',
            'tan' => 'D2B48C',
            'rosybrown' => 'BC8F8F',
            'moccasin' => 'FFE4B5',
            'navajowhite' => 'FFDEAD',
            'peachpuff' => 'FFDAB9',
            'mistyrose' => 'FFE4E1',
            'lavenderblush' => 'FFF0F5',
            'linen' => 'FAF0E6',
            'oldlace' => 'FDF5E6',
            'papayawhip' => 'FFEFD5',
            'seashell' => 'FFF5EE',
            'mintcream' => 'F5FFFA',
            'slategray' => '708090',
            'slategrey' => '708090',
            'lightslategray' => '778899',
            'lightslategrey' => '778899',
            'lightsteelblue' => 'B0C4DE',
            'lavender' => 'E6E6FA',
            'floralwhite' => 'FFFAF0',
            'aliceblue' => 'F0F8FF',
            'ghostwhite' => 'F8F8FF',
            'honeydew' => 'F0FFF0',
            'ivory' => 'FFFFF0',
            'azure' => 'F0FFFF',
            'snow' => 'FFFAFA',
            'dimgray' => '696969',
            'dimgrey' => '696969',
            'darkgray' => 'A9A9A9',
            'darkgrey' => 'A9A9A9',
            'silver' => 'C0C0C0',
            'lightgray' => 'D3D3D3',
            'lightgrey' => 'D3D3D3',
            'gainsboro' => 'DCDCDC',
            'whitesmoke' => 'F5F5F5',
        ];
        
        // Check if input is a named color (case-insensitive)
        $lowerColour = strtolower(trim($colour));
        if (isset($namedColors[$lowerColour])) {
            return $namedColors[$lowerColour];
        }
        
        // Not a named color, return original value
        return $colour;
    }
}

