#!/usr/bin/perl
use strict;
use GD;
use lib '/home/dave/lego/bricklink';
use lib '/home/dave/perl';
use Text::Unidecode;
use bl;
use colorlib;

require "../cgi-lib.pl";

our(%params,@params);
our(%palette);
our($img);
ReadParse(*params);

print "Content-Type: image/png\n\n";

#Defaults and initialization
my $maxwidth = 640;
my $maxheight = 480;
my $left_offset = 55;
my $top_offset = 15;
my $bot_offset = 85;
my $right_offset = 10;
my $img_width = $maxwidth+$left_offset+$right_offset;
my $img_height = $maxheight+$top_offset+$bot_offset;

$img = new GD::Image($img_width,$img_height,1);
$img->fill(1,1,color(255,255,255));

my(%colors);
$colors{incomplete}[0] = color(255,187,187);
$colors{incomplete}[1] = color(255,223,223);
$colors{new}[0] = color(0,191,0);
$colors{new}[1] = color(127,255,127);
$colors{used}[0] = color(127,63,191);
$colors{used}[1] = color(191,127,255);

my($id) = $params{id};
my($type);
my($cyear,$cmonth);
my(@lt) = localtime;
$cyear = $lt[5]+1900;
$cmonth = $lt[4]+1;
if($id =~ /^\b[SGPBOIMC]:(\w+)\-(\d+)$/) {
    #normal!
    $type = $1;
    $id = "$2-$3";
} elsif($params{type} =~ /^[SGPBOIMC]$/) {
    $type = $params{type};
    if($type =~ /[SOI]/ && $id !~ /\-\d+$/) {
	#assume it's a singular instance
	$id .= "-1";
    }
} elsif($id =~ /^\b(\w+)\-(\d+)$/) {
    #assume it's a set
    $type = "S";
} elsif($id =~ /^\b(\w+)$/) {
    #getting sketchy
    $type = "S";
    $id .= "-1";
} else {
    error("Unrecognized ID format");
}

my($catalog) = bl::get_bl_catalog($type);
if(!$catalog->{$id}) {
    error("Unknown item: $id");
}
#Read in data
my $ret = bl::read_sales_data($id,$type);
convert_currencies($ret);
if($params{cur_only}) {
    foreach my $date (keys %{$ret}) {
	@{$ret->{$date}} = grep { ($_->{cur} eq $params{currency}) ? 1 : 0 } @{$ret->{$date}};
    }
}
my($first) = 0;
my($last) = 0;
my @dt = (sort {$a <=> $b;} (keys %{$ret}));
my($year,$mon) = unpack("a4a2",$dt[0]);

if($year < 2000) {
    #no valid sale data-- BL didn't exist until 2000
    error("No sale data available for $id");
}
#Get list of dates from oldest date to present date
my @dates = ();
my(@data,%totals);
while(1) {
    last if($year > $cyear || ($mon > $cmonth && $year == $cyear));
    push @dates,sprintf("%04d%02d",$year,$mon);
    $mon++;
    if($mon > 12) {$mon = 1; $year++; }
}
#parse each sale, and categorize it
my($maxprice) = 0;
for(my $i=0;$i<@dates;$i++) {
    foreach my $sale (@{$ret->{$dates[$i]}}) {
	#'used' / 'new'
	#'C' (complete) / 'I' (incomplete)
	my($cat);
	if($sale->{complete} ne "C") {
	    $cat = "incomplete";
	} elsif($sale->{cond} eq "new") {
	    $cat = "new";
	} else {
	    $cat = "used";
	}
	if($params{qty} =~ /[0nN]/) {
	    push @{$data[$i]{$cat}{prices}},$sale->{price};
	    $totals{$cat}++;
	} else {
	    for(1 .. $sale->{qty}) {
		push @{$data[$i]{$cat}{prices}},$sale->{price};
	    }
	    $totals{$cat} += $sale->{qty};
	}
	if($sale->{price} > $maxprice) {
	    $maxprice = $sale->{price};
	}
    }
}
#Calculate weighted average

my($wavg_nmonths) = 5; #number of months forward and backwards to calculate weighted average

