#!/usr/local/bin/perl

# $Id: gwfstats.pl,v 1.15 1997/11/21 19:43:49 elkner Exp $

use GD; # Graphics class library
use Socket;

require 'getopts.pl';
require 'ctime.pl';
$Version="gwfstats 1.1";

# gwfstats: Program to create graphical statistics from HTTPd and FTPd
# Log files. 
# Copyright 1997, Jens Elkner
#
# Derived from Benjamin Franz' GraphFTPWebLog 1.0 and FTPWebLog 1.0.2
# <URL:http://www.netimages.com/~snowhare/utilities/ftpweblog/>
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS 
# OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
# PARTICULAR PURPOSE.
#
# I offer it to the public domain and I ask, however, that this paragraph
# and my name be retained in any modified versions of the file you may
# make, and that you notify me of any improvements you make to the code.
#
# Use of this software in any way or in any form, source or binary,
# is not allowed in any country which prohibits disclaimers of any 
# implied warranties of merchantability or fitness for a particular
# purpose or any disclaimers of a similar nature.
#
# IN NO EVENT SHALL I BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 
# SPECIAL, INCIDENTAL,  OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 
# USE OF THIS SOFTWARE AND ITS DOCUMENTATION (INCLUDING, BUT NOT 
# LIMITED TO, LOST PROFITS) EVEN IF I HAVE BEEN ADVISED OF THE 
# POSSIBILITY OF SUCH DAMAGE

#-------------------------------------------------------#
# Adjust the following to match your system environment #
#-------------------------------------------------------#

# Location of the 'country-codes' file 
$CountriesFile="/local/www/etc/country-codes";

# Location and name of the decompression program
# (usually '/usr/bin/zcat', '/usr/local/bin/zcat',
# '/usr/bin/gzip -cd' or '/usr/local/bin/gzip -cd')
$Zcat="/usr/local/bin/gzip -cd";

# Pattern to match for Archive Section.
# It *must* have the form (...) to allow the search
# to know the name of the archive section it matched
# '--matched pattern--' is a special substitution that says use the
# matched (...) area. 
%ArchivePatterns=(
		  '^(/[^/]*/).*$',                '--matched pattern--',
		  '^.*(/)$',                        ' html files',
		  '^.*(\.[Hh][Tt][Mm][Ll]?)$',      ' html files',
		  '^.*(\.[Gg][Ii][Ff])$',           ' gif graphic files',
		  '^.*(\.[Jj][Pp][Gg])$',           ' jpg graphic files',
		  '^.*(\.[Pp][Nn][Gg])$',           ' png graphic files',
		  '^.*(\.[Xx][Bb][Mm])$',           ' xbm graphic files',
		  '^.*(\.class)$',                  ' Java class files',
		  '^(\/)[^/]*$',                    ' Home page',
);
#'

# Estimate the header size (in bytes) of typical responses not counted in logfile
# for NCSA/1.5.2
%RespEstimates = (
#		  Code   Size     Description
		  '200', 176,     # A normal response header
		  '302', 168,     # A redirect message
		  '304', 98,      # A Not Modified response to a Conditional GET
		  '400', 106,     # A Bad Request response message
		  '401', 150,     # An Authorization Required response message
		  '403', 78,      # A Forbidden response message
		  '404', 104,     # A Not Found message
		  '500', 100,     # A Server Error response message
		  '501', 100,     # A Not Implemented response message
		  );

# Edit the second value in the line, to generate graphic files with other names
# But watch out, they have to be uniqe	  
%GraphName = ('HourlyByHits','HourlyByHits.gif',
	       'HourlyByVolume','HourlyByVolume.gif',
	       'DailyByHits','DailyByHits.gif',
	       'DailyByVolume','DailyByVolume.gif',
	       'TopNTopLevelDomainsByHits','TopNTopLevelDomainsByHits.gif',
	       'TopNTopLevelDomainsByVolume','TopNTopLevelDomainsVolume.gif',
	       'TopNArchivesByHits','TopNArchivesByHits.gif',
	       'TopNArchivesByVolume','TopNArchivesByVolume.gif',
	       'TopNFilesByHits','TopNFilesByHits.gif',
	       'TopNFilesByVolume','TopNFilesByVolume.gif',
	       'TopNHostsByHits','TopNHostsByHits.gif',
	       'TopNHostsByVolume','TopNHostsByVolume.gif',
	       );
#---------------------------------------------------------------------------
# Now the main program starts. No more changes are  necessary !!!
#---------------------------------------------------------------------------
$newsection="<p><hr><p>\n<h2><a name=\"";
$return2index = "<p><a href=\"#index\">Return to Index</a>\n";
$li="<li> <a href=\"";   
$trailer = 
   "<hr><br>\nGenerated using ".
   "<a href=\"http://irb.cs.uni-magdeburg.de/~elkner/webtools/gwfstats.shtml\">$Version</a>".
   " by \n". 
   "<a href=\"http://irb.cs.uni-magdeburg.de/~elkner/\">Jens Elkner</a>\n".
   "</body></html>\n";
 
&Initialize;
if ($OldFile) {
    @OldFiles = split(",",$OldFile);
}
if ($ARGV[0]) {
    @NewFiles = @ARGV;
}
&debug("Starting open files");
&OpenFiles;
&debug("Read country file");
&ReadCountryFile if ($TopLevelDomains || $TopNTopLevelDomains);
if ($OldFiles[0]) {
    foreach (@OldFiles) {
	&debug("Start reading $_");
	&ReadOldFile($_);
    }
}
if ($NewFiles[0]) {
    foreach (@NewFiles) { 
        &debug("Start reading log $_");
	&ReadLog ($_); 
    }
}
&debug("Start processing archives");
&ProcessArchives if ($Archives || $TopNArchives);
&debug("Start writing stats");
&WriteWFLog if $WFLogFile;
&debug("Start writing graph stats");
&WriteGWFLog if $GWFLogFile;
&debug("Start renaming files");
&RenameFiles;
&debug("Creating stat files finished");

#############################################################################
sub debug {
    if ($tune) {
	$tmp = &ctime(time);
	chop $tmp;
	print "$tmp: @_\n";
    }
}

sub Initialize {
    %MonthToNumber=(
		    'Jan','01',
		    'Feb','02',
		    'Mar','03',
		    'Apr','04',
		    'May','05',
		    'Jun','06',
		    'Jul','07',
		    'Aug','08',
		    'Sep','09',
		    'Oct','10',
		    'Nov','11',
		    'Dec','12',
		    );
    
    %NumberToMonth=(
		    '01','January',
		    '02','February',
		    '03','March',
		    '04','April',
		    '05','May',
		    '06','June',
		    '07','July',
		    '08','August',
		    '09','September',
		    '10','October',
		    '11','November',
		    '12','December',
		    );
    
    %RespCodes = (
		  '100', 'Continue',
		  '101', 'Switching Protocols',
		  '200', 'OK',
		  '201', 'Created',
		  '202', 'Accepted',
		  '203', 'Non-Authoriative Information',
		  '204', 'No Content',
		  '205', 'Reset Content',
		  '206', 'Partial Content',
		  '220', 'Uses Protocol Extensions',
		  '300', 'Multiple Choices',
		  '301', 'Moved Permanently',
		  '302', 'Moved Temporarily',
		  '303', 'See Other',
		  '304', 'Not Modified',
		  '305', 'Use Proxy (proxy redirect)',
		  '400', 'Bad Request',
		  '401', 'Unauthorized',
		  '402', 'Payment Required',
		  '403', 'Forbidden',
		  '404', 'Not Found',
		  '405', 'Method Not Allowed',
		  '406', 'Not Acceptable',
		  '407', 'Proxy Authentication Required',
		  '408', 'Request Timeout',
		  '409', 'Conflict',
		  '410', 'Gone',
		  '411', 'Length Required',
		  '412', 'Precondition Failed',
		  '413', 'Request Entity To Large',
		  '414', 'Request-URI Too Long',
		  '415', 'Unsupported Media Type',
		  '420', 'Bad Protocol Extension Request',
		  '421', 'Protocol Extension Unknown',
		  '422', 'Protocol Extension Refused',
		  '423', 'Bad Protocol Extension Parameters',
		  '500', 'Internal Server Error',
		  '501', 'Not Implemented',
		  '502', 'Bad Gateway',
		  '503', 'Service Unavailable',
		  '504', 'Gateway Timeout',
		  '505', 'HTTP Version Not Supported',
		  '520', 'Protocol Extension Error',
		  '521', 'Protocol Extension Not Implemented',
		  '522', 'Protocol Extension Parameters Not Acceptable',
		  );

    $EndDate="0000 00 00 00 00 00";
    $StartDate="9999 99 99 99 99 99";

    $result = &Getopts('Aa:B:Cc:Dd:Ff:g:hHI:i:LN:O:o:p:Q:q:R:r:T:t:U:u:vX:x:YyZz');

    &Usage if $opt_h || $result == 0;
    &Version if $opt_v;

    # Print full archives report
    # ignored in GWFLog
    $Archives = $opt_A ? 0 : 1 ;
    # Number of archives to include in the Top N archives report	
    if ( ($opt_a == 0) || ($opt_a > 1) ) {
	$TopNArchives=$opt_a;
    }
    else {
	warn "Option -a $opt_a - not an numeric value!\nUsing defaults!\n";
	$TopNArchives=0;
    }
    # Blankout pattern, i.e. delete this pattern in all logged files
    $BlankOut=$opt_B ? $opt_B : "" ;
    # Print the full clients report
    # ignored in GWFLog
    $Hosts = $opt_C ? 0 : 1;
    # Number of clients to include in the Top N clients report
    if (($opt_c == 0) || ($opt_c > 1)) {
	$TopNHosts = $opt_c;
    }
    else {
	warn "Option -c $opt_c - not an numeric value!\nUsing defaults!\n";
	$TopNHosts = 0;
    }
    # Print full top level domains report
    # ignored in GWFLog
    $TopLevelDomains = $opt_D ? 0 : 1 ;
    # Number of top level domains to include in Top TLD report.
    if (($opt_d == 0) || ($opt_d > 1)) {
	$TopNTopLevelDomains = $opt_d;
    }
    else {
	warn "Option -d $opt_d - not an numeric value!\nUsing defaults!\n";
	$TopNTopLevelDomains=0;
    }
    # Print the full files report
    # ignored in GWFLog
    $Files = $opt_F ? 0 : 1 ;
    # Number of files to include in Top File list
    if (($opt_f == 0) || ($opt_f > 1)) {  
	$TopNFiles = $opt_f;
    }
    else {
	warn "Option -f $opt_f - not an numeric value!\nUsing defaults!\n";
	$TopNFiles = 0;
    }
    # file name where the graphic reports are to be written
    $GWFLogFile = $opt_g;
    # Verbose timing output
    $tune = $opt_H ? 1 : 0;
    # Directory Index files
    $DefaultPageName = $opt_I ? $opt_I : "index.html" ;
    # Include this from previously generated gwfstats file
    $OldFile=$opt_i if $opt_i;
    # Lookup hostnames from IP numbers if not already resolved 
    $LookupHosts = $opt_L ? 0 : 1;
    # Name of your system
    $SystemName = $opt_N ? $opt_N : "www.dom.ain";
    # Order of Rule execution
    $tmp = $opt_O ? $opt_O : "BuUxXrR";
    @Order = reverse split(/ */,$tmp);
    # WFLog file (no graphics)
    $WFLogFile = $opt_o if $opt_o;
    # location to store the generated gif files
    $DirBase=$opt_p ? $opt_p : "/htdocs/stats/current";
    # Max number of bytes without penalty for volume (0=no quota)
    $VolumeQuota = $opt_Q ? $opt_Q : "" ;
    # Dollars per meg per day for excess volume
    $ExcessVolumeRate = $opt_q ? $opt_q : 0 ;
    # exclude refs to files matching the perl regex
    $ExcludeRefsTo=$opt_R if $opt_R;
    # only include URLs that match the perl regex
    $IncludeOnlyRefsTo=$opt_r if $opt_r;
    # Exclude files matching regex from the top N files report
    $TopFileListFilter=$opt_T if $opt_T;
    # What kind of log being analyzed
    #    common   = WWW common log format
    #    ftp      = FTPd log
    #    combined = NCSA combined log format
    #    apache   = Apache multi-homed log
    $LogType = 'common';
    if ($opt_t) {
	if (($opt_t eq 'common') || ($opt_t eq 'apache') ||
	    ($opt_t eq 'combined') ||  ($opt_t eq 'ftp')) {
	    $LogType=$opt_t;
	}
	else {
	    warn "Unknow logfile type $opt_t!\nUsing common format!\n";
	}
    }
    # Replace logged file names with regex
    if ($opt_u) {
	@tmp = split("@@",$opt_u);
	foreach (@tmp) {
	    $RegexReplace = <<EOF;
	    \$FileName=~ s$_;
EOF
    	    push @FileRegexReplace,$RegexReplace;
        }
    }
    # Replace logged host names with regex
    if ($opt_U) {
	@tmp = split("@@",$opt_U);
	foreach (@tmp) {
	    $RegexReplace = <<EOF;
	    \$Host=~ s$_;
EOF
    	    push @HostRegexReplace,$RegexReplace;
	}
    }
    # exclude domains matching the perl regex
    $ExcludeDomain=$opt_X if $opt_X;
    # only include domains that match the perl regex  
    $IncludeOnlyDomain=$opt_x if $opt_x;
    # Graph the hourly stats
    $HourlyStats = $opt_Y ? 0 : 1 ;
    # Graph the daily stats
    $DailyStats = $opt_y ? 0 : 1 ;
    # Print summary report
    $Summary = $opt_Z ? 0 : 1;
    # Print summary about all status' send from WWW Server
    $StatusSummary = $opt_z ? 0 : 1;
    &debug("Initialization finished");
}

