#!/usr/bin/perl
###############################################################################
#                                                                             #
#                                  top100.pl                                  #
#                                                                             #
#                     Generate diagrams for TOP1000 data                      #
#                                                                             #
#                                                                             #
#  (C) 2005-2009 Ullrich von Bassewitz                                        #
#                Roemerstrasse 52                                             #
#                D-70794 Filderstadt                                          #
#  EMail:        uz@musoftware.de                                             #
#                                                                             #
#                                                                             #
#  This software is provided 'as-is', without any expressed or implied        #
#  warranty.  In no event will the authors be held liable for any damages     #
#  arising from the use of this software.                                     #
#                                                                             #
#  Permission is granted to anyone to use this software for any purpose,      #
#  including commercial applications, and to alter it and redistribute it     #
#  freely, subject to the following restrictions:                             #
#                                                                             #
#  1. The origin of this software must not be misrepresented; you must not    #
#     claim that you wrote the original software. If you use this software    #
#     in a product, an acknowledgment in the product documentation would be   #
#     appreciated but is not required.                                        #
#  2. Altered source versions must be plainly marked as such, and must not    #
#     be misrepresented as being the original software.                       #
#  3. This notice may not be removed or altered from any source               #
#     distribution.                                                           #
#                                                                             #
###############################################################################



# Include stuff
use GD;
use Time::Local;
use Getopt::Long;


# ----------------------------------------------------------
#    			      Constants
# ----------------------------------------------------------


# Image stuff
my $Version  	 = "0.82";
my $Copyright    = "top1000.pl v$Version (C) 2006-2009 by Ullrich von Bassewitz";
my $YMax         = 500;
my $XMax         = 900;
my $TopBorder    = 40;
my $BottomBorder = 100 + gdLargeFont->width * length ("2005-01-01");
my $LeftBorder   = 100;
my $RightBorder  = 100;

# Command line options
$DataDir     	 = ".";
my $ImageName	 = "";
my $ImageTitle	 = "";
my %Systems      = ();
my $Days         = 90;
my $WeeklyTicks  = 0;
my $Thickness    = 1;


# ----------------------------------------------------------
#                            Data
# ----------------------------------------------------------



my %Ratings = ();
my $MaxDateOffs = 0;
my $Today;

# Image stuff
my $Image;
my $White;
my $Black;
my $Gray2;
my $Red;

# Legend
my $LegX;
my $LegY;
my $LegW;
my $LegFirstX;
my $LegFirstY;
my $LegLastY;



# ----------------------------------------------------------
#   	       	         Helper functions
# ----------------------------------------------------------



# Terminate with an error
sub Abort {
    print "@_\n";
    exit 1;
}



# Scale an X value (=offset) for the image
sub ScaleX {

    my $X = shift (@_);
    return $LeftBorder + $XMax - ($X * $XMax) / $Days;
}



# Scale an Y value (=rating) for the image
sub ScaleY {

    my $Y = shift (@_);
    return $TopBorder + ($Y * $YMax) / 1000;
}



# ----------------------------------------------------------
#      	       	          Create an image
# ----------------------------------------------------------