#Try and adjust the number of months forward/backward for weighted average
#based on volatility. A set that sells less often should have a larger timeframe
#to avoid looking bizarrely volatile. Default is 5 months, or however many months
#it would likely take to get 10 sales
my(%wavg_nmonths) = ();
foreach my $cat ("new","incomplete","used") {
    my($sales_per_month) = $totals{$cat} / scalar(@dates);
    if(!$sales_per_month) {
	$wavg_nmonths{$cat} = scalar(@dates);
    } elsif((10/$sales_per_month) < 5) {
	$wavg_nmonths{$cat} = 5;
    } else {
	#$wavg_nmonths{$cat} = 5;
	$wavg_nmonths{$cat} = int(10/$sales_per_month);
    }
}


for(my $i=0;$i<@dates;$i++) {
    foreach my $cat ("new","incomplete","used") {
	my($tot) = 0;
	my($qty) = 0;
	foreach my $j (1..$wavg_nmonths{$cat}) {
	    if(($i-$j) >= 0) {
		foreach my $price (@{$data[$i-$j]{$cat}{prices}}) { $tot += $price * ($wavg_nmonths{$cat}-$j); $qty += ($wavg_nmonths{$cat}-$j); }
	    }
	}
	foreach my $price (@{$data[$i]{$cat}{prices}}) { $tot += $price * $wavg_nmonths{$cat}; $qty += $wavg_nmonths{$cat}; }
	foreach my $j (1..$wavg_nmonths{$cat}) {
	    if(($i+$j) <= $#dates) {
		foreach my $price (@{$data[$i+$j]{$cat}{prices}}) { $tot += $price * ($wavg_nmonths{$cat}-$j); $qty += ($wavg_nmonths{$cat}-$j); }
	    }
	}
	$data[$i]{$cat}{wqty} = $qty;
	if($qty) {
	    $data[$i]{$cat}{wavg} = $tot/$qty;
	} else {
	    $data[$i]{$cat}{wavg} = undef;
	}
    }
}

#Title
#my($graphtitle) = "$id - ".unidecode($catalog->{$id}{name});
my($graphtitle) = "$id - $catalog->{$id}{name}";
#$img->string(gdSmallFont,int(($img_width/2)-5*length($graphtitle)/2),0,$graphtitle,color(0,0,0));

{
    my($fontsize) = 10;
    my($font) = '/home/dave/public_html/tmp/fonts/arial.ttf';
    my @bounds = GD::Image->stringFT(color(0,0,0),$font,$fontsize,0,100,100,$graphtitle,{});
    my($twidth) = $bounds[2] - $bounds[6] + 1;
    my($theight) = $bounds[3] - $bounds[7] + 1;
    #$img->string(gdTinyFont,100,100,"$bounds[6],$bounds[7] <-> $bounds[2],$bounds[3]",color(255,0,0));
    $img->stringFT(color(0,0,0),$font,$fontsize,0,int(($img_width/2)-($twidth/2)),(100-$bounds[7]),$graphtitle,{});
}

#$img->string(gdSmallFont,int(($img_width/2)-5*length($graphtitle)/2),0,$graphtitle,color(0,0,0));

#Graph Boundary Bars
$img->line($left_offset,$top_offset,$left_offset,$img_height-$bot_offset,color(0,0,0));
$img->line($left_offset,$img_height-$bot_offset,$img_width-$right_offset,$img_height-$bot_offset,color(0,0,0));

#Vertical Scale bars
my(@possible_scales) = (0.10,0.20,0.25,0.50,
			   1,   2,2.50,   5,
			  10,  20,  25,  50,
			 100, 200, 250, 500,
			1000,2000,2500,5000);
