#!/usr/bin/env perl
#############################################################################
# gd_colormix_graphic.psgi
##
# Service that creates the graphic and nothing else.
# This brings the original project back to it's root of being a GD library
# demonstration.
#############################################################################
use strict;

#############################################################################
# Plack -- Web Server Interface (not the point of this program).
use Encode;
use Plack::Request;
use Plack::Response;
#############################################################################

#############################################################################
# GD.pm -- This is the most common Perl On-The-Fly Graphics lib.
use GD;
# For Debian - we need - apt-get install libgd-gd2-perl
#   -- This is what _I_ installed though...
#   apt-get install libgd-gd2-perl libgd-tools libgd2-xpm-dev
# For RedHat - you have to get and install it yourself.  CPAN works
#   works just fine.  However, you first need to download and install
#   libgd and libxpm (unless you don't want to add XPM support).
# For Docker - (based on perl image), we need
#       RUN apt-get update
#       RUN apt-get install -y libgd-perl
#############################################################################

#############################################################################
# Color ... local libraries to support this project
use lib q{.};
## Color::Geometry -- cMixGeom, hold the internal Geometry of the color box
## Functions: 
###    cMixGeom
use Color::Geometry qw{cMixGeom};
#############################################################################

my $SVNr = '$Rev$';
$SVNr =~ s/^\$Rev: //;
$SVNr =~ s/ \$$//;
my $VERSION = "1.0.$SVNr";

#############################################################################
## opt
## ########
## $opt->{'env'} -- env as passed by PSGI)
## $opt->{'req'} -- Plack::Request->new()
## $opt->{'type'} -- Body Type
## $opt->{'errstr'} -- Errors
## $opt->{'warnstr'} -- Warnings will only be printed if type remains html
## $opt->{'body'} -- String to output
## $opt->{'wanted'} -- from path_info
## $opt->{'red'}        -- red value
## $opt->{'green'}      -- green value
## $opt->{'blue'}       -- blue value
## $opt->{'height'}     -- image height
## $opt->{'width'}      -- image width
#############################################################################

my $app = sub {
    my $opt = {};
    $opt->{'env'} = shift;
    $opt->{'type'} = 'text/html';
    my $req = Plack::Request->new($opt->{'env'});
    my $res = Plack::Response->new(200);
    $opt->{'req'} = $req;

    # This ALSO initializes other things, it's important to run near the top.
    fillOpt($opt);

    process($opt);

    $res->content_type( $opt->{'type'} );
    $res->body( $opt->{'body'} );
    return $res->finalize();
};

#############################################################################
# THIS runs at startup.  Things that only need to be done once.
#############################################################################
INIT: {
    return $app;
}