sub OpenFiles {
    # we do this because it saves you a lot of time when a specified
    # file or directory  does not exist or cannot be opened/created
    if ($WFLogFile) {
	open (WFLOG,">$DirBase/$WFLogFile.$$") || 
	    die ("Unable to open stat file $DirBase/$WFLogFile\n$!");
    }
    if ($GWFLogFile) {
	open (GWFLOG,">$DirBase/$GWFLogFile.$$") || 
	    die ("Unable to open graph file $DirBase/$GWFLogFile\n$!");
	foreach (values %GraphName) {
	    open (GRAPH,">$DirBase/$_.$$") || 
		die ("Unable to open graph file $DirBase/$_\n$!");
	    close (GRAPH);
	}
    }
    if ($OldFiles[0]) {
	foreach (@OldFiles) {
	    open (OLDLOG,"$_") ||
		die ("Unable to open old stat file $DirBase/$_\n$!");
	    close(OLDLOG);
	}
    }
    if ($NewFiles[0]) {
	foreach (@NewFiles) {
	    open (NEWLOG,"$_") ||
		die ("Unable to open log file $_\n$!");
	    close(NEWLOG);
	}
    }
}

sub RenameFiles {
    rename "$DirBase/$WFLogFile.$$", "$DirBase/$WFLogFile" if ($WFLogFile);
    if ($GWFLogFile) {
	rename "$DirBase/$GWFLogFile.$$", "$DirBase/$GWFLogFile";
	foreach (values %GraphName) {
	    rename "$DirBase/$_.$$", "$DirBase/$_";
	}
    }
}

sub ReadOldFile {
# ==========================================================================
# If an old output file is to be included, read it into the counters
# We assume that the old output file was created with the same options
# and that its content is disjunct from the current log.
# NOTE that using search options with inclusion cannot work unless the
# included file was also created with those search options.

    $InSegment=0;
    die "Unable to open include file: $_[0]\n" if (! open(OLD,$_[0]) );
    while (<OLD>) {
	chop;
	last if (m#</HTML>#oi);	# check for end of html file 
	if (/<a name=\"(summary|hourly|daily|status|files|hosts|topleveldomains)\">/oi) {	# Check for name element
	    $logsegment = $1;	# Set flag for correct log element
	    $InSegment=1;
	    if ($logsegment ne 'summary') {
		<OLD>;
		<OLD>;
		<OLD>; # skip the three header lines
	    }
	}
	next if (!$InSegment); # If not in a data section, keep scanning
	if ($logsegment eq "status" ) {
	    &ReadStatusSection;
	} elsif ($logsegment eq "files") {
	    &ReadFilesSection;
	} elsif ($logsegment eq "hosts") {
	    &ReadHostsSection;
	} elsif ($logsegment eq "summary") {
	    &ReadSummarySection;
	} elsif ($logsegment eq "daily") {
	    &ReadDailySection;
	} elsif ($logsegment eq "hourly") {
	    &ReadHourlySection;
	} elsif ($logsegment eq "topleveldomains") {
	    &ReadTopLevelDomainsSection;
	} 
    }
    close(OLD);
}

sub ReadLog {
    return if (($_[0] eq '') || ($_[0] eq '/dev/null'));
    $_[0]="$Zcat $_[0] |" if ($_[0]=~m/(\.gz|\.Z)/o);
    die ("Unable to open $_[0]\n$!") if (! open(LOGFILE,$_[0]) );
    if ($LogType eq 'ftp') {
	&ReadFTPLog;
    } elsif (($LogType eq 'common') || ($LogType eq 'apache')) {
	&ReadWWWLog;
    } elsif ($LogType eq 'combined') {
	&ReadCombinedWWWLog;
    }
    close(LOGFILE);
}

# Read a NCSA common format log
sub ReadWWWLog {
    while(<LOGFILE>) {
	chop;
	if ($LogType eq 'common') {
	    ($Host,$rfc931,$authuser,$TimeDate,$Request,$Status,$Bytes) =
		/^(\S+) (\S+) (\S+) \[([^\]\[]+)\] \"([^\"]*)\" (\S+) (\S+)/o;
	} elsif ($LogType eq 'apache') {
	    ($Host,$rfc931,$authuser,$TimeDate,$ServerDomain,$Request,$Status,$Bytes) =
		/^(\S+) (\S+) (\S+) \[([^\]\[]+)\] (\S+) \"([^\"]*)\" (\S+) (\S+)/o;
	}
	next if (!($Host && $rfc931 && $authuser && $TimeDate && $Request && $Status));
	($Method,$FileName,$Protocal)=split(/\s/,$Request,3);
	$Day="0$Day" if (length($Day) < 2);
	$FileName=~s#%7e#~#goi;	# Caniconicalize %7[Ee] as '~'
	$FileName=~s#//#/#go;   # Remove any extra slashes
	$FileName=~s#^ *$#/#o;	# fix root ref if needed
	$FileName=~s#\?.*$##o;	# Strip parms
	$FileName=~s/#.*$//o;	# Strip anchors
	$FileName=~s/${DefaultPageName}$//o; # remove DirectoryIndex names
        $Filename=~s/\x0*//g;       # remove not printable 0
        $FileName=~s#/(\./)+#/#g;   # remove /./ /../ etc.
	$FileName=~s/\/$//o;        # remove trailing /
	($FileName = "/") if ( $FileName eq "");   # "" means /
	$FileName=~s#$BlankOut##o if ($BlankOut);  # Blank out stuff
                                        # append / to */~user
	$FileName.="/" if ( $FileName =~ /^\/\~[^\/]+$/ );
	$Host = &LookupHostName($Host) if $LookupHosts;
        next if &InExReplace;
	($Day,$Month,$Year,$Hour,$Minute,$Second)=$TimeDate=~m#^(\d\d)/(\w\w\w)/(\d\d\d\d):(\d\d):(\d\d):(\d\d) #o;
	$FileName=~s#&#\&amp\;#go;
	$FileName=~s#<#\&lt\;#go;
        $FileName=~s#>#\&gt\;#go;
	$FileName=~s#"#\&quot;#go;
	$Host="Unresolved" if ($Host eq '');
	&RecordData;
    }
}

# Read a NCSA combined format log
sub ReadCombinedWWWLog {
    while(<LOGFILE>) {
	chop;
	($Host,$rfc931,$authuser,$TimeDate,$Request,$Status,$Bytes,$Referrer,$Agent) =
	    /^(\S+) (\S+) (\S+) \[([^\]\[]+)\] \"([^"]*)\" (\S+) (\S+) \"([^"]*)\" \"([^\"]*)\"/o;
	next if (!($Host && $rfc931 && $authuser && $TimeDate && $Request && $Status));
	($Method,$FileName,$Protocal)=split(/\s/,$Request,3);
	$Day="0$Day" if (length($Day) < 2);
	$FileName=~s#%7e#~#goi;	# Caniconicalize %7[Ee] as '~'
	$FileName=~s#//#/#go;   # Remove any extra slashes
	$FileName=~s#^ *$#/#o;	# fix root ref if needed
	$FileName=~s#\?.*$##o;	# Strip parms
	$FileName=~s/#.*$//o;	# Strip anchors
	$FileName=~s/${DefaultPageName}$//o; # remove DirectoryIndex names
        $Filename=~s/\x0*//g;       # remove not printable 0
        $FileName=~s#/(\./)+#/#g;   # remove /./ /../ etc.
	$FileName=~s/\/$//o;        # remove trailing /
	($FileName = "/") if ( $FileName eq "");   # "" means /
	$FileName=~s#$BlankOut##o if ($BlankOut);
	$Host = &LookupHostName($Host) if $LookupHosts;
        next if &InExReplace;
	($Day,$Month,$Year,$Hour,$Minute,$Second)=$TimeDate=~m#^(\d\d)/(\w\w\w)/(\d\d\d\d):(\d\d):(\d\d):(\d\d) #o;
	$FileName=~s#&#\&amp\;#go;
	$FileName=~s#<#\&lt\;#go;
        $FileName=~s#>#\&gt\;#go;
	$FileName=~s#"#\&quot;#go;
	$Host="Unresolved" if ($Host eq '');	
	&RecordData;
    }
}

# Read  FTPd format log file

sub ReadFTPLog {
    while(<LOGFILE>) {
	chop;
	($DayOfWeek,$Month,$Day,$TimeOfDay,$Year,$Unknown1,$Host,
	 $Bytes,$FileName,$BinAscii,$Unknown2,$Unknown3,$Unknown4,$User,
	 $Unknown5,$Unknown6)=split;
	$FileName=~s#%7e#~#goi;
	$FileName=~s#$BlankOut##o if $BlankOut;
	$Host = &LookupHostName($Host) if $LookupHosts;
	next if &InExReplace;
	($Hour,$Minute,$Second)=$TimeOfDay=~m#(\d\d):(\d\d):(\d\d)#o;
	$Day="0$Day" if (length($Day) < 2);
	$FileName=~s#&#\&amp\;#go;
	$FileName=~s#<#\&lt\;#go;
	$FileName=~s#>#\&gt\;#go;
	$FileName=~s#"#\&quot;#go;
	&RecordData;
    }
}

