# <@LICENSE>
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to you under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </@LICENSE>

package Mail::SpamAssassin::HTML::Color;
use strict;
use warnings;
no warnings 'numeric';
use Carp qw(croak);
use overload '""' => sub { shift->as_hex }, fallback => 1;

my %html_color = (
    # HTML 4 defined 16 colors
    aqua => [0, 255, 255],
    black => [0, 0, 0],
    blue => [0, 0, 255],
    fuchsia => [255, 0, 255],
    gray => [128, 128, 128],
    green => [0, 128, 0],
    lime => [0, 255, 0],
    maroon => [128, 0, 0],
    navy => [0, 0, 128],
    olive => [128, 128, 0],
    purple => [128, 0, 128],
    red => [255, 0, 0],
    silver => [192, 192, 192],
    teal => [0, 128, 128],
    white => [255, 255, 255],
    yellow => [255, 255, 0],
    # colors specified in CSS3 color module
    aliceblue => [240, 248, 255],
    antiquewhite => [250, 235, 215],
    aquamarine => [127, 255, 212],
    azure => [240, 255, 255],
    beige => [245, 245, 220],
    bisque => [255, 228, 196],
    blanchedalmond => [255, 235, 205],
    blueviolet => [138, 43, 226],
    brown => [165, 42, 42],
    burlywood => [222, 184, 135],
    cadetblue => [95, 158, 160],
    chartreuse => [127, 255, 0],
    chocolate => [210, 105, 30],
    coral => [255, 127, 80],
    cornflowerblue => [100, 149, 237],
    cornsilk => [255, 248, 220],
    crimson => [220, 20, 60],
    cyan => [0, 255, 255],
    darkblue => [0, 0, 139],
    darkcyan => [0, 139, 139],
    darkgoldenrod => [184, 134, 11],
    darkgray => [169, 169, 169],
    darkgreen => [0, 100, 0],
    darkkhaki => [189, 183, 107],
    darkmagenta => [139, 0, 139],
    darkolivegreen => [85, 107, 47],
    darkorange => [255, 140, 0],
    darkorchid => [153, 50, 204],
    darkred => [139, 0, 0],
    darksalmon => [233, 150, 122],
    darkseagreen => [143, 188, 143],
    darkslateblue => [72, 61, 139],
    darkslategray => [47, 79, 79],
    darkturquoise => [0, 206, 209],
    darkviolet => [148, 0, 211],
    deeppink => [255, 20, 147],
    deepskyblue => [0, 191, 255],
    dimgray => [105, 105, 105],
    dodgerblue => [30, 144, 255],
    firebrick => [178, 34, 34],
    floralwhite => [255, 250, 240],
    forestgreen => [34, 139, 34],
    gainsboro => [220, 220, 220],
    ghostwhite => [248, 248, 255],
    gold => [255, 215, 0],
    goldenrod => [218, 165, 32],
    greenyellow => [173, 255, 47],
    honeydew => [240, 255, 240],
    hotpink => [255, 105, 180],
    indianred => [205, 92, 92],
    indigo => [75, 0, 130],
    ivory => [255, 255, 240],
    khaki => [240, 230, 140],
    lavender => [230, 230, 250],
    lavenderblush => [255, 240, 245],
    lawngreen => [124, 252, 0],
    lemonchiffon => [255, 250, 205],
    lightblue => [173, 216, 230],
    lightcoral => [240, 128, 128],
    lightcyan => [224, 255, 255],
    lightgoldenrodyellow => [250, 250, 210],
    lightgray => [211, 211, 211],
    lightgreen => [144, 238, 144],
    lightpink => [255, 182, 193],
    lightsalmon => [255, 160, 122],
    lightseagreen => [32, 178, 170],
    lightskyblue => [135, 206, 250],
    lightslategray => [119, 136, 153],
    lightsteelblue => [176, 196, 222],
    lightyellow => [255, 255, 224],
    limegreen => [50, 205, 50],
    linen => [250, 240, 230],
    magenta => [255, 0, 255],
    mediumaquamarine => [102, 205, 170],
    mediumblue => [0, 0, 205],
    mediumorchid => [186, 85, 211],
    mediumpurple => [147, 112, 219],
    mediumseagreen => [60, 179, 113],
    mediumslateblue => [123, 104, 238],
    mediumspringgreen => [0, 250, 154],
    mediumturquoise => [72, 209, 204],
    mediumvioletred => [199, 21, 133],
    midnightblue => [25, 25, 112],
    mintcream => [245, 255, 250],
    mistyrose => [255, 228, 225],
    moccasin => [255, 228, 181],
    navajowhite => [255, 222, 173],
    oldlace => [253, 245, 230],
    olivedrab => [107, 142, 35],
    orange => [255, 165, 0],
    orangered => [255, 69, 0],
    orchid => [218, 112, 214],
    palegoldenrod => [238, 232, 170],
    palegreen => [152, 251, 152],
    paleturquoise => [175, 238, 238],
    palevioletred => [219, 112, 147],
    papayawhip => [255, 239, 213],
    peachpuff => [255, 218, 185],
    peru => [205, 133, 63],
    pink => [255, 192, 203],
    plum => [221, 160, 221],
    powderblue => [176, 224, 230],
    rosybrown => [188, 143, 143],
    royalblue => [65, 105, 225],
    saddlebrown => [139, 69, 19],
    salmon => [250, 128, 114],
    sandybrown => [244, 164, 96],
    seagreen => [46, 139, 87],
    seashell => [255, 245, 238],
    sienna => [160, 82, 45],
    skyblue => [135, 206, 235],
    slateblue => [106, 90, 205],
    slategray => [112, 128, 144],
    snow => [255, 250, 250],
    springgreen => [0, 255, 127],
    steelblue => [70, 130, 180],
    tan => [210, 180, 140],
    thistle => [216, 191, 216],
    tomato => [255, 99, 71],
    turquoise => [64, 224, 208],
    violet => [238, 130, 238],
    wheat => [245, 222, 179],
    whitesmoke => [245, 245, 245],
    yellowgreen => [154, 205, 50],
);