my($scale);
foreach my $pscale (reverse @possible_scales) {
    if($maxprice/$pscale > 5 && $maxheight/($maxprice/$pscale) > 20) {
	$scale = $pscale;
    }
}
$maxprice = (int($maxprice/$scale) + 1)*$scale;
for(my $v=$scale;$v<=$maxprice;$v+=$scale) {
    my($y) = $top_offset+$maxheight-(int(($v/$maxprice)*$maxheight));
    my($price) = sprintf("%0.2f",$v);
    if($params{currency} eq "USD" || !$params{currency}) {
	$price = '$' . $price;
    }
    $img->line($left_offset+1,$y,$img_width-$right_offset,$y,color(239,239,239));

    my($fontsize) = 7;
    my($font) = '/home/dave/public_html/tmp/fonts/arial.ttf';
    my @bounds = GD::Image->stringFT(color(0,0,0),$font,$fontsize,0,100,100,$price,{});
    my($twidth) = $bounds[2] - $bounds[6] + 1;
    my($theight) = $bounds[3] - $bounds[7] + 1;
    $img->stringFT(color(0,0,0),$font,$fontsize,0,$left_offset-5-$twidth,$y-($theight/2)+(100-$bounds[7]),$price,{});
    
    #$img->string(gdTinyFont,$left_offset-5-5*(length($price)),$y-4,$price,color(0,0,0));
}

#Key
my($kx) = $left_offset+220;
my($ky) = $img_height-10;
foreach my $t ("new","used","incomplete") {
    $img->line($kx,$ky,$kx+20,$ky,$colors{$t}[0]);
    $kx+=25;
    my $string = $t;
    $string =~ s/^(\w)/uc($1)/e;
    $totals{$t} ||= 0;
    $string .= " ($totals{$t})";

    my($fontsize) = 8;
    my($font) = '/home/dave/public_html/tmp/fonts/arial.ttf';
    my(@bounds) = GD::Image->stringFT(color(0,0,0),$font,$fontsize,0,100,100,$string,{});
    my($twidth) = $bounds[2] - $bounds[6] + 1;
    my($theight) = $bounds[3] - $bounds[7] + 1;
    $img->stringFT(color(0,0,0),$font,$fontsize,0,int($kx+(100-$bounds[6])),int($ky-($theight/2)+(100-$bounds[7])+1),$string,{});
    $kx += $twidth + 20;

    #$img->string(gdTinyFont,$kx,$ky-3,$string,color(0,0,0));
    #$kx+=length($string)*5 + 20;
}


