package color;
use strict;

# see http://en.wikipedia.org/wiki/HSL_and_HSV and the like
# these are sensible algorithms grounded in color logic
sub getColorData {
    my($R, $G, $B) = @_;
    my $c = {};

    $c->{R} = $R; # sRGB red component 0..255
    $c->{G} = $G; # sRGB green component 0..255
    $c->{B} = $B; # sRGB blue component 0..255

    my $M = $R > $G ? $R > $B ? $R : $B > $G ? $B : $G : $B > $G ? $B : $G;
    my $m = $R < $G ? $R < $B ? $R : $B < $G ? $B : $G : $B < $G ? $B : $G;
    my $C = $M - $m;

    $c->{M} = $M; # HSV Value 0..255
    $c->{C} = $C; # Chroma 0..255

    if ($C == 0) {
        $c->{H} = 0;
    } else {
        my $H;
        if ($M == $R) {
            $H = 60 * ((($G-$B)/$C) + 0);
        } elsif ($M == $G) {
            $H = 60 * ((($B-$R)/$C) + 2);
        } else { # ($M == $B) 
            $H = 60 * ((($R-$G)/$C) + 4);
        }
        $H += 360 if $H < 0;
        $c->{H} = $H;
    }

    # Luma (Y') with sRGB (Rec. 709) primaries
    $c->{Y} = 0.21*$R + 0.72*$G + 0.07*$B;

    return $c;
}

# the hue adjuster just clips out certain sections of the hue spectrum
my $hueClipInput = [];
my $hueClipOutput = [];
my $maxAdjustedHue = 360;
sub initHueAdjuster {
    # the values are empirical
    my $hueClipBoundaries = [[0, 15], [55, 70], [75, 85], [90, 150], [160, 170], [185, 205], [230, 250], [255, 265], [270, 280], [290, 325]];
    die unless $hueClipBoundaries->[0]->[0] == 0; # algorithm would have to be different if not true
    my $currentMax = 0;
    my $lastMax = 0;
    foreach my $subrange (@$hueClipBoundaries) {
        push(@$hueClipInput, $subrange->[0]);
        push(@$hueClipInput, $subrange->[1]);
        $maxAdjustedHue -= $subrange->[1] - $subrange->[0];
        $currentMax += $subrange->[0] - $lastMax;
        push(@$hueClipOutput, $currentMax);
        $lastMax = $subrange->[1];
    }
}
initHueAdjuster();

sub adjustHue {
    my($hue) = @_;
    my $min = 0;
    my $max = scalar @$hueClipInput;
    my $pos = int($max/2);
    while ($pos > $min) {
        if ($hue < $hueClipInput->[$pos]) {
            $max = $pos;
        } else {
            $min = $pos;
        }
        $pos = $min + int(($max - $min) / 2);
    }
    if ($pos % 2 == 0) {
        $hue = $hueClipOutput->[$pos/2];
    } else {
        $hue = ($hue - $hueClipInput->[$pos]) + $hueClipOutput->[int($pos/2)];
    }
    return $hue;
}

# this is a whole bunch of magical heuristics
sub similar {
    my $c = getColorData($_[0]->{red}, $_[0]->{green}, $_[0]->{blue});
    $c->{H} = adjustHue($c->{H});
    my $s = getColorData($_[1]->{red}, $_[1]->{green}, $_[1]->{blue});
    $s->{H} = adjustHue($s->{H});

    my $DRGB = abs($c->{R}-$s->{R}) + abs($c->{G}-$s->{G}) + abs($c->{B}-$s->{B});
    return 1 if $DRGB < 90;

    my $DMS = abs($s->{M}**2-$c->{M}**2)/255;
    return 0 if $DMS > 100;

    my $DY = abs($s->{Y}-$c->{Y});
    return 0 if $DY > 70;
    return 1 if $DRGB + $DY < 75;

    my $DC = abs($s->{C}-$c->{C});
    my $MC = ($s->{C}+$c->{C})/2;
    my $DM = abs($s->{M}-$c->{M});
    if ($DC < 50 and $MC < 40) {
        return 1 if $DM < 100;
    }
    return 0 if $DC > 100 and $DM > 40;

    my $minC = $c->{C} < $s->{C} ? $c->{C} : $s->{C};
    return 0 if $minC < 20 and $DC > 90;

    my $DH = abs($s->{H}-$c->{H});
    my $MM = ($s->{M}+$c->{M})/2;
    if ($DH > $maxAdjustedHue/2) {
        $DH = $maxAdjustedHue - $DH;
    }
    if ($DM < 128 and $MM < 128) {
        $DH *= ($MM/128)**2;
    }
    return 1 if $DH <= 2;
    return 0 if $DC + $DY > 170;
    return 1 if $DH <= 6;
    return 0;
}

sub pi() { 4 * atan2(1, 1) }
sub randomComponent() { return (cos(pi*rand(1))+1)*128; }

my @goodColors = ([255,0,0],
                  [0,255,0],
                  [0,0,255],
                  [255,255,0],
                  [255,0,255],
                  [0,255,255],
                  [255,128,0],
                  [255,255,255]);

sub new {
    my($colors) = @_;
    my $attempt = -1;
    search: for (;;) {
        $attempt += 1;
        my $color;
        if ($attempt < scalar @goodColors) {
            $color = {
                red => $goodColors[$attempt]->[0],
                green => $goodColors[$attempt]->[1],
                blue => $goodColors[$attempt]->[2],
            };
        } else {
            $color = {
                red => randomComponent(),
                green => randomComponent(),
                blue => randomComponent(),
            };
        }
        foreach (@$colors) {
            redo search if similar($color, $_);
        }
        return $color;
    }
}

1;