sub new {
    my ($class, $color) = @_;
    my $self = [];
    bless $self, $class;

    croak("Color value is required") unless (defined $color);
    $color =~ s/^\s+|\s+$//g;  # Trim whitespace
    $color = lc($color);

    # If color is 'transparent', set all values to 0
    if ($color eq 'transparent') {
        @$self = (0, 0, 0, 0);
        return $self;
    }

    # Check if color is a named color
    if (exists $html_color{$color}) {
        @$self = (@{ $html_color{$color} }, 1);
        return $self;
    }

    # Check if color is in hexadecimal format (#000 or #aabbcc)
    if ($color =~ /^#([0-9a-f]{3}|[0-9a-f]{6})$/) {
        my $hex = length($1) == 3
                ? join('', map { $_ x 2 } split //, $1)
                : $1;
        @$self = map { hex($_) } $hex =~ /../g;
        push @$self, 1;
        return $self;
    }

    # Check if color is in RGB format (rgb(255, 0, 153) or rgb(255 0 153 / 80%))
    if ($color =~ /^rgba?\s*\((.*)\)$/) {
        my @args = split(/[ ,\/]+/, $1);
        push @args, 1 if @args == 3;
        croak("Invalid number of arguments for RGB color") unless @args == 4;
        for (@args) {
            croak("Invalid RGB value: $_") unless $_ =~ /^(?:none|[+-]?\d*\.?\d+%?)$/;
        }
        my ($r, $g, $b) = map {
            /^(.*)%$/ ? _round($1 * 255 / 100) : $_ + 0;
        } @args[0..2];
        $a = $args[3];
        $a = $a =~ s/%$// ? $a / 100 : $a + 0;
        @$self = ($r, $g, $b, $a);
        return $self;
    }

    # Check if color is in HSL format (hsl(360, 100%, 50%) or hsl(360 100% 50% / 80%))
    if ($color =~ /^hsla?\s*\((.*)\)$/) {
        my @args = split(/[ ,\/]+/, $1);
        push @args, 1 if @args == 3;
        croak("Invalid number of arguments for HSL color") unless @args == 4;
        for (@args[1..3]) {
            croak("Invalid HSL value: $_") unless $_ =~ /^(?:none|[+-]?\d*\.?\d+%?)$/;
        }
        my ($h, $s, $l, $a) = @args;
        $h = _parse_angle($h);
        $s =~ s/%$//; $s /= 100;
        $l =~ s/%$//; $l /= 100;
        $a = $a =~ s/%$// ? $a / 100 : $a + 0;

        @$self = _hsl_to_rgb($h, $s, $l);
        push @$self, $a;
        return $self;
    }

    # Check if color is in HWB format (hwb(240, 100%, 0%) or hwb(240 100% 0% / 80%))
    if ($color =~ /^hwba?\s*\((.*)\)$/) {
        my @args = split(/[ ,\/]+/, $1);
        push @args, 1 if @args == 3;
        croak("Invalid number of arguments for HWB color") unless @args == 4;
        for (@args[1..3]) {
            croak("Invalid HWB value: $_") unless $_ =~ /^(?:none|[+-]?\d*\.?\d+%?)$/;
        }
        my ($h, $wh, $bl, $a) = @args;
        $h = _parse_angle($h);
        $wh =~ s/%$//; $wh /= 100;
        $bl =~ s/%$//; $bl /= 100;
        $a = $a =~ s/%$// ? $a / 100 : $a + 0;

        @$self = _hwb_to_rgb($h, $wh, $bl);
        push @$self, $a;
        return $self;
    }

    croak("Unsupported color format: $color");
}

sub blend {
    my ($self, $background) = @_;
    my ($r, $g, $b, $a) = @$self;
    return $self if $a == 1;
    my ($br, $bg, $bb) = @$background;

    my $new_r = _round(($r * $a) + ($br * (1 - $a)));
    my $new_g = _round(($g * $a) + ($bg * (1 - $a)));
    my $new_b = _round(($b * $a) + ($bb * (1 - $a)));

    @$self = ($new_r, $new_g, $new_b, 1);
    return $self;
}

sub distance {
    my ($self, $other_color) = @_;
    my ($r1, $g1, $b1) = @$self[0..2];
    my ($r2, $g2, $b2) = @$other_color[0..2];

    my $r = ($r1 - $r2);
    my $g = ($g1 - $g2);
    my $b = ($b1 - $b2);

    # geometric distance weighted by brightness
    # maximum distance is 191.151823601032
    my $distance = ((0.2126 * $r)**2 + (0.7152 * $g)**2 + (0.0722 * $b)**2)**0.5;

    return $distance;
}

sub as_hex {
    my ($self) = @_;
    my ($r, $g, $b) = @$self[0..2];
    return sprintf("#%02x%02x%02x", $r, $g, $b);
}

sub as_array {
    my ($self) = @_;
    return @$self;
}

#
# Static Private Methods
#

sub _hsl_to_rgb {
    my ($hue, $saturation, $lightness) = @_;

    # Ensure the hue is between 0-360 degrees and S, L are between 0 and 1
    $hue %= 360;
    $saturation = 0 if $saturation < 0;
    $lightness = 0 if $lightness < 0;
    $saturation = 1 if $saturation > 1;
    $lightness = 1 if $lightness > 1;

    my $c = (1 - abs(2 * $lightness - 1)) * $saturation;
    my $x = $c * (1 - abs(($hue / 60) % 2 - 1));
    my $m = $lightness - $c / 2;
    my @rgb;

    if ($hue >= 0 && $hue < 60) {
        @rgb = ($c, $x, 0);
    } elsif ($hue >= 60 && $hue < 120) {
        @rgb = ($x, $c, 0);
    } elsif ($hue >= 120 && $hue < 180) {
        @rgb = (0, $c, $x);
    } elsif ($hue >= 180 && $hue < 240) {
        @rgb = (0, $x, $c);
    } elsif ($hue >= 240 && $hue < 300) {
        @rgb = ($x, 0, $c);
    } else {
        @rgb = ($c, 0, $x);
    }

    return map { _round(($_ + $m) * 255) } @rgb;
}

sub _hwb_to_rgb {
    my ($hue, $whiteness, $blackness) = @_;

    # Ensure the hue is between 0-360 degrees and W, B are between 0 and 1
    $hue %= 360;
    $whiteness = 0 if $whiteness < 0;
    $blackness = 0 if $blackness < 0;
    $whiteness = 1 if $whiteness > 1;
    $blackness = 1 if $blackness > 1;

    # Convert hue to range 0 to 1
    my $h = $hue / 60;  # Divide hue by 60 to put it in [0, 6)
    my $f = $h - int($h);
    my @rgb_base;

    # Get RGB base values based on hue
    if    ($h < 1) { @rgb_base = (1, $f, 0); }
    elsif ($h < 2) { @rgb_base = (1 - $f, 1, 0); }
    elsif ($h < 3) { @rgb_base = (0, 1, $f); }
    elsif ($h < 4) { @rgb_base = (0, 1 - $f, 1); }
    elsif ($h < 5) { @rgb_base = ($f, 0, 1); }
    else           { @rgb_base = (1, 0, 1 - $f); }

    # Apply whiteness and blackness to compute final RGB values
    my $i = 1 - $whiteness - $blackness;
    my @rgb = map { _round(($whiteness + $i * $_) * 255) } @rgb_base;

    return @rgb;
}

sub _parse_angle {
    my ($angle) = @_;

    croak("Invalid color angle: $angle") unless $angle =~ /^(?:none|[+-]?\d*\.?\d+(?:deg|grad|rad|turn)?)$/;

    $angle = $angle =~ s/deg$// ? $angle
        : $angle =~ s/grad$// ? $angle * 360 / 400
        : $angle =~ s/rad$// ? $angle * 180 / 3.14159
        : $angle =~ s/turn$// ? $angle * 360
        : $angle;

    return _round($angle) % 360;

}

sub _round {
    my ($value) = @_;
    return int($value + 0.5);
}

1;

__END__

=head1 NAME

Mail::SpamAssassin::HTML::Color - A class to parse and manipulate CSS color values

=head1 SYNOPSIS

  use Mail::SpamAssassin::HTML::Color;

  my $color = Mail::SpamAssassin::HTML::Color->new('rgba(255, 0, 153, 0.5)');
  $color->blend([255, 255, 255]);
  my $distance = $color->distance([0, 0, 0]);
  print "$color";  # Outputs the color as a hex string

=head1 DESCRIPTION

This class provides methods to parse various CSS color formats, blend them with a background color, calculate the distance between two colors, and convert the color to a hex string.

=head1 METHODS

=head2 new($color)

Creates a new color object from a CSS color string.

=head2 blend($background)

Blends the color with the given background color. Modifies the color in-place and returns the modified object.

=head2 distance($other_color)

Calculates the distance between the current color and another color using a brightness-weighted geometric formula.

=head2 as_hex

Returns the color as a hex string with a leading '#'.

=head2 as_array

Returns the color as an array of RGB values.

=cut