sub CreateImage {

    my $Host = shift (@_);
    my $Len, $Val;


    # Create a new image and make the background color white
    $Image  = new GD::Image ($LeftBorder + $XMax + $RightBorder,
                             $TopBorder + $YMax + $BottomBorder);
    $White  = $Image->colorAllocate (255, 255, 255);
    $Black  = $Image->colorAllocate (0, 0, 0);
    $Gray2  = $Image->colorAllocate (196, 196, 196);
    $Red    = $Image->colorAllocate (255, 0, 0);

    # Set the line thickness
    $Image->setThickness ($Thickness);

    # Setup a dashed line pattern
    $Image->setStyle ($Gray2, $Gray2, $Gray2, gdTransparent, gdTransparent, gdTransparent);

    # Draw the axis
    $Image->line ($LeftBorder, $TopBorder + $YMax, $LeftBorder, $TopBorder-10, $Black);
    $Image->line ($LeftBorder-10, $TopBorder + $YMax, $LeftBorder + $XMax+10, $TopBorder + $YMax, $Black);

    # TOP1000 rating ticks
    for (my $i = 100; $i >= 0; $i--) {
       	my $Y = $TopBorder + $YMax - $i * ($YMax / 100);
       	if (($i % 10) == 0) {
       	    # Display the rating
       	    $Len   = 5;
       	    $Val   = sprintf ("%d", 1000 - $i * 10);
	    $Width = length ($Val) * gdLargeFont->width;
    	    $TextY = $Y - gdLargeFont->height / 2;
	    $Image->string (gdLargeFont, $LeftBorder - $Width - 10, $TextY, $Val, $Black);
    	    # Line across the diagram
            if ($i != 0) {
    	        $Image->line ($LeftBorder + 1, $Y, $LeftBorder + $XMax + 5, $Y, gdStyled);
            }
       	} else {
       	    $Len = 2;
    	}
        if (($i % 5) == 0) {
            $Image->line ($LeftBorder - $Len, $Y, $LeftBorder, $Y, $Black);
        }
    }

    # Date ticks
    for (my $Offs = $Days; $Offs >= 0; $Offs--) {

        my $X = ScaleX ($Offs);
        my $Len = 2;

        $Date = $Today - ($Offs * 24 * 60 * 60);
        my ($sec,$min,$hour,$mday,$mon,$year,$wday,@rest) = localtime ($Date);

        # Dashed line across the diagram on each sunday or each month start
        if (($WeeklyTicks && $wday == 0) || (!$WeeklyTicks && $mday == 1)) {
            # Line across the diagram
            $Image->line ($X, $TopBorder, $X, $TopBorder + $YMax - 1, gdStyled);

            # Add the date
            my $DateStr = sprintf ("%04d-%02d-%02d", $year + 1900, $mon + 1, $mday);
            my $TextY   = $TopBorder + $YMax + 10 + length ($DateStr) * gdLargeFont->width;
            my $TextX   = $X - gdLargeFont->height / 2;
            $Image->stringUp (gdLargeFont, $TextX, $TextY, $DateStr, $Black);

            # Tick is bigger
            $Len = 5;
        }

        if ($WeeklyTicks || $wday == 0) {
            # Tick mark
            $Image->line ($X, $TopBorder + $YMax + $Len, $X, $TopBorder + $YMax, $Black);
        }
    }

    # Header
    my $Width = length ($Host) * gdLargeFont->width;
    my $X = $LeftBorder + ($XMax / 2) - ($Width / 2);
    $Image->string (gdLargeFont, $X, $TopBorder / 2, $Host, $Black);

    # Footer
    my $X = $LeftBorder + $XMax + $RightBorder - 3 - length ($Copyright) * gdSmallFont->width;
    my $Y = $TopBorder + $YMax + $BottomBorder - 3 - gdSmallFont->height;
    $Image->string (gdSmallFont, $X, $Y, $Copyright, $Black);

    # Setup the legend coordinates
    $LegFirstX = $LeftBorder / 2;
    $LegFirstY = $TopBorder + $YMax + 10 +
                 gdLargeFont->width * length ("2005-01-01") +
                 gdLargeFont->height;
    $LegLastY  = $TopBorder + $YMax + $BottomBorder;
    $LegX      = $LegFirstX;
    $LegY      = $LegFirstY;
    $LegW      = 0;
}



# ----------------------------------------------------------
#      	       	          Plot a line
# ----------------------------------------------------------



sub AddSystem {

    my $System = shift (@_);
    my @Color  = (shift (@_), shift (@_), shift (@_));

    # Allocate a color and set the line thickness
    my $Color  = $Image->colorAllocate (@Color);
    $Image->setThickness ($Thickness);

    # Draw
    my $LastOffs   = -1;
    my $LastRating = -1;
    for ($Offs = 0; $Offs <= $MaxDateOffs; $Offs++) {

        if (exists ($Ratings{$System}{"$Offs"})) {

            # Get the rating value
            $Rating = $Ratings{$System}{"$Offs"};

            # Limit the rating
            if ($Rating < 1000) {
	      	if ($LastOffs >= 0 && $LastRating < 1000) {
		    # We can draw a line
		    $Image->line (ScaleX ($Offs), ScaleY ($Rating),
			      	  ScaleX ($LastOffs), ScaleY ($LastRating),
				  $Color);
		}
	    }

            # Remember the coordinates for the next round
            $LastOffs   = $Offs;
            $LastRating = $Rating;
        }
    }

    # Legend
    if ($LegY >= $LegLastY) {
	# Next column
     	$LegX += $LegW + 40;
        $LegY = $LegFirstY;
     	$LegW = 0;
    }

    # Print the system name in the color
    $Image->string (gdLargeFont, $LegX, $LegY, $System, $Color);

    # Next X
    $LegY += gdLargeFont->height + 10;

    # Remember the largest width in this column
    $Width = length ($System) * gdLargeFont->width;
    if ($Width > $LegW) {
	$LegW = $Width;
    }
}



# ----------------------------------------------------------
#      	       	   Write the image to a file
# ----------------------------------------------------------



sub WriteImage {

    my $PNG = shift (@_);

    # Convert the image to png and copy it to a file
    open (PNG, ">$PNG") or die "Cannot open $PNG\n";
    print PNG $Image->png;
    close PNG;
}



# ----------------------------------------------------------
#                Command line helper functions
# ----------------------------------------------------------