sub InExReplace {
    @tmp = @Order;
    while ($tmp[0]) {
        $x = pop(@tmp);
        if ( ($x eq 'u') && $opt_u ) {
	    foreach (@FileRegexReplace) { eval $_ ; }
        }
        elsif ( ($x eq 'U') && $opt_U ) {
	    foreach (@HostRegexReplace) { eval $_ ; }
        }
        elsif ( $x eq 'x') {
            return 1 if (($IncludeOnlyDomain) && !($Host=~m#$IncludeOnlyDomain#o));
	}
        elsif ( $x eq 'X') {
            return 1 if (($ExcludeDomain) && ($Host=~m#$ExcludeDomain#o));
	}
        elsif ( $x eq 'r') {
            return 1 if (($IncludeOnlyRefsTo) && !($FileName=~m#$IncludeOnlyRefsTo#o));
        }
        else {
            return 1 if (($x eq 'R') && ($ExcludeRefsTo) && ($FileName=~m#$ExcludeRefsTo#o));
        }
    }
    0;
}

sub ProcessArchives {
    foreach $file (keys (%FileHitsCounter)) {
	foreach $pattern (keys (%ArchivePatterns)) {
	    $match='';
	    ($match) = $file =~ m:$pattern:;
	    next if (! $match); # Skip stuff that does not match an archive section
	    $archive = $file;
	    if ($ArchivePatterns{$pattern} eq '--matched pattern--') {
		$substitute=$match;
	    } else {
		$substitute=$ArchivePatterns{$pattern};
	    }
	    $archive =~ s:$pattern:$substitute:;
	    $ArchiveHitsCounter{$archive} += $FileHitsCounter{$file};
	    $ArchiveBytesCounter{$archive} += $FileBytesCounter{$file};
	    if ( ($LastAccessArchive{$archive} lt $LastAccessFile{$file}) ) {
		$LastAccessArchive{$archive} = $LastAccessFile{$file};
	    }
	}
    }
}

sub WriteWFLog {
    &debug("Start writing Header");
    &PrintWFHeader;
    &debug("Start writing Summary");
    &PrintSummary($WFLogFile) if $Summary;
    &debug("Start writing Daily");
    &PrintDaily if $DailyStats;
    &debug("Start writing Hourly");
    &PrintHourly if $HourlyStats;
    &debug("Start writing Status");
    &PrintStatus if ( $StatusSummary && ($LogType eq 'www') );
    &debug("Start writing Top N TLD Hits");
    &PrintTopNTopLevelDomainsByHits if $TopNTopLevelDomains;
    &debug("Start writing Top N TLD Bytes");
    &PrintTopNTopLevelDomainsByVolume if $TopNTopLevelDomains;
    &debug("Start writing Top N Archives Hits");
    &PrintTopNArchivesByHits if $TopNArchives;
    &debug("Start writing Top N Archives Bytes");
    &PrintTopNArchivesByVolume if $TopNArchives;
    &debug("Start writing Top N Files Hits");
    &PrintTopNFilesByHits if $TopNFiles;
    &debug("Start writing Top N Files Bytes");
    &PrintTopNFilesByVolume if $TopNFiles;
    &debug("Start writing Top N Hosts Hits");
    &PrintTopNHostsByHits if $TopNHosts;
    &debug("Start writing Top N Hosts Bytes");
    &PrintTopNHostsByVolume if $TopNHosts;
    &debug("Start writing TLD");
    &PrintTopLevelDomains if $TopLevelDomains;
    &debug("Start writing Archives");
    &PrintArchives if $Archives;
    &debug("Start writing Files");
    &PrintFiles if $Files;
    &debug("Start writing Hosts");
    &PrintHosts if $Hosts;
    &debug("Start writing trailer");
    print WFLOG $trailer;
}

sub WriteGWFLog {
    &debug("Start writing graph Header");
    &PrintGWFHeader;
    &debug("Start writing graph Summary");
    &PrintSummary($GWFLogFile) if $Summary;
    &debug("Start writing graph Daily");
    &GraphDaily if $DailyStats;
    &debug("Start writing graph Hourly");
    &GraphHourly if $HourlyStats;
    &debug("Start writing graph Top N TLD");
    &GraphTopNTopLevelDomains if $TopNTopLevelDomains;
    &debug("Start writing graph Archives");
    &GraphTopNArchives if $TopNArchives;
    &debug("Start writing graph Files");
    &GraphTopNFiles if $TopNFiles;
    &debug("Start writing graph Hosts");
    &GraphTopNHosts if $TopNHosts;
    &debug("Start writing graph trailer");
    print GWFLOG $trailer;
}

sub PrintGWFHeader {
    print GWFLOG "<HTML><HEAD>\n<TITLE>$SystemName</TITLE></HEAD>\n",
    "<BODY><center><H1>Graphic $SystemName</H1></center>\n",
    $newsection,"index\">Index</a></h2>\n<ul>\n";
    print GWFLOG "$li#summary\">Summary</a>\n" if $Summary;
    if ($DailyStats) {
	print GWFLOG "$li#dailybyhits\">Daily Number of Hits</a>\n",
	"$li#dailybyvolume\">Daily Volume Transferred</a>\n";
    }
    if ($HourlyStats) { 
	print GWFLOG "$li#hourlybyhits\">",
	"Cumulative Number of Hits by Hour of Day</a>\n",
	"$li#hourlybyvolume\">",
	"Cumulative Volume Transferred by Hour of Day</a>\n";
    }
    if ($TopNTopLevelDomains) {
	print GWFLOG "$li#topntopleveldomainsbyhits\">Top ",
	$TopNTopLevelDomains, " Top Level Domains by Number of Hits</a>\n",
	"$li#topntopleveldomainsbyvolume\">Top ",
	$TopNTopLevelDomains, " Top Level Domains by Volume Transferred</a>\n";
    }
    if ($TopNArchives) { 
	print GWFLOG "$li#topnarchivesbyhits\">Top ",
	$TopNArchives, " Archives by Number of Hits</a>\n",
	"$li#topnarchivesbyvolume\">Top ",
	$TopNArchives, " Archives by Volume Transferred</a>\n";
    }
    if ($TopNFiles) {
	print GWFLOG "$li#topnfilesbyhits\">Top ",
	$TopNFiles, " Files by Number of Hits</a>\n",
	"$li#topnfilesbyvolume\">Top ",
	$TopNFiles, " Files by Volume Transferred</a>\n";
    }
    if ($TopNHosts) {
	print GWFLOG "$li#topnhostsbyhits\">Top ",
	$TopNHosts, " Hosts by Number of Hits</a>\n",
	"$li#topnhostsbyvolume\">Top ",
	$TopNHosts, " Hosts by Volume Transferred</a>\n";
    }
    print GWFLOG "</ul>\n";
}

sub PrintWFHeader {
    $li="<li> <a href=\"";
    print WFLOG "<HTML><HEAD><TITLE>\n$SystemName</TITLE></HEAD>\n",
    "<BODY><center><H1>$SystemName</H1></center>\n",
    $newsection,"index\">Index</a></h2>\n<ul>\n";
    print WFLOG "$li#summary\">Summary</a>\n" if $Summary;
    print WFLOG "$li$GWFLogFile\">Graphical Log Report</a>\n" if $GWFLogFile;
    print WFLOG "$li#daily\">Daily Number of Hits</a>\n" if $DailyStats;
    print WFLOG "$li#hourly\">Hourly Statistic</a>\n" if $HourlyStats;
    print WFLOG "$li#status\">Status Statistic</a>\n" if $StatusSummary;
    if ($TopNTopLevelDomains) {
	print WFLOG "$li#topntopleveldomainsbyhits\">Top ",
	$TopNTopLevelDomains, " Top Level Domains by Number of Hits</a>\n",
	"$li#topntopleveldomainsbyvolume\">Top ",
	$TopNTopLevelDomains, " Top Level Domains by Volume Transferred</a>\n";
    }
    if ($TopNArchives){
	print WFLOG "$li#topnarchivesbyhits\">Top ",
	$TopNArchives, " Archives by Number of Hits</a>\n",
	"$li#topnarchivesbyvolume\">Top ",
	$TopNArchives, " Archives by Volume Transferred</a>\n";
    }
    if ($TopNFiles) {
	print WFLOG "$li#topnfilesbyhits\">Top ",
	$TopNFiles, " Files by Number of Hits</a>\n",
	"$li#topnfilesbyvolume\">Top ",
	$TopNFiles, " Files by Volume Transferred</a>\n";
    }
    if ($TopNHosts) {
	print WFLOG "$li#topnsitesbyhits\">Top ",
	$TopNHosts, " Hosts by Number of Hits</a>\n",
	"$li#topnsitesbyvolume\">Top ",
	$TopNHosts, " Hosts by Volume Transferred</a>\n";
    }
    print WFLOG "$li#topleveldomains\">Complete Top Level Domain Statistic</a>\n" if $TopLevelDomains;
    print WFLOG "$li#archives\">Complete Archives Statistic</a>\n" if $Archives;
    print WFLOG "$li#files\">Complete File Statistic</a>\n" if $Files;
    print WFLOG "$li#hosts\">Complete Host Statistic</a>\n" if $Hosts;
    print WFLOG "</ul>\n";
}

sub commas { # Insert commas into an integer
    local($_)=@_;
    1 while s/(.*\d)(\d\d\d)/$1,$2/;
    $_;
}

sub PrintSummary {
    $lines  = $newsection;
    $lines .= "summary\">Summary</a></h2>\n";
    $lines .= "<strong>Period Covered:</strong> ";
    ($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$StartDate);
    $lines .= "$Hour:$Minute:$Second $Day $NumberToMonth{$Month} $Year to ";
    ($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$EndDate);
    $lines .= "$Hour:$Minute:$Second $Day $NumberToMonth{$Month} $Year<br>\n";
    $lines .= "<strong>Total Files Transfered:</strong> ",
    $lines .= &commas($TotalHitsCounter);
    $lines .= "<br>\n<strong>Total Bytes Transfered:</strong> ";
    $lines .= &commas($TotalBytesCounter);
    $lines .= "<br>\n";
    if ($VolumeQuota) {
	$ExcessVolumeCharge=0;
	foreach $key (sort keys(%DayHitsCounter)) {
	    ($Year,$Month,$Day)=split(/ /,$key);
	    $ExcessBytesPerDay = $DayBytesCounter{$key} - $VolumeQuota;
	    if($ExcessBytesPerDay > 0) {
		$ExcessVolumeCharge+=(($ExcessBytesPerDay/1048576)
				      * $ExcessVolumeRate);
	    }
	}
	if ($ExcessVolumeCharge) {
	    $lines .= "<strong>Total Excess Volume Charge during this ";
	    $lines .= "reporting period:</strong> \$";
	    $lines .= sprintf "%1.2f<br>\n", $ExcessVolumeCharge;
	}
    }
    
    $lines .= "<strong>Unique Hosts:</strong> $UniqueHosts\n";
    $lines .= $return2index;
    ( $_[0] eq $WFLogFile ) ? print WFLOG $lines : print GWFLOG $lines;
}

sub PrintDaily {
    print WFLOG $newsection,"daily\">Daily Number of Hits</a></h2>\n<pre>\n";
    printf WFLOG "%8s%14s%19s\n\n","Accesses","Bytes","Date";
    foreach $key (sort keys(%DayHitsCounter)) {
	($Year,$Month,$Day)=split(/ /,$key);
	printf WFLOG "%8s%14s%19s",
	$DayHitsCounter{$key},
	$DayBytesCounter{$key},
	"$Day $NumberToMonth{$Month} $Year";
	if ($VolumeQuota && ($DayBytesCounter{$key} > $VolumeQuota)) {
	    $OverVolume=($DayBytesCounter{$key} - $VolumeQuota)/1048576;
	    $Penalty=$OverVolume * $ExcessVolumeRate;
	    printf WFLOG " (quota exceeded by %1.2f megabytes; \$%1.2f charge)",
	    $OverVolume,$Penalty;
	}
	print WFLOG "\n";
    }
    print WFLOG "</pre>\n",$return2index;
}

sub GraphDaily {
    print GWFLOG <<"End";

${newsection}dailybyhits\">
    Daily Number of Hits</a></h2>
<p align=center>
<img src=\"$GraphName{DailyByHits}\" 
     alt=[DailyHitsGraph]</p>
$return2index

${newsection}dailybyvolume\">
    Daily Volume Transferred</a></h2>
<p align=center>
<img src=\"$GraphName{DailyByVolume}\" 
     alt=[DailyVolumeGraph]></p>
$return2index

End
    &ChartMonth;
}

sub PrintHourly {
    print WFLOG $newsection,"hourly\">Hourly Statistic</a></h2>\n<pre>\n";
    printf WFLOG "%8s%14s%5s\n\n","Accesses","Bytes","Hour";
    foreach $key (sort keys(%HourHitsCounter)) {
	printf WFLOG "%8s%14s%5s\n",
	$HourHitsCounter{$key},
	$HourBytesCounter{$key},
	$key;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub GraphHourly {
    print GWFLOG << "End";

${newsection}hourlybyhits\">
    Cumulative Number of Hits by Hour of Day</a></h2>
<p align=center>
<img src=\"$GraphName{HourlyByHits}\" 
     alt=\"[Hourly Hits Graph]\"></p>
$return2index
${newsection}hourlybyvolume\">
    Cumulative Volume Transferred by Hour of Day</a></h2> 
<p align=center>
<img src=\"$GraphName{HourlyByVolume}\"  
     alt=\"[Hourly Volume Graph]\"></p>
$return2index

End
    &ChartHours;
}

sub PrintStatus {
    print WFLOG $newsection,"status\">Status Statistic</a></h2>\n<pre>\n";
    printf WFLOG "%-5s%-25s%8s%16s\n\n","Code","Description","Accesses","Bytes";
    foreach $key (sort keys(%StatusHitsCounter)) {
	printf WFLOG "%-5s%-25s%8s%16s\n",
	$key,$RespCodes{$key},
	$StatusHitsCounter{$key},
	$StatusBytesCounter{$key},
    }
    print WFLOG "</pre>\n", $return2index;
}

sub PrintTopNTopLevelDomainsByHits {
    print WFLOG $newsection,"topntopleveldomainsbyhits\">Top ",
    $TopNTopLevelDomains, " Top Level Domains by Number of Hits</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%8s%12s   %-10s   %-s\n\n","Accesses","Bytes"," ","Top Level Domain";
    $Counter=1;
    foreach $key (sort ByTopLevelDomainHits keys(%TopLevelDomainHitsCounter)) {
	last if ($Counter > $TopNTopLevelDomains);
	printf WFLOG "%8s%12s   %-10s - %-s\n",
	$TopLevelDomainHitsCounter{$key},
	$TopLevelDomainBytesCounter{$key},
	$key,
	$CountryCode{$key};
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub PrintTopNTopLevelDomainsByVolume {
    print WFLOG $newsection,"topntopleveldomainsbyvolume\">Top ",
    $TopNTopLevelDomains," Top Level Domains by Volume Transferred</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%8s%12s   %-10s   %-s\n\n","Accesses","Bytes"," ","Top Level Domain";
    $Counter=1;
    foreach $key (sort ByTopLevelDomainBytes keys(%TopLevelDomainHitsCounter)) {
	last if ($Counter > $TopNTopLevelDomains);
	printf WFLOG "%8s%12s   %-10s - %-s\n",
	$TopLevelDomainHitsCounter{$key},
	$TopLevelDomainBytesCounter{$key},
	$key,
	$CountryCode{$key};
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub GraphTopNTopLevelDomains {
    print GWFLOG << "End";

${newsection}topntopleveldomainsbyhits\">
    Top $TopNTopLevelDomains Top Level Domains by Number of Hits</a></h2>
<p align=center>
<img src=\"$GraphName{TopNTopLevelDomainsByHits}\"
     alt=\"[Top Level Domain Hits Graph]\"></p>
$return2index

${newsection}topntopleveldomainsbyvolume\">
    Top $TopNTopLevelDomains Top Level Domains by Volume Transferred</a></h2>
<p align=center>
<img src=\"$GraphName{TopNTopLevelDomainsByVolume}\"
     alt=\"[Top Level Domain by Volume Transferred]\"></p>
$return2index

End
    &ChartTopLevelDomains;
}

sub PrintTopNArchivesByHits {
    print WFLOG $newsection,"topnarchivesbyhits\">Top ",
    $TopNArchives," Archives by Number of Hits</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %-s\n\n","Last Accessed","Accesses","Bytes","Archive";
    $Counter=1;
    foreach $key (sort ByArchiveHits keys(%ArchiveHitsCounter)) {
	last if ($Counter > $TopNArchives);
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessArchive{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %-s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$ArchiveHitsCounter{$key},
	$ArchiveBytesCounter{$key},
	$key;
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub PrintTopNArchivesByVolume {
    print WFLOG $newsection,"topnarchivesbyvolume\">Top ",
    $TopNArchives, " Archives by Volume Transferred</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %-s\n\n","Last Accessed","Accesses","Bytes","Archive";
    $Counter=1;
    foreach $key (sort ByArchiveBytes keys(%ArchiveHitsCounter)) {
	last if ($Counter > $TopNArchives);
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessArchive{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %-s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$ArchiveHitsCounter{$key},
	$ArchiveBytesCounter{$key},
	$key;
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}


sub GraphTopNArchives {
    print GWFLOG << "End";

${newsection}topnarchivesbyhits\">
    Top $TopNArchives Archives by Number of Hits</a></h2>
<p align=center>
<img src=\"$GraphName{TopNArchivesByHits}\"
     alt=\"[Archives Hits Graph]\"></p>
$return2index

${newsection}topnarchivesbyvolume\">
    Top $TopNArchives Archives by Volume Transferred</a></h2>
<p align=center>
<img src=\"$GraphName{TopNArchivesByVolume}\"
     alt=\"[Archives by Volume Transferred]\">\n</p>
$return2index

End
    &ChartTopNArchives;
}

sub PrintTopNFilesByHits {
    print WFLOG $newsection,"topnfilesbyhits\">Top ",
    $TopNFiles, " Files by Number of Hits</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %-s\n\n","Last Accessed","Accesses","Bytes","File";
    $Counter=1;
    foreach $key (sort ByFileHits keys(%FileHitsCounter)) {
	last if ($Counter > $TopNFiles);
	next if ($TopFileListFilter && ($key=~m#$TopFileListFilter#oi));
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessFile{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %-s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$FileHitsCounter{$key},
	$FileBytesCounter{$key},
	$key;
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub PrintTopNFilesByVolume {
    print WFLOG $newsection,"topnfilesbyvolume\">Top ",
    $TopNFiles, " Files by Volume Transferred</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %-s\n\n","Last Accessed","Accesses","Bytes","File";
    $Counter=1;
    foreach $key (sort ByFileBytes keys(%FileHitsCounter)) {
	last if ($Counter > $TopNFiles);
	next if ($TopFileListFilter && ($key=~m#$TopFileListFilter#oi));
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessFile{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %-s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$FileHitsCounter{$key},
	$FileBytesCounter{$key},
	$key;
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub GraphTopNFiles {
    print GWFLOG << "End";

${newsection}topnfilesbyhits\">
    Top $TopNFiles Files by Number of Hits</a></h2>
<p align=center>
<img src=\"$GraphName{TopNFilesByHits}\" 
     alt=\"[Top $TopNFiles Hits Graph]\"></p>
$return2index

${newsection}topnfilesbyvolume\">
    Top $TopNFiles Files by Volume Transferred</a></h2>
<p align=center>
<img src=\"$GraphName{TopNFilesByVolume}\" 
     alt=\"[Top $TopNFiles File Volume Graph]\"></p>
$return2index

End
    &ChartTopNFiles;
}

sub PrintTopNHostsByHits {
    print WFLOG $newsection,"topnsitesbyhits\">Top ",
    $TopNHosts, " Hosts by Number of Hits</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %33s\n\n","Last Access","Accesses","Bytes","Host";
    $Counter=1;
    foreach $key (sort ByHostHits keys(%HostBytesCounter)) {
	last if ($Counter > $TopNHosts);
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessHost{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %33s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$HostHitsCounter{$key},
	$HostBytesCounter{$key},
	$key;
	$Counter++;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub PrintTopNHostsByVolume {
    print WFLOG $newsection,"topnsitesbyvolume\">Top ",
    $TopNHosts, " Hosts by Volume Transferred</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %33s\n\n","Last Access","Accesses","Bytes","Host";
    $Counter=1;
    foreach $key (sort ByHostBytes keys(%HostHitsCounter)) {
	last if ($Counter > $TopNHosts);
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessHost{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %33s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$HostHitsCounter{$key},
	$HostBytesCounter{$key},
	$key;
	$Counter++;
    }
    print WFLOG "</pre>\n" ,$return2index;
}

sub GraphTopNHosts {
    print GWFLOG << "End";

${newsection}topnhostsbyhits\">
    Top $TopNHosts Hosts by Number of Hits</a></h2>
<p align=center>
<img src=\"$GraphName{TopNHostsByHits}\" 
     alt=\"[Top $TopNHosts Hits Graph]\"></p>
$return2index

${newsection}topnhostsbyvolume\">
    Top $TopNHosts Hosts by Volume Transferred</a></h2>
<p align=center>
<img src=\"$GraphName{TopNHostsByVolume}\" 
     alt=\"[Top $TopNHosts Hosts Volume Graph]\"></p>
$return2index

End
    &ChartTopNHosts;
}

sub PrintTopLevelDomains {
    print WFLOG $newsection,"topleveldomains\">Complete Top Level Domain Statistic</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%8s%12s   %-10s  %-s\n\n","Accesses","Bytes"," ","Top Level Domain";
    foreach $TopDomain (sort ByTopLevelDomainHits keys(%TopLevelDomainHitsCounter)) {
	printf WFLOG "%8s%12s   %-10s - %-s\n",
	$TopLevelDomainHitsCounter{$TopDomain},
	$TopLevelDomainBytesCounter{$TopDomain},
	$TopDomain,
	$CountryCode{$TopDomain};
	$Counter++;
    }
    print WFLOG "</pre>\n", $return2index;
}


sub PrintArchives {
    print WFLOG $newsection,"archives\">Complete Archive Statistic</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %-s\n\n","Last Accessed","Accesses","Bytes","Archive";
    foreach $key (sort keys(%ArchiveHitsCounter)) {
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessArchive{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %-s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$ArchiveHitsCounter{$key},
	$ArchiveBytesCounter{$key},
	$key;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub PrintFiles {
    print WFLOG $newsection,"files\">Complete File Statistic</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %-s\n\n","Last Accessed","Accesses","Bytes","File";
    foreach $key (sort keys(%FileHitsCounter)) {
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessFile{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;
	printf WFLOG "%-21s%8s%12s   %-s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$FileHitsCounter{$key},
	$FileBytesCounter{$key},
	$key;
    }
    print WFLOG "</pre>\n",$return2index;
}
   
sub PrintHosts {
    print WFLOG $newsection,"hosts\">Complete Hosts Statistic</a></h2>\n",
    "<pre>\n";
    printf WFLOG "%-21s%8s%12s   %33s\n\n","Last Access","Accesses","Bytes","Host";
    foreach $key (sort ByReversedSubDomains keys(%HostHitsCounter)) {
	($Year,$Month,$Day,$Hour,$Minute,$Second)=split(/ /,$LastAccessHost{$key});
	$Month=$NumberToMonth{$Month};
	$Month=~s#^(\w\w\w).*$#$1#o;	
	printf WFLOG "%-21s%8s%12s   %33s\n",
	"$Hour:$Minute:$Second $Day $Month $Year",
	$HostHitsCounter{$key},
	$HostBytesCounter{$key},
	$key;
    }
    print WFLOG "</pre>\n",$return2index;
}

sub ByTopLevelDomainHits {
    $TopLevelDomainHitsCounter{$b}<=>$TopLevelDomainHitsCounter{$a};
}
sub ByTopLevelDomainBytes {
    $TopLevelDomainBytesCounter{$b}<=>$TopLevelDomainBytesCounter{$a};
}

sub ByArchiveHits {
    $ArchiveHitsCounter{$b}<=>$ArchiveHitsCounter{$a};
}

sub ByArchiveBytes {
    $ArchiveBytesCounter{$b}<=>$ArchiveBytesCounter{$a};
}

sub ByFileHits {
    $FileHitsCounter{$b}<=>$FileHitsCounter{$a};
}
sub ByFileBytes {
    $FileBytesCounter{$b}<=>$FileBytesCounter{$a};
}

sub ByHostHits {
    $HostHitsCounter{$b}<=>$HostHitsCounter{$a};
}
sub ByHostBytes {
    $HostBytesCounter{$b}<=>$HostBytesCounter{$a};
}

sub ByReversedSubDomains {
    local(@adomains,@bdomains,$aisIP,$bisIP,$counter,$acounter,$bcounter,$result);
    $result=0;
    (@adomains)=split(/\./,$a);
    (@bdomains)=split(/\./,$b);
    $aisIP=$a=~m#^\d+\.\d+\.\d+\.\d+$#o;
    $bisIP=$b=~m#^\d+\.\d+\.\d+\.\d+$#o;
    
    # Put raw IPs before domains
    if ($aisIP && (!$bisIP)) {
	$result=-1;
    } elsif ((!$aisIP) && $bisIP) {
	$result=1;
    } elsif ($aisIP && $bisIP) {
	$counter=0;
	while ((!$result) && ($counter < 4)) {
	    $result= $adomains[$counter] <=> $bdomains[$counter];
	    $counter++;
	}
    } elsif ((!$aisIP) && (!$bisIP)) {
	$acounter=$#adomains;
	$bcounter=$#bdomains;
	while ((!$result) && ($acounter>=0) && ($bcounter>=0)) {
	    $result=$adomains[$acounter] cmp $bdomains[$bcounter];
	    $acounter--;
	    $bcounter--;
	}
	if (!$result) {
	    $result=1 if ($acounter>=0);
	    $result=-1 if ($bcounter>=0);
	}
    }
    $result;
}

sub Version {
    die <<"EndVersion";
This is $Version (Jens Elkner <elkner\@irb.cs.uni-magdeburg.de>).

It is derived from GraphFTPWebLog 1.0 and FTPWebLog 1.0.2 .
EndVersion
}

sub Usage {
	die <<"EndUsage";
Usage: gwfstats [-h] [-v] [-A] [-a N] [-B regex] [-C] [-c N] [-D] [-d N]
                [-F] [-f N] [-g Statfile] [-i StatFileList] [-I regex] 
		[-L] [-N name] [-O order] [-o StatFile] [-p path] [-Q] [-q R] 
                [-R regex] [-r regex] [-T regex] [-t type] [-U substExprList] 
		[-u substExprList] [-X regex] [-x regex] [-Y] [-y] [-Z] [-z]
		[LogFileList]

Options: 
  -h  Help    --  Display this message and quit.
  -v  Version --  Display version and quit.
 
  -A                Do not print Archive Sections report
  -a N              Print Top N Archives
  -B regex          Blank out partial URLs matching the regex
  -C                Do not print full hosts listing
  -c N              Print Top N Hosts
  -D                Do not print full top level domain report
  -d N              Print Top N Top Level Domains
  -F                Do not print full file listing
  -f N              Print Top N Files
  -g StatFile       Name of the file where graphic reports are stored
  -I regex          Directory Index pages (see NCSA option: DirectoryIndex)
  -i StatFileList   Include Reports from old Files (assumed to be a prior
                    $Version output).
  -L                Do not perform hostname lookups of unresolved addresses
  -N name           System name for report
  -O order          Order of include/exclude/replace functions to perform
                    (default = "uUxXrR" )
  -o StatFile       Name of the file where non-graphic reports are stored
  -p path           System Directory where the generated files are to be stored.
                    (default = /local/www/htdocs/stats/WWW/current)
  -Q N              Volume Quota in bytes
  -q R              Quota Rate in \$/meg/day over volume quota
  -R regex          Do not include refs to files matching the perl regex
  -r regex          Only include refs to files matching the perl regex 
  -T regex          Filter top N file list to exclude regex
  -t logtype        Select whether log files are to be processed as FTP 
                    xferlogs, Common Log format, NCSA Combined format, or 
                    Apache multihomed server log format.
  -U substExprList  A @@ separated List of Perl substitute expressions
                    for doing Hostname Substitutions/Replacements
  -u substExprList  A @@ separated List of Perl substitute expressions
                    for doing FileName Substitutions/Replacements
  -X regex          Do not include any domain/host name matching the perl regex
  -x regex          Only include any domain/host name matching the perl regex 
  -Y                Do not print Hourly stats
  -y                Do not print Daily stats
  -Z                Do not print summary report
  -z                Do not print Status summary report

N              ...  Number of type Integer >= 0  (default 0 = do not)  
R              ...  Number of type Real
StatFile       ...  regular file name where generated reports are stored
                    (path is automatic. prefixed if necessary)
LogFileList    ...  a whitespace separated list of httpd access log files
                    (compressed files like file.Z or file.gz are allowed)
StatFileList   ...  a comma separated list of $Version generated files
                    containing reports from previous runs 
logtype        ...  What kind of log you want to analyze:
                       common   = WWW common log format
		       ftp      = FTPd log
                       combined = NCSA combined log format
                       apache   = Apache multi-homed log
                    (default = common)
regex            ...  Perl regular expression
Perl subst expr  ...  everything after the s in s/PATTERN/REPLACEMENT/SWITCHES 
                      (e.g. #^/((disk1)|(disk2))/#/#oi )
order            ...  the same as include/exclude/replace switches (i.e.,
                      [[r][R][u][U][x][X]]
EndUsage
}

sub RecordData {
    if ($LogType ne 'ftp') {
	return if ( ($Status > 599) || ($Status < 100) );
	if ($Status != 200 && $Status != 304) {
	    $FileName= "Code $Status: $RespCodes{$Status}";
	}
	$Bytes+=$RespEstimates{$Status};
	$StatusBytesCounter{$Status} += $Bytes; 
	$StatusHitsCounter{$Status}++;
    }
    $HourHitsCounter{$Hour}++;
    $HourBytesCounter{$Hour}+=$Bytes;
    $Today="$Year $MonthToNumber{$Month} $Day";
    $DayHitsCounter{$Today}++;
    $DayBytesCounter{$Today}+=$Bytes;
    $Today="$Today $Hour $Minute $Second";
    $StartDate=$Today if ($Today lt $StartDate);
    $EndDate=$Today if ($Today gt $EndDate);
    $HostHitsCounter{$Host}++;
    $HostBytesCounter{$Host}+=$Bytes;
    $UniqueHosts++ if ($HostHitsCounter{$Host}==1);
    if ($LastAccessHost{$Host} lt $Today) {
	$LastAccessHost{$Host}=$Today;
    }
    ($TopDomain)=($Host=~m#\.(\w+)$#o);
    $TopDomain=~tr/A-Z/a-z/;
    $TopDomain="Unresolved" if ($TopDomain > 0 || $TopDomain eq '');
    $TopLevelDomainHitsCounter{$TopDomain}++;
    $TopLevelDomainBytesCounter{$TopDomain}+=$Bytes;
    $FileHitsCounter{$FileName}++;
    $FileBytesCounter{$FileName}+=$Bytes;
    if ($LastAccessFile{$FileName} lt $Today) {
	$LastAccessFile{$FileName}=$Today;
    }
    $TotalBytesCounter+=$Bytes;
    $TotalHitsCounter++;
}

sub ReadSummarySection {
    while (<OLD>) {
	last if ( /<a\ href=\"#index\">Return\ to\ Index<\/a>/oi );
	$line=$_;
	if ($line =~ m#Period Covered:#o) {
	    ($starthour,$startminute,$startsec,$startday,
	     $startmonth,$startyear,$endhour,$endminute,
	     $endsec,$endday,$endmonth,$endyear) =
	    $line =~ m#</strong> (\d\d):(\d\d):(\d\d) (\d\d) (\w\w\w)\w* (\d\d\d\d) to (\d\d):(\d\d):(\d\d) (\d\d) (\w\w\w)\w* (\d\d\d\d)<br>#o;
	    $StartDateTmp="$startyear $MonthToNumber{$startmonth} $startday $starthour $startminute $startsec";
	    $StartDate = $StartDateTmp if ($StartDateTmp lt $StartDate);
	    $EndDateTmp="$endyear $MonthToNumber{$endmonth} $endday $endhour $endminute $endsec";
	    $EndDate = $EndDateTmp if ($EndDateTmp gt $EndDate);
	}
    }
    $InSegment=0;
}

sub ReadHourlySection {
    while (<OLD>) {
	last if ( /<\/pre>/oi );
	($Accesses,$Bytes,$Hour)=m#^\s*(\d+)\s+(\d+)\s+(\d\d)#o;
	$HourHitsCounter{$Hour} += $Accesses;
	$HourBytesCounter{$Hour} += $Bytes;
    }
    $InSegment=0;
}

sub ReadDailySection {
    while (<OLD>) {
	last if ( /<\/pre>/oi );
	($Accesses,$Bytes,$Day,$Month,$Year)=
	    m#^\s*(\d+)\s+(\d+)\s+(\d\d)\s+(\w\w\w)\w*\s+(\d\d\d\d)#o; # Parse out
	$Today="$Year $MonthToNumber{$Month} $Day";
	$DayHitsCounter{$Today} += $Accesses;
	$DayBytesCounter{$Today} += $Bytes;
    }
    $InSegment=0;
}

sub ReadStatusSection {
    while (<OLD>) {
	chop;
	last if ( /<\/pre>/oi );
	($Status,$Description,$Accesses,$Bytes)= 
	    m#^\s*(\d{3})\s+(.*)\s+(\d+)\s+(\d+)#o;
	$StatusBytesCounter{$Status} += $Bytes;
	$StatusHitsCounter{$Status} += $Accesses;
    }
    $InSegment=0;
}

sub ReadFilesSection {
    while (<OLD>) {
	last if ( /<\/pre>/oi );
	($Hour,$Minute,$Second,$Day,$Month,$Year,$Accesses,
	 $Bytes,$FileName)=
	     m#^\s*(\d\d):(\d\d):(\d\d) (\d\d) (\w\w\w)\w* (\d\d\d\d) \s*(\d+)\s+(\d+)\s+(\S.*)#o;
	next if (($IncludeOnlyRefsTo) && !($FileName=~m#$IncludeOnlyRefsTo#o));
	next if (($ExcludeRefsTo) && ($FileName=~m#$ExcludeRefsTo#o));
	$FileName=~s#$BlankOut##o if ($BlankOut);

	$TotalHitsCounter += $Accesses;
	$TotalBytesCounter   += $Bytes;
	$FileHitsCounter{$FileName} += $Accesses;
	$FileBytesCounter{$FileName} += $Bytes;
	$la = "$Year $MonthToNumber{$Month} $Day $Hour $Minute $Second";
        if ( $la gt $LastAccessFile{$FileName} ) {
	    $LastAccessFile{$FileName} = $la;
	}
    }
    $InSegment=0;
}     

sub ReadHostsSection {
    while (<OLD>) {
	last if ( /<\/pre>/oi );
	($Hour,$Minute,$Second,$Day,$Month,$Year,$Accesses,$Bytes,$Host)=
	    m#^\s*(\d\d):(\d\d):(\d\d) (\d\d) (\w\w\w)\w* (\d\d\d\d) \s*(\d+)\s+(\d+)\s+(\S.*)#o;
	
	$Host = &LookupHostName($Host) if $LookupHosts;
	next if (($IncludeOnlyDomain) && !($Host=~m#$IncludeOnlyDomain#o));
        next if (($ExcludeDomain) && ($Host=~m#$ExcludeDomain#o));
	$UniqueHosts++ if  (! $HostHitsCounter{$Host});
	$HostHitsCounter{$Host} += $Accesses;
	$HostBytesCounter{$Host} += $Bytes;
	$la = "$Year $MonthToNumber{$Month} $Day $Hour $Minute $Second";
	if ( $la gt $LastAccessHost{$Host} ) {
	    $LastAccessHost{$Host} = $la;
	}
    }
    $InSegment=0;
}

sub ReadTopLevelDomainsSection {
    while (<OLD>) {
	last if ( /<\/pre>/oi );
	($Accesses,$Bytes,$TopLevelDomain)=m#^\s*(\d+)\s+(\d+)\s+(\w+)\s+-\s+#o;
	$TopLevelDomainHitsCounter{$TopLevelDomain} += $Accesses;
	$TopLevelDomainBytesCounter{$TopLevelDomain} += $Bytes;
    }
    $InSegment=0;
}

sub ReadCountryFile {
    if(open(COUNTRIES,$CountriesFile)) {
	while(<COUNTRIES>) {
	    ($Code,$Description)=m#^(\w+)\s+(.*)$#;
	    $Code=~tr/A-Z/a-z/;
	    $CountryCode{$Code}=$Description;
	}
	close(COUNTRIES);
	$CountryCode{"Unresolved"}=""; # No text
    }
    else {
	warn ("Couldn't open $CountriesFile\n$!");
    }
}

sub LookupHostName {
    local ($IP)=$_[0];
    # Check to see if this is a number we have to lookup
    return $IP if (! ($IP =~ m#^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$#o));
    # Check to see if we already know this one.
    if (defined($DomainLookupList{$IP})) {
	return $DomainLookupList{$IP};
    }
    $ip4 = inet_aton $IP;
    $name = (gethostbyaddr($ip, AF_INET))[0];
    $name = $IP if (! $name);
    $DomainLookupList{$IP}=$name;
    $name;
}

sub WrongPattern {
    $option=$_[0];
    die <<"EndWrongPattern";
Invalid argument for switch -$option.
e.g. use -$option \"/admin\@\@/admin/\"\n\n
EndWrongPattern
}

sub ChartHours {
    local($DateString)=@_;
    undef %BarNumber;
    undef %XLabels;
    undef %YValues;
    $ChartWidth=525;
    $ChartHeight=260;
    $HourlyGraphFile="$DirBase/$GraphName{HourlyByHits}.$$";
    $HeaderText="Cumulative Number of Hits by Hour of Day";
    if (! (open (HOURLYGRAPH,">$HourlyGraphFile"))) { 
	warn ("Couldn't open $HourlyGraphFile\n$!");
	return ;
    }
    foreach $hour (keys(%HourHitsCounter)) {
	$N=$hour;
	$BarNumber{$N}=$N;
	$XLabels{$N}=$hour;
	$YValues{$N}=$HourHitsCounter{$hour};
    }
    $hourlyChart=&VerticalBarChart($ChartWidth,$ChartHeight,
				   $HeaderText,'Hour','Number of Hits',24);
    print HOURLYGRAPH $hourlyChart->gif;
    close (HOURLYGRAPH);
    undef %BarNumber;
    undef %XLabels;
    undef %YValues;
    $HourlyGraphFile="$DirBase/$GraphName{HourlyByVolume}.$$";
    $HeaderText="Cumulative Volume Transferred by Hour of Day";
    if (! (open (HOURLYGRAPH,">$HourlyGraphFile"))) { 
	warn ("Couldn't open $HourlyGraphFile\n$!");
	return;
    }
    foreach $hour (keys(%HourBytesCounter)) {
	$N=$hour;
	$BarNumber{$N}=$N;
	$XLabels{$N}=$hour;
	$YValues{$N}=int($HourBytesCounter{$hour}/1024);
    }
    $hourlyChart=&VerticalBarChart($ChartWidth,$ChartHeight,
				   $HeaderText,'Hour','Kilobytes',24);
    print HOURLYGRAPH $hourlyChart->gif;
    close (HOURLYGRAPH);
}


sub ChartMonth {
    undef %BarNumber;
    undef %XLabels;
    undef %YValues;
    $ChartWidth=539;
    $ChartHeight=260;
    $ThisMonth="0000 00 00";
    foreach $key (keys(%DayHitsCounter)) {
	$ThisMonth=$key if ($key gt $ThisMonth);
    }
    $FileEnd = $ThisMonth;
    $FileEnd =~ s# \d\d$##o;
    $FileEnd =~ s# ##o;
    $DailyGraphFile="$DirBase/$GraphName{DailyByHits}.$$";
    ($Year,$Month)=split(/ /,$ThisMonth);
    $MonthName=$NumberToMonth{$Month};
    $HeaderText="Daily Number of Hits for $MonthName $Year";
    if (! (open (DAILYGRAPH,">$DailyGraphFile"))) { 
	warn ("Couldn't open $DailyGraphFile\n$!");
	return;
    }
    foreach $date (sort keys(%DayHitsCounter)) {
	($ThisYear,$ThisMonth,$ThisDay)=split(/ /,$date);
	if (($ThisMonth eq $Month) && ($ThisYear eq $Year)) {
	    $N=$ThisDay;
	    $BarNumber{$N}=$N -1;
	    $XLabels{$N}=$ThisDay;
	    $YValues{$N}=$DayHitsCounter{$date};
	}
    }
    $dailyChart=&VerticalBarChart($ChartWidth,$ChartHeight,
				  $HeaderText,'Day','Number of Hits',31);
    print DAILYGRAPH $dailyChart->gif;
    close (DAILYGRAPH);
    $DailyGraphFile="$DirBase/$GraphName{DailyByVolume}.$$";
    $HeaderText="Daily Volume Transferred for $MonthName $Year";
    undef %BarNumber;
    undef %XLabels;
    undef %YValues;
    if (! (open (DAILYGRAPH,">$DailyGraphFile"))) { 
	warn ("Couldn't open $DailyGraphFile\n$!");
	return;
    }
    foreach $date (sort keys(%DayHitsCounter)) {
	($ThisYear,$ThisMonth,$ThisDay)=split(/ /,$date);
	if (($ThisMonth eq $Month) && ($ThisYear eq $Year)) {
	    $N=$ThisDay;
	    $BarNumber{$N}=$N -1;
	    $XLabels{$N}=$ThisDay;
	    $YValues{$N}=int($DayBytesCounter{$date}/1024);
	}
    }
    $dailyChart=&VerticalBarChart($ChartWidth,$ChartHeight,
				  $HeaderText,'Day','Kilobytes',31);
    print DAILYGRAPH $dailyChart->gif;
    close (DAILYGRAPH);
}

sub ChartTopLevelDomains {
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $ChartWidth=600;
    $TopLevelDomainsGraphFile="$DirBase/$GraphName{TopNTopLevelDomainsByHits}.$$";
    $HeaderText="Top $TopNTopLevelDomains Top Level Domains by Number of Hits";
    $N=0;
    if (! (open (TOPDOMAINGRAPH,">$TopLevelDomainsGraphFile"))) { 
	warn ("Couldn't open $TopLevelDomainsGraphFile\n$!");
	return;
    }
    foreach $TopDomain (sort ByTopLevelDomainHits keys(%TopLevelDomainHitsCounter)) {
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}=sprintf"%-5s %-5s",
	$TopDomain,$CountryCode{$TopDomain};
	$BarValues{$N}=$TopLevelDomainHitsCounter{$TopDomain};
	$N++;
	last if ($N == $TopNTopLevelDomains);
    }
    $topDomainChart=&HorizontalBarChart($ChartWidth,10,
					$HeaderText,'Number of Hits',$N);
    print TOPDOMAINGRAPH $topDomainChart->gif;
    close (TOPDOMAINGRAPH);
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $TopLevelDomainsGraphFile="$DirBase/$GraphName{TopNTopLevelDomainsByVolume}.$$";
    $HeaderText="Top $TopNTopLevelDomains Top Level Domains by Volume Transferred";
    $N=0;
    if (! (open (TOPDOMAINGRAPH,">$TopLevelDomainsGraphFile"))) {
	warn ("Couldn't open $TopLevelDomainsGraphFile\n$!");
	return;
    }
    foreach $TopDomain (sort ByTopLevelDomainBytes keys(%TopLevelDomainBytesCounter)) {
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}=sprintf"%-5s %-5s",
	$TopDomain,$CountryCode{$TopDomain};
	$BarValues{$N}=int($TopLevelDomainBytesCounter{$TopDomain}/1024);
	$N++;
	last if ($N == $TopNTopLevelDomains);
    }
    $topDomainChart=&HorizontalBarChart($ChartWidth,10,
					$HeaderText,'Kilobytes',$N);
    print TOPDOMAINGRAPH $topDomainChart->gif;
    close (TOPDOMAINGRAPH);
}

sub ChartTopNArchives {
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $ChartWidth=600;
    $TopNArchivesGraphFile="$DirBase/$GraphName{TopNArchivesByHits}.$$";
    $HeaderText="Top $TopNArchives Archives by Number of Hits";
    $N=0;
    if (! (open (TOPNARCHIVESGRAPH,">$TopNArchivesGraphFile"))) {
	warn ("Couldn't open $TopNArchivesGraphFile\n$!");
	return;
    }
    foreach $archive (sort ByArchiveHits keys(%ArchiveHitsCounter)) {
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}=$archive;
	if (length($archive) > 34) {
	    ($firstelement)=$archive=~m#^(/[^/]+/)#o;
	    $firstelement=$archive if (!$firstelement);
	    $charcount=34 - length($firstelement);
	    ($tailelement)=$archive=~m#(/[^/]+/?)$#o;
	    $tailelement=$archive if (!$tailelement);
	    $newarchive="$firstelement....$tailelement";
	    $newarchive=$archive if (length($newarchive) >= length($archive));
	    $BarLabels{$N}="$newarchive";
	}
	$BarValues{$N}=$ArchiveHitsCounter{$archive};
	$N++;
	last if ($N == $TopNArchives);
    }
    $topNArchivesChart=&HorizontalBarChart($ChartWidth,10,$HeaderText,'Number of Hits',$N);
    print TOPNARCHIVESGRAPH $topNArchivesChart->gif;
    close (TOPNARCHIVESGRAPH);
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $TopNArchivesGraphFile="$DirBase/$GraphName{TopNArchivesByVolume}.$$";
    $HeaderText="Top $TopNArchives Archives by Volume Transferred";
    $N=0;
    if (! (open (TOPNARCHIVESGRAPH,">$TopNArchivesGraphFile"))) {
	warn ("Couldn't open $TopNArchivesGraphFile\n$!");
	return;
    }
    foreach $archive (sort ByArchiveBytes keys(%ArchiveHitsCounter)) {
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}="$archive";
	if (length($archive) > 34) {
	    ($firstelement)=$archive=~m#^(/[^/]+/)#o;
	    $firstelement=$archive if (!$firstelement);
	    $charcount=34 - length($firstelement);
	    ($tailelement)=$archive=~m#(/[^/]+/?)$#o;
	    $tailelement=$archive if (!$tailelement);
	    $newarchive="$firstelement....$tailelement";
	    $newarchive=$archive if (length($newarchive) >= length($archive));
	    $BarLabels{$N}="$newarchive";
	}
	$BarValues{$N}=$ArchiveBytesCounter{$archive}/1024;
	$N++;
	last if ($N == $TopNArchives);
    }
    $topNArchivesChart=&HorizontalBarChart($ChartWidth,10, $HeaderText,'Kilobytes',$N);
    print TOPNARCHIVESGRAPH $topNArchivesChart->gif;
    close (TOPNARCHIVESGRAPH);
}

sub ChartTopNFiles {
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $ChartWidth=600;
    $TopNFilesGraphFile="$DirBase/$GraphName{TopNFilesByHits}.$$";
    $HeaderText="Top $TopNFiles Files by Number of Hits";
    $N=0;
    if (! (open (TOPNFILESGRAPH,">$TopNFilesGraphFile"))) {
	warn ("Couldn't open $TopNFilesGraphFile\n$!");
	return;
    }
    foreach $file (sort ByFileHits keys(%FileHitsCounter)) {
	next if (($TopFileListFilter) && ($file =~ m#$TopFileListFilter#o));
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}=$file;
	if (length($file) > 34) {
	    ($firstelement)=$file=~m#^(/[^/]+/)#o;
	    $firstelement=$file if (!$firstelement);
	    $charcount=34 - length($firstelement);
	    ($tailelement)=$file=~m#(/[^/]+/?)$#o;
	    $tailelement=$file if (!$tailelement);
	    $newfile="$firstelement....$tailelement";
	    $newfile=$file if (length($newfile) >= length($file));
	    $BarLabels{$N}="$newfile";
	}
	$BarValues{$N}=$FileHitsCounter{$file};
	$N++;
	last if ($N == $TopNFiles);
    }
    $topNFilesChart=&HorizontalBarChart($ChartWidth,10,
					$HeaderText,'Number of Hits',$N);
    print TOPNFILESGRAPH $topNFilesChart->gif;
    close (TOPNFILESGRAPH);
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $TopNFilesGraphFile="$DirBase/$GraphName{TopNFilesByVolume}.$$";
    $HeaderText="Top $TopNFiles Files by Volume Transferred";
    $N=0;
    if (! (open (TOPNFILESGRAPH,">$TopNFilesGraphFile"))) {
	warn ("Couldn't open $TopNFilesGraphFile\n$!");
	return;
    }
    foreach $file (sort ByFileBytes keys(%FileBytesCounter)) {
	next if (($TopFileListFilter) && ($file =~ m#$TopFileListFilter#o));
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}="$file";
	if (length($file) > 34) {
	    ($firstelement)=$file=~m#^(/[^/]+/)#o;
	    $firstelement=$file if (!$firstelement);
	    $charcount=34 - length($firstelement);
	    ($tailelement)=$file=~m#(/[^/]+/?)$#o;
	    $tailelement=$file if (!$tailelement);
	    $newfile="$firstelement....$tailelement";
	    $newfile=$file if (length($newfile) >= length($file));
	    $BarLabels{$N}="$newfile";
	}
	$BarValues{$N}=$FileBytesCounter{$file}/1024;
	$N++;
	last if ($N == $TopNFiles);
    }
    $topNFilesChart=&HorizontalBarChart($ChartWidth,10,
					$HeaderText,'Kilobytes',$N);
    print TOPNFILESGRAPH $topNFilesChart->gif;
    close (TOPNFILESGRAPH);
}

sub ChartTopNHosts {
    $ChartWidth=600;
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $TopNHostsGraphFile="$DirBase/$GraphName{TopNHostsByHits}.$$";
    $HeaderText="Top $TopNHosts Hosts by Number of Hits";
    $N=0;
    if (! (open (TOPNHOSTSGRAPH,">$TopNHostsGraphFile"))) {
	warn ("Couldn't open $TopNHostsGraphFile\n$!");
	return;
    }
    foreach $Host (sort ByHostHits keys(%HostHitsCounter)) {
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}=$Host;
	$BarValues{$N}=$HostHitsCounter{$Host};
	$N++;
	last if ($N >= $TopNHosts);
    }
    $topNHostsChart=&HorizontalBarChart($ChartWidth,10,
					$HeaderText,'Number of Hits',$N);
    print TOPNHOSTSGRAPH $topNHostsChart->gif;
    close (TOPNHOSTSGRAPH);
    undef %BarNumber;
    undef %BarLabels;
    undef %BarValues;
    $TopNHostsGraphFile="$DirBase/$GraphName{TopNHostsByVolume}.$$";
    $HeaderText="Top $TopNHosts Hosts by Volume Transferred";
    $N=0;
    if (! (open (TOPNHOSTSGRAPH,">$TopNHostsGraphFile"))) {
	warn ("Couldn't open $TopNHostsGraphFile\n$!");
	return;
    }
    foreach $Host (sort ByHostBytes keys(%HostBytesCounter)) {
	$N=~s#^(\d\d)$#0$1#o;
	$N=~s#^(\d)$#00$1#o;
	$BarNumber{$N}=$N;
	$BarLabels{$N}=$Host;
	$BarValues{$N}=$HostBytesCounter{$Host}/1024;
	$N++;
	last if ($N >= $TopNHosts);
    }
    $topNHostsChart=&HorizontalBarChart($ChartWidth,10,
					$HeaderText,'Kilobytes',$N);
    print TOPNHOSTSGRAPH $topNHostsChart->gif;
    close (TOPNHOSTSGRAPH);
}

sub VerticalBarChart {

# Calling parms:
#
# $ImageWidth,$ImageHeight,$HeaderText,$XLegend,$YLegend,$NumberofBars
# $NumberofBars controls how wide each bar should be, it does not
# necessarily have to be equal to the actual number of bars; it can be
# larger. Smaller is not recommended.
#
# Pass x-values in globals $XLabels{N} and Y values $in YValues{N}
# routine returns the gif chart. Specify Bar Number for each bar in
# $BarNumber{N} (0-X)

    local ($ImageWidth,$ImageHeight,$HeaderText,$XLegend,$YLegend,$NumberofBars)=@_;

    local ($myImage,$ImageBorder,$ImageTopEdge,$ImageLeftEdge,
	   $ImageBottomEdge,$ImageRightEdge,$ImageBorderColor,
	   $ImageBorderColor,$ImageBackgroundColor,$BannerTextColor,
	   $ValueTextColor,$GraphTopEdge,$GraphLeftEdge,$GraphBottomEdge,
	   $GraphRightEdge,$GraphHorizontalCenter,$LegendTextColor,
	   $GraphVerticalCenter,$GraphBorder,$GraphBorderColor,
	   $GraphBackgroundColor,$GraphColor,$GraphHeight,
	   $BigTextWidth,$BigTextHeight,
	   $HeaderWidth,$SmallTextWidth,$SmallTextHeight,
	   $NumberofTicks,$YPerTick,$TickUnit);

    $ImageBorder=2;
    
    ($BigTextWidth,$BigTextHeight) = (gdLargeFont->width,gdLargeFont->height);
    ($SmallTextWidth,$SmallTextHeight) = (gdSmallFont->width,gdSmallFont->height);

    $NumberofTicks = 4;
    
    $ImageTopEdge=$ImageBorder;
    $ImageLeftEdge=$ImageBorder;
    $ImageBottomEdge=$ImageHeight-$ImageBorder-1;
    $ImageRightEdge=$ImageWidth-$ImageBorder-1;

    $GraphTopEdge=$ImageTopEdge+30;
    $GraphLeftEdge=$ImageLeftEdge+70;
    $GraphBottomEdge=$ImageBottomEdge-30;
    $GraphRightEdge=$ImageRightEdge-10;
    $GraphHorizontalCenter=int(($GraphLeftEdge+$GraphRightEdge)/2);
    $GraphVerticalCenter=int(($GraphTopEdge+$GraphBottomEdge)/2);
    $GraphBorder=2;

    $GraphHeight=$GraphBottomEdge - $GraphTopEdge + 1;

    $myImage = new GD::Image($ImageWidth,$ImageHeight) || 
	die("Couldn't make new image\n");
    $ImageBorderColor = $myImage->colorAllocate(0,0,0);
    $ImageBackgroundColor  = $myImage->colorAllocate(255,255,255);
    $BannerTextColor = $myImage->colorAllocate(0,0,230);
    $LegendTextColor = $myImage->colorAllocate(0,0,230);
    $ValueTextColor = $myImage->colorAllocate(0,0,230);
    $GraphBorderColor=$myImage->colorAllocate(0,0,0);
    $GraphBackgroundColor=$myImage->colorAllocate(100,100,100);
    $GraphColor=$myImage->colorAllocate(0,0,255);
    
    $myImage->filledRectangle(0,0,$ImageWidth-1,$ImageHeight-1,$ImageBorderColor);
    $myImage->filledRectangle($ImageLeftEdge,$ImageTopEdge,$ImageRightEdge,$ImageBottomEdge,$ImageBackgroundColor);
    
    $myImage->filledRectangle($GraphLeftEdge-$GraphBorder,
			      $GraphTopEdge-$GraphBorder,
			      $GraphRightEdge+$GraphBorder,
			      $GraphBottomEdge+$GraphBorder,
			      $GraphBorderColor);
    $myImage->filledRectangle($GraphLeftEdge,
			      $GraphTopEdge,
			      $GraphRightEdge,
			      $GraphBottomEdge,
			      $GraphBackgroundColor);

    $HeaderWidth=length($HeaderText)*$BigTextWidth;
    
    $myImage->string(gdLargeFont,
		     $GraphHorizontalCenter-int($HeaderWidth/2),
		     $GraphTopEdge-21,
		     $HeaderText,
		     $BannerTextColor);
    $myImage->string(gdLargeFont,
		     $GraphHorizontalCenter-int($HeaderWidth/2)-1,
		     $GraphTopEdge-21,
		     $HeaderText,
		     $BannerTextColor);
    
    $MinCount = 0;
    $MaxCount = 0;

    foreach $element (keys(%YValues)) {
	$MaxCount = $YValues{$element} if ($YValues{$element} > $MaxCount);
    }
    
    $YPerTick = ($GraphHeight / $NumberofTicks);
    $TickUnit=($MaxCount - $MinCount) / $NumberofTicks;
    
    for ($TickCount=1 ; $TickCount < $NumberofTicks ; $TickCount++) {
	$Tick=int($MinCount + $TickUnit * $TickCount);
	$TickY=int($GraphBottomEdge - $TickCount * $YPerTick);
	$myImage->string(gdSmallFont,$GraphLeftEdge-$SmallTextWidth*(length($Tick)+1),int($TickY-$SmallTextHeight/2),$Tick,$ValueTextColor);
	$myImage->line($GraphLeftEdge,$TickY,$GraphLeftEdge-3,$TickY,$GraphBorderColor);
    }

# Top tick label for Y axis
    $myImage->string(gdSmallFont,
		     $GraphLeftEdge-$SmallTextWidth*(length(int($MaxCount))+1),
		     $GraphTopEdge-int($SmallTextHeight/2),
		     int($MaxCount),
		     $ValueTextColor);
# Bottom tick label for Y axis
    $myImage->string(gdSmallFont,
		     $GraphLeftEdge-$SmallTextWidth*(length(int($MinCount))+1),
		     $GraphBottomEdge-8,
		     int($MinCount),
		     $ValueTextColor);
# Legend for Y axis
    $myImage->stringUp(gdSmallFont,
		       $ImageLeftEdge+2,
		       $ImageBottomEdge-int(($ImageHeight/2)-(length($YLegend)*$SmallTextWidth/2)),
		       $YLegend,
		       $TextColor);
# Legend for X axis
    $myImage->string(gdSmallFont,
		     $GraphHorizontalCenter-int(length($XLegend)*$SmallTextWidth/2),
		     $ImageBottomEdge-$SmallTextHeight,
		     $XLegend,
		     $TextColor);
# Make the actual graph

    foreach $Bar (sort keys(%BarNumber)) {
	&PlotVerticalBar($Bar);
	$N++;
    }
    $myImage;
}

sub PlotVerticalBar {
    local ($Bar)=$_[0];
    local ($x1,$y1,$x2,$y2,$XBarWidth,$XBarStride);
    
    $Number=$BarNumber{$Bar};
    $Y=$YValues{$Bar};
    $Label=$XLabels{$Bar};

    $XBarStride=int(($GraphRightEdge-$GraphLeftEdge-1)/$NumberofBars);
    $XBarWidth=$XBarStride-3;
    $MaxCount++ if $MaxCount == $MinCount;
    $YScale = ($GraphTopEdge - $GraphBottomEdge) / ($MaxCount - $MinCount); 
    $x1 = $Number * $XBarStride + $GraphLeftEdge + 5;
    $y1 = int(($Y-$MinCount)*$YScale+$GraphBottomEdge);
    $x2 = $x1 + $XBarWidth;
    $y2 = $GraphBottomEdge;
    
    $myImage->filledRectangle($x1,$y1,$x2,$y2,$GraphColor);
    
    $myImage->string(gdSmallFont,$x1,$y2+10,$Label,$ValueTextColor);
}

sub HorizontalBarChart {

# Calling parms:
#
# $ImageWidth,$BarHeight,$HeaderText,$XLegend,$NumberofBars
# $NumberofBars controls how wide each bar should be, it does not
# necessarily have to be equal to the actual number of bars; it can be
# larger. Smaller is not recommended.
# $ImageHeight is determined dynamically from $BarHeight & $NumberofBars
# Pass x-values in globals $XLabels{N} and Y values $in YValues{N}
# routine returns the gif chart. Specify Bar Number for each bar in
# $BarNumber{N} (0-X)

    local ($ImageWidth,$BarHeight,$HeaderText,$XLegend,$NumberofBars)=@_;

    local ($myImage,$ImageBorder,$ImageTopEdge,$ImageLeftEdge,
	   $ImageBottomEdge,$ImageRightEdge,$ImageBorderColor,
	   $ImageBorderColor,$ImageBackgroundColor,$BannerTextColor,
	   $ValueTextColor,$GraphTopEdge,$GraphLeftEdge,$GraphBottomEdge,
	   $GraphRightEdge,$GraphHorizontalCenter,$LegendTextColor,
	   $GraphVerticalCenter,$GraphBorder,$GraphBorderColor,
	   $GraphBackgroundColor,$GraphColor,$GraphHeight,
	   $BigTextWidth,$BigTextHeight,
	   $HeaderWidth,$SmallTextWidth,$SmallTextHeight,
	   $NumberofTicks,$YPerTick,$TickUnit);

    $ImageBorder=2;
    $YBarStride=$BarHeight+2;
    
    ($BigTextWidth,$BigTextHeight) = (gdLargeFont->width,gdLargeFont->height);
    ($SmallTextWidth,$SmallTextHeight) = (gdSmallFont->width,gdSmallFont->height);

# Determine how much room to make for text to the left of chart rows

    $MaxTextWidth=0;
    foreach $bar (keys(%BarNumber)) {
	$BarTextWidth=$SmallTextWidth*length($BarLabels{$bar});
	if ($BarTextWidth > $MaxTextWidth) {
	    $MaxTextWidth=$BarTextWidth;
	}
    }

    $NumberofTicks   = 4;
    $ImageHeight     = 70 + ($YBarStride * $NumberofBars);
    $ImageTopEdge    = $ImageBorder;
    $ImageLeftEdge   = $ImageBorder;
    $ImageBottomEdge = $ImageHeight-$ImageBorder-1;
    $ImageRightEdge  = $ImageWidth-$ImageBorder-1;

    $GraphTopEdge=$ImageTopEdge+50;
    $GraphLeftEdge=$ImageLeftEdge+$MaxTextWidth+10;
    $GraphBottomEdge=$ImageBottomEdge-5;
    $GraphRightEdge=$ImageRightEdge-10;
    $GraphHorizontalCenter=int(($GraphLeftEdge+$GraphRightEdge)/2);
    $GraphVerticalCenter=int(($GraphTopEdge+$GraphBottomEdge)/2);
    $GraphBorder=2;
    
    $GraphHeight=$GraphBottomEdge - $GraphTopEdge + 1;
    $GraphWidth=$GraphRightEdge - $GraphLeftEdge + 1;
    
    $myImage = new GD::Image($ImageWidth,$ImageHeight) || 
	die("Couldn't make new image\n");
    $ImageBorderColor = $myImage->colorAllocate(0,0,0);
    $ImageBackgroundColor  = $myImage->colorAllocate(255,255,255);
    $BannerTextColor = $myImage->colorAllocate(0,0,230);
    $LegendTextColor = $myImage->colorAllocate(0,0,230);
    $ValueTextColor = $myImage->colorAllocate(0,0,230);
    $GraphBorderColor=$myImage->colorAllocate(0,0,0);
    $GraphBackgroundColor=$myImage->colorAllocate(100,100,100);
    $GraphColor=$myImage->colorAllocate(0,0,255);
    
    $myImage->filledRectangle(0,0,$ImageWidth-1,$ImageHeight-1,$ImageBorderColor);
    $myImage->filledRectangle($ImageLeftEdge,$ImageTopEdge,$ImageRightEdge,$ImageBottomEdge,$ImageBackgroundColor);
    
    $myImage->filledRectangle($GraphLeftEdge-$GraphBorder,
			      $GraphTopEdge-$GraphBorder,
			      $GraphRightEdge+$GraphBorder,
			      $GraphBottomEdge+$GraphBorder,
			      $GraphBorderColor);
    $myImage->filledRectangle($GraphLeftEdge,
			      $GraphTopEdge,
			      $GraphRightEdge,
			      $GraphBottomEdge,
			      $GraphBackgroundColor);
    
    $HeaderWidth=length($HeaderText)*$BigTextWidth;
# Banner
    $myImage->string(gdLargeFont,
		     $GraphHorizontalCenter-int($HeaderWidth/2),
		     $ImageTopEdge+2,
		     $HeaderText,
		     $BannerTextColor);
    $myImage->string(gdLargeFont,
		     $GraphHorizontalCenter-int($HeaderWidth/2)-1,
		     $ImageTopEdge+2,
		     $HeaderText,
		     $BannerTextColor);
    
    $MinCount = 0;
    $MaxCount = 0;
    
    foreach $element (keys(%BarValues)) {
	$MaxCount = $BarValues{$element} if ($BarValues{$element} > $MaxCount);
    }

    $XPerTick = ($GraphWidth / $NumberofTicks);
    $TickUnit=($MaxCount - $MinCount) / $NumberofTicks;
    
    for ($TickCount=1 ; $TickCount < $NumberofTicks ; $TickCount++) {
	$Tick=int($MinCount + $TickUnit * $TickCount);
	$TickX=int($GraphLeftEdge + $TickCount * $XPerTick);
	$myImage->string(gdSmallFont,
			 $TickX-int(length($Tick)/2)*$SmallTextWidth,
			 $GraphTopEdge-$SmallTextHeight-3,
			 $Tick,
			 $ValueTextColor);
	$myImage->line($TickX,
		       $GraphTopEdge-4,
		       $TickX,
		       $GraphTopEdge+2,
		       $GraphBorderColor);
    }

# Left tick label for X axis
    $myImage->string(gdSmallFont,
		     $GraphLeftEdge,
		     $GraphTopEdge-$SmallTextHeight-3,
		     int($MinCount),
		     $ValueTextColor);

# Right tick label for X axis
    $myImage->string(gdSmallFont,
		     $GraphRightEdge-int($SmallTextWidth*length(int($MaxCount))),
		     $GraphTopEdge-$SmallTextHeight-3,
		     int($MaxCount),
		     $ValueTextColor);

# Legend for Y axis
#	$myImage->stringUp(gdSmallFont,
#		$ImageLeftEdge+2,
#		$ImageBottomEdge-int(($ImageHeight/2)-(length($YLegend)*$SmallTextWidth/2)),
#		$YLegend,
#		$TextColor);

# Legend for X axis
    $myImage->string(gdSmallFont,
		     $GraphHorizontalCenter-int(length($XLegend)*$SmallTextWidth/2),
		     $GraphTopEdge - $SmallTextHeight*2 - 2,
		     $XLegend,
		     $TextColor);

# Make the actual graph

    foreach $Bar (sort keys(%BarNumber)) {
	&PlotHorizontalBar($Bar);
    }
    $myImage;
}

sub PlotHorizontalBar {
    local ($Bar)=$_[0];
    local ($x1,$y1,$x2,$y2,$XBarWidth,$XBarStride);
    
    $Number=$BarNumber{$Bar};
    $X=$BarValues{$Bar};
    $Label=$BarLabels{$Bar};
    $MaxCount++ if $MaxCount == $MinCount;
    $XScale = ($GraphRightEdge - $GraphLeftEdge) / ($MaxCount - $MinCount); 
    $x1 = $GraphLeftEdge;
    $y1 = $GraphTopEdge + 5 + $Number * $YBarStride;
    $x2 = $GraphLeftEdge + $X * $XScale;
    $y2 = $y1 + $BarHeight - 1;
    
    $myImage->filledRectangle($x1,$y1,$x2,$y2,$GraphColor);
    
    $myImage->string(gdSmallFont,$ImageLeftEdge+3,$y1,$Label,$ValueTextColor);
}