sub process {
    my $opt = shift;
    my $wanted = $opt->{'wanted'};
    $wanted =~ s{^/*}{};

    colorMix($opt);
}


sub fillOpt
{
    my $opt = shift;
    my $req = $opt->{'req'};

    my $wanted = $req->path_info();
    $opt->{'wanted'} = $wanted;

    my @colors = ();
    my ( $height, $width) = ();

OPT:
    foreach my $arg ( split( m{[/,]}, $wanted ) ) {
        if ( $arg =~ m/^$/ ) {
            next OPT;
        }
        elsif ( $arg =~ m/^h:(\d+)$/i ) {
            $height = $1;
        }
        elsif ( $arg =~ m/^w:(\d+)$/i ) {
            $width = $1;
        }
        elsif ( $arg =~ m/^r:(\S+)$/i ) {
            $colors[0] = numericInput($1);
        }
        elsif ( $arg =~ m/^g:(\S+)$/i ) {
            $colors[1] = numericInput($1);
        }
        elsif ( $arg =~ m/^b:(\S+)$/i ) {
            $colors[2] = numericInput($1);
        }
        elsif ( $arg =~ m/^c:([A-Fa-f0-9]+)$/i ) {
            my $rgb = $1;
            @colors = rgbHtmlStyle($rgb);
        }
        elsif ( $arg !~ m/^[a-z]:.+$/i ) {
            push @colors, ( numericInput($arg) );
        }
    }

    # Fix Height and Width Here...
    if ( $width && $height ) {
        if ( 100 > $height ) {
            $height = 100;
        }
        if ( 120 > $width ) {
            $width = 120;
        }
        if ( $width && $height ) {
            # Keep Larger offered value
            if ( ( 1.2 * $height ) > $width ) {
                $width = 0;
            }
            if ( ( 1.2 * $height ) < $width ) {
                $height = 0;
            }
        }
    }
    if ( !$width && !$height ) {
        $height = 600;
        $width = 1.2 * $height;
    }
    if ( !$width ) {
        if ( 100 <= $height ) {
            $width = 1.2 * $height;
        }
        else {
            # MINIMUM SIZE
            $height = 100;
            $width = 120;
        }
    }
    if ( !$height ) {
        if ( 120 <= $width ) {
            $height = $width / 1.2;
            if ( $height != int( $height ) ) {
                $height = 1 + int( $height );
            }
        }
        else {
            # MINIMUM SIZE
            $height = 100;
            $width = 120;
        }
    }

    foreach ( $colors[0,1,2] ) {
        if ( ! defined $_ ) {
            $_ = 255;
        }
    }

    my ($red, $green, $blue) = @colors;

    $opt->{'red'}        = $red;
    $opt->{'green'}      = $green;
    $opt->{'blue'}       = $blue;
    $opt->{'height'}     = $height;
    $opt->{'width'}      = $width;
}


############
# colorMix
#   Creates a color merging, three circle image
sub colorMix {
    my $opt   = shift;
    my $clRed = $opt->{'red'};
    my $clGreen = $opt->{'green'};
    my $clBlue = $opt->{'blue'};
    my $clHigh = $opt->{'height'};
    my $clWide = $opt->{'width'};

    if ( $clRed eq '' ) { $clRed = 255; };
    if ( $clGreen eq '' ) { $clGreen = 255; };
    if ( $clBlue eq '' ) { $clBlue = 255; };

    # Important ... Picture Square Size...
    # If this is below 200, the circlePercent range tightens closer to 55%
    # Below 100 - it gets too small to fit everything.
    my $squareSize = 600;
    if ( $clWide && ( 120 <= $clWide )) {
        $squareSize = int( $clWide / 1.2 );
    }
    elsif ( $clHigh && ( 100 <= $clHigh )) {
        $squareSize = $clHigh;
    }
    # Each Circle will be 60% of the size of the square.
    # Will work from 43% to 66%
    my $circlePercent = 0.6;

    # This is big, and uses a lot of variables.
    my ( $black, $white, $red, $blue, $green, $rg, $rb, $bg, $rgb );
    my ( $redX, $greenX, $blueX ) = (0,0,0);
    my ( $arcsize, $redY, $greenY, $blueY ) = (0,0,0,0);
    my ( $triLen, $triHigh ) = (0,0);
    my ( $panelX, $panelY ) = (0,0);
    my ( $panelWidX, $panelHighY ) = (0,0);
    my ( $panelWid, $panelHigh ) = (0,0);
    my ( $panelStep, $panelFY, $panelFNextY ) = (0,0,0);

    my $pic = 0;

    # pic is my GD Image object...
    # Note I pass in the width and height of the image.
    $pic = new GD::Image( ($squareSize * 1.20), $squareSize);
    $opt->{'pic'} = $pic;

    # Use GD's colorAllocate method to create 
    #   colors to use in my graphic...
    $black = $pic->colorAllocate(0,0,0);
    $white = $pic->colorAllocate(255,255,255);
    $red = $pic->colorAllocate($clRed,0,0);
    $green = $pic->colorAllocate(0,$clGreen,0);
    $blue = $pic->colorAllocate(0,0,$clBlue);
    $rb = $pic->colorAllocate($clRed,0,$clBlue);
    $rg = $pic->colorAllocate($clRed,$clGreen,0);
    $bg = $pic->colorAllocate(0,$clGreen,$clBlue);
    $rgb = $pic->colorAllocate($clRed,$clGreen,$clBlue);

    # Each Circle will be a percentage of the size of the square.
    $arcsize = int( $squareSize * $circlePercent );
    # Red Circle will be aligned at 1/3 horizontal width
    $redX = int( $squareSize / 3 );
    # Green Circle will be aligned at 2/3 horizontal width
    $greenX = int($squareSize - ( $squareSize / 3 ));
    # Blue Circle will be aligned at 1/2 horizontal width
    $blueX = int( $squareSize / 2 );
    # Circles will be aligned vertically to center the equilateral triangle.
    $triLen = int( $greenX - $redX );
    $triHigh = int( sqrt( ( $triLen**2 ) - (( $triLen/2 )**2 )));
    $redY = int( ($squareSize - $triHigh) / 2 );
    $greenY = $redY;
    $blueY = int( $squareSize - (( $squareSize - $triHigh) / 2 ));

    # Whole fill.. black.
    $pic->fill(1, 1, $black);

    # Create my circles
    $pic->arc($redX, $redY, $arcsize, $arcsize, 0, 360, $red);
    $pic->arc($greenX, $greenY, $arcsize, $arcsize, 0, 360, $green);
    $pic->arc($blueX, $blueY, $arcsize, $arcsize, 0, 360, $blue);

    # Fill Red...
    $pic->fill($redX, $redY, $red);
    # Fill Green...
    $pic->fill($greenX, $greenY, $green);
    # Fill Blue...
    $pic->fill($blueX, $blueY, $blue);
    # Fill Red Green...
    $pic->fill($blueX, ($squareSize - $blueY) - ($triHigh/3), $rg);
    # Fill Red Blue...
    #$pic->fill(200, 250, $rb);
    $pic->fill($redX, $blueY - ($triHigh/3), $rb);
    # Fill Green Blue...
    #$pic->fill(300, 250, $bg);
    $pic->fill($greenX, $blueY - ($triHigh/3), $bg);
    # Fill Center...
    $pic->fill($squareSize/2, $squareSize/2 - ($triHigh/6), $rgb);

    #########
    # Create the Info Panel...
    $panelX = $squareSize + 1;
    $panelY = 1;
    $panelWidX = int( $squareSize * 1.20) - 2;
    $panelWid = ( $panelWidX - $panelX );
    $panelHighY = ( $squareSize - 2 );
    $panelHigh = ( $panelHighY - $panelY );

    # Create Main Color Block
    $pic->rectangle($panelX + 1, $panelY + 1, 
    $panelWidX - 1, $panelWid - 1, $rgb);
    $pic->fill($panelX + 2, $panelY + 2, $rgb);

    # Create Fade
    $panelFY = $panelWid + 2;
    $panelStep = int( ( $panelHighY - $panelFY ) / 19 );
    $panelFNextY = $panelFY + $panelStep;

    foreach my $csteps ( 0 .. 19 ) {
        my $color = $pic->colorAllocate(
              percentShade(100 - (5 * (1 + $csteps))
                          , $clRed, $clGreen, $clBlue)
#              percentTint((5 * (1 + $csteps)), $clRed, $clGreen, $clBlue)
           );
        my @fourpts = cMixGeom($opt, 'shadebox', $csteps);
        $pic->rectangle( @fourpts, $color );
        $pic->fill( $fourpts[0] + 2, $fourpts[1] + 2, $color );

    }


    ############################
    # temp - construction lines
    ####
    # Center Crosshairs (visually helpful):
    #$pic->line($squareSize/2, 0, $squareSize/2, $squareSize, $black);
    # Center Of Circles Crosshairs:
    #$pic->line(0,$squareSize/2 - ($triHigh/6), $squareSize, $squareSize/2 - ($triHigh/6), $black);

    # Circle Center to Center Triangle (visually helpful):
    #$pic->line($redX, $redY, $greenX, $greenY, $black);
    #$pic->line($redX, $redY, $blueX, $blueY, $black);
    #$pic->line($blueX, $blueY, $greenX, $greenY, $black);

    # Inverted Triangle Centered on Center of Circles:
    #$pic->line($blueX, ($squareSize - $blueY) - ($triHigh/3), $greenX, $blueY - ($triHigh/3), $white);
    #$pic->line($blueX, ($squareSize - $blueY) - ($triHigh/3), $redX, $blueY - ($triHigh/3), $white);
    #$pic->line($greenX, $blueY - ($triHigh/3), $redX, $blueY - ($triHigh/3), $white);

     # Info and Shade Panel ( cMixGeom tests )
    #$pic->rectangle(cMixGeom($opt, 'infobox'), $white);
    #$pic->rectangle(cMixGeom($opt, 'colorbox'), $white);
    #foreach my $debugshadow ( 0 .. 19 ) {
    #    if ( 0 == $debugshadow % 2 ) {
    #        $pic->rectangle(cMixGeom($opt, 'shadebox', $debugshadow), $green);
    #    }
    #}
    #$pic->rectangle(cMixGeom($opt, 'shadebox', 0), $white);
    #$pic->rectangle(cMixGeom($opt, 'shadebox', 3), $white);
    #$pic->rectangle(cMixGeom($opt, 'shadebox', 5), $white);
    #$pic->rectangle(cMixGeom($opt, 'shadebox', 9), $white);
    #$pic->rectangle(cMixGeom($opt, 'shadebox', 19), $white);

    #################
    # Done, display!
    ####

    # print "Content-type: image/png\n\n";
    #print CGI::header('image/png');
    $opt->{'type'} = 'image/png';

    # Use GD's png method to print out the PNG version
    #   of my finished graphic.
    $opt->{'body'} = $pic->png;
}


sub rgbHtmlStyle
{
    my $str = shift;
    $str =~ s/^\s+//;
    $str =~ s/\s+$//;
    $str =~ s/^0x//;

    my @c  = ();
    my $ax = 0;

    if ( 3 == length($str) ) {
        $ax = 1;
    }
    elsif ( 6 == length($str) ) {
        $ax = 2;
    }
    else {
        return @c;
    }

    for ( my $cx = 0; $cx < ( 3*$ax ); $cx += $ax ) {
        my $num = substr($str,$cx,$ax);
        if ( 1 == length($num) ) {
            $num = "${num}${num}";
        }
        push @c, ( numericInput(q{0x}.${num}) );
    }

    return @c;
}


sub numericInput
{
    my $input = shift;
    if ( $input =~ m/^0x([0-9a-fA-F]+)$/ ) {
        $input = hex($1);
    }
    elsif ( $input =~ m/^#([0-9a-fA-F]+)$/ ) {
        $input = hex($1);
    }
    elsif ( $input =~ m/^(\d+)%$/ ) {
        $input = int( 2.55 * $input );
    }
    elsif ( int($input) != $input ) {
        if (( 0 < $input) && (1 > $input)) {
            $input = int( 255 * $input );
        }
        else {
            $input = 255;
        }
    }

    if ( $input < 0 ) {
        $input = 0;
    }
    elsif ( $input > 255 ) {
        $input = 255;
    }
    elsif ( $input !~ m/^\d+$/ ) {
        $input = 0;
    }

    return $input;
}

sub percentTint
{
    my $percent = shift;
    $percent = 100 - $percent;
    my ( $r, $g, $b ) = @_;

    foreach my $c ( $r, $g, $b ) {
        $c = $c + int( ( 255 - $c ) * ( $percent / 100 ) );
    }

    return ( $r, $g, $b );
}


sub percentShade
{
    my $percent = shift;
    my ( $r, $g, $b ) = @_;

    foreach my $c ( $r, $g, $b ) {
        $c = int( $c * ( $percent / 100 ) );
    }

    return ( $r, $g, $b );
}


# vim: sw=4 ts=4
# End gd_colormix.psgi.pl