sub SetImageName {

    if ($ImageName ne "") {
    	Abort ("Duplicate image name");
    }
    $ImageName = $_[0];
}



sub AddSystemName {

    local $Name = $_[1];
    local $C;

    if ($Name =~ /^([^:]+):(.*)$/) {
     	$Name = $1;
        $C = $2;
     	if ($C =~ /^(\d+),(\d+),(\d+)$/) {
            $Systems{$Name}{"R"} = $1;
            $Systems{$Name}{"G"} = $2;
            $Systems{$Name}{"B"} = $3;
     	} else {
     	    Abort ("Unknown color: $C");
     	}
    } else {
        $Systems{$Name}{"R"} = 0;
        $Systems{$Name}{"G"} = 0;
        $Systems{$Name}{"B"} = 0;
    }
}



sub Usage {

    print ("Usage: top1000.pl [options] image-file\n");
    print ("Valid options are:\n");
    print ("  --bottomborder n\tSet the bottom border (default: $BottomBorder)\n");
    print ("  --datadir dir\t\tSpecify top1000 data directory (default: $DataDir)\n");
    print ("  --days n\t\tShow n days (default: $Days)\n");
    print ("  --help\t\tPrint this text\n");
    print ("  --leftborder n\tSet the left border (default: $LeftBorder)\n");
    print ("  --rightborder n\tSet the right border (default: $RightBorder)\n");
    print ("  --system name[:r,g,b]\tAdd system graph with given color\n");
    print ("  --thickness n\t\tUse given line thickness for graphs (default: $Thickness)\n");
    print ("  --title string\tSpecify the image title\n");
    print ("  --topborder n\t\tSet the top border (default: $TopBorder)\n");
    print ("  --weekly\t\tDisplay weekly (instead of monthly) date marks\n");
    print ("\n");
    print ("top1000 data files must be named top1000-yyyy-mm-dd.txt\n");
    exit (0);

}



# ----------------------------------------------------------
#    	       	      	      	Code
# ----------------------------------------------------------

# Get program options
GetOptions ("bottomborder=i"    => \$BottomBorder,
            "datadir=s"         => \$DataDir,
            "days=i"   	       	=> \$Days,
            "help"              => \&Usage,
            "leftborder=i"      => \$LeftBorder,
            "rightborder=i"     => \$RightBorder,
	    "system=s"          => \&AddSystemName,
            "thickness=i"       => \$Thickness,
	    "title=s" 	      	=> \$ImageTitle,
            "topborder=i"       => \$TopBorder,
            "weekly!"           => \$WeeklyTicks,
            "<>"                => \&SetImageName);

# Check for mandatory options and option ranges
if ($ImageName eq "") {
    Abort ("Image name is missing");
}
if ($Days < 30 || $Days > 365) {
    Abort ("Invalid value for days");
}

# Get the current date and time
($sec,$min,$hour,$mday,$mon,$year,@rest) = localtime;
$Now = sprintf ("%d.%d.%d - %02d:%02d:%02d", $mday,
                $mon + 1, $year + 1900, $hour, $min, $sec);
$Today = timelocal (0, 0, 0, $mday, $mon, $year);

# Get the list of all files, then loop over all the files
my $Files = `ls $DataDir/*`;
my @FILES = split /\n/, $Files;
while ($FILE = shift @FILES) {

    # Check the name and extract the date
    if ($FILE !~ /$DataDir\/top1000-(\d{4})-(\d\d)-(\d\d)\.txt/) {
        next;
    }

    # Convert the date to a day offset
    $DateOffs = int (($Today - timelocal (0, 0, 0, $3, $2 - 1, $1 - 1900)) / (24 * 60 * 60));

    # Ignore anything older than $Days
    if ($DateOffs > $Days) {
	next;
    }

    # Remember the oldest entry
    if ($DateOffs > $MaxDateOffs) {
        $MaxDateOffs = $DateOffs;
    }

    # Open the input file
    open (FILE, $FILE) or Abort ("Cannot open $FILE: $!");

    # Loop over lines and store the contents into a hash
    while ($Line = <FILE>) {

        # Only accept matching lines and matching systems
        if ($Line =~ /^\s+(\d+)\s+\d+\.\d+\s+(\S+)\s*$/) {
            if (exists ($Systems{$2})) {
                $Ratings{$2}{"$DateOffs"} = $1;
            }
        }
    }

    # Close the input file
    close FILE;
}

# Generate the image
CreateImage ($ImageTitle);
foreach my $System (keys %Systems) {
    my $Color = $Systems{$System};
    AddSystem ($System, $Color->{"R"}, $Color->{"G"}, $Color->{"B"});
}
WriteImage ($ImageName);


# Done
exit 0;