my($di) = 1;
my($lastx) = 0;
foreach my $date (@data) {
    my($x) = int($di*($maxwidth/(scalar @data)));
    #Horizontal scale
    $img->line($left_offset+$x,$top_offset,$left_offset+$x,$img_height-$bot_offset-1,color(239,239,239));
    my($year,$mon) = unpack("A4A2",$dates[$di-1]);
    my($month) = ("?","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$mon];
    my $dstring = "$month $year";

    my($fontsize) = 7;
    my($font) = '/home/dave/public_html/tmp/fonts/arial.ttf';
    my @bounds = GD::Image->stringFT(color(0,0,0),$font,$fontsize,0,100,100,$dstring,{});
    my($twidth) = $bounds[2] - $bounds[6] + 1;
    my($theight) = $bounds[3] - $bounds[7] + 1;
    $img->stringFT(color(0,0,0),$font,$fontsize,3.14159/2,$left_offset+$x-($theight/2)+(100-$bounds[7]),$img_height-$bot_offset+$twidth+(100-$bounds[6]),$dstring,{});

    #$img->stringUp(gdTinyFont,$left_offset+$x-4,$img_height-$bot_offset+2+5*length($dstring),$dstring,color(0,0,0));

    foreach my $cat (keys %{$date}) {
	#Figure out how many sales fall onto the same pixel-height (so we could make higher-count pixels larger)
	foreach my $price (@{$date->{$cat}{prices}}) {
	    my($y) = int((($maxprice - $price)/$maxprice)*$maxheight);
	    $date->{$cat}{points}{$y}++;
	}
	my($color1,$color2);
	$color1 = $colors{$cat}[0];
	$color2 = $colors{$cat}[1];
	foreach my $point (keys %{$date->{$cat}{points}}) {
	    draw_dot($x+$left_offset,$point+$top_offset,$color1,$color2);
	    #$img->setPixel($x+$left_offset,$point+$top_offset,$color);
	}
	#Draw weighted average line
	if(defined($date->{$cat}{wavg}) && $di>1 && defined($data[$di-2]{$cat}{wavg})) {
	    my($wqty) = ($date->{$cat}{wqty} + $data[$di-2]{$cat}{wqty}) / 2;
	    my($color) = $color1;
	    my($base) = 12;
	    if($wqty < $base) {
		my $str = ($base-$wqty)/$base;
		my($r,$g,$b) = get_rgb($color1);
		my($h,$s,$l) = colorlib::rgb_to_hsl($r,$g,$b);
		$l += (1-$l)*$str;
		($r,$g,$b) = colorlib::hsl_to_rgb($h,$s,$l);
		$color = color($r,$g,$b);
	    }
	    my($y1) = int((($maxprice - $date->{$cat}{wavg})/$maxprice)*$maxheight);
	    my($y2) = int((($maxprice - $data[$di-2]{$cat}{wavg})/$maxprice)*$maxheight);
	    
	    #$img->line($x+$left_offset,$y1+$top_offset,$lastx+$left_offset,$y2+$top_offset,$color);
	    $img->setAntiAliased($color);
	    $img->line($x+$left_offset,$y1+$top_offset,$lastx+$left_offset,$y2+$top_offset,gdAntiAliased);
	}
    }
    $lastx = $x;
    $di++;
}

print $img->png;

sub convert_currencies {
    my($obj) = @_;
    my($cur) = bl::get_currencies();
    foreach my $date (keys %{$obj}) {
	foreach my $sale (@{$obj->{$date}}) {
	    if($sale->{cur} =~ /^[A-Z]{2}[A-Z]?$/) {
		#Foreign or explicit currency
		my($c) = $sale->{cur};
		if(length($c) == 2) {
		    $c .= "D";
		}
		$sale->{cur} = $c; #We need to make these consistent!
		if($cur->{$c}{$date}) {
		    #hooray!
		    if($params{currency} && $params{currency} ne "USD") {
			if($c ne $params{currency}) {
			    $sale->{price} /= $cur->{$c}{$date}; #convert to USD as intermediary
			    $sale->{price} *= $cur->{$params{currency}}{$date};
			}
		    } else {
			#Default to USD
			$sale->{price} /= $cur->{$c}{$date}; #converted to USD
		    }
		} else {
		    #Uh oh! Now what?
		    print STDERR "BL_CURRENCY_ERROR: no data for ($c,$date)\n";
		}
	    } else {
		#Listed as "Other" or "", but actually in USD, possibly converted to USD
		if($params{currency} && $params{currency} ne "USD") {
		    $sale->{price} *= $cur->{$params{currency}}{$date};
		}
	    }
	}
    }
}

sub draw_dot {
    my($x,$y,$color1,$color2) = @_;
    $img->setPixel($x,$y,$color1);
    $img->setPixel($x+1,$y,$color1);
    $img->setPixel($x-1,$y,$color1);
    $img->setPixel($x,$y+1,$color1);
    $img->setPixel($x,$y-1,$color1);
    $img->setPixel($x+1,$y+1,$color2);
    $img->setPixel($x+1,$y-1,$color2);
    $img->setPixel($x-1,$y+1,$color2);
    $img->setPixel($x-1,$y-1,$color2);
}

sub error {
    my($err) = @_;
    $img->string(gdSmallFont,30,30,"$err",color(255,0,0));
    print $img->png;
    exit(0);
}

sub get_rgb {
    my($obj) = @_;
    foreach my $rgb (keys %palette) {
	if($palette{$rgb} == $obj) {
	    return split /_/,$rgb;
	}
    }
    return(undef,undef,undef);
}

sub color {
    my($r,$g,$b) = @_;
    $r = standardize($r);
    $g = standardize($g);
    $b = standardize($b);
    if(!defined($palette{"$r\_$g\_$b"})) {
        $palette{"$r\_$g\_$b"} = $img->colorAllocate($r,$g,$b);
    }
    return $palette{"$r\_$g\_$b"};
}

sub standardize {
    my($v) = @_;
    $v = int($v);
    if($v < 0) { $v = 0; }
    if($v > 255) { $v = 255; }
    return $v;
}
