#!/usr/bin/perl
#
# Summarize all IP accounting files from start to end time
#
# Copyright (C) 1997 - 1999 Moritz Both
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# The author can be reached via email: moritz@daneben.de, or by
# snail mail: Moritz Both, Im Moore 26, 30167 Hannover,
#             Germany. Phone: +49-511-1610129
#

use 5.000;
use Getopt::Long;
use Sys::Hostname;

BEGIN {
	eval {require GD; import GD;};
	$have_GD = $@ ? 0 : 1;
}

@moff = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 );
@mofg = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
@mons = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", 
	 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
@wday_short = ("Su", "Mo", "Tu", "We", "Th", "Fr", "Sa");
@wday = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");

# =()<$datdir="@<ACCTDIR>@";>()=
$datdir="/var/log/ip-acct";
# =()<$datdelim="@<DATDELIM>@";>()=
$datdelim="#-#-#-#-#";
# =()<$version="@<VERSION>@";>()=
$version="1.05";
# =()<$chain_name[0]="@<CH_INNAME>@";>()=
$chain_name[0]="ipac_in";
# =()<$chain_name[1]="@<CH_OUTNAME>@";>()=
$chain_name[1]="ipac_out";
# =()<$chain_name[2]="@<CH_IONAME>@";>()=
$chain_name[2]="ipac_bth";
$rcs_id = q|$Id: ipacsum,v 1.47 1999/08/06 10:53:53 moritz Exp $|;
$copyright="(C) 1997, 1998 Moritz Both";

$me=$0;
$me =~ s|^.*/([^/]+)$|$1|;
$now=time;
$replace=0;
$exact = 0;
$progression = 0;		# if 1, collect progressions (needs memory!)
$graph = 0;			# make ascii graphs
$gif = undef;			# make gif graphs
$rule_regex = ".*";		# match rules with this regex only
$show_run_progression = 0;	# show % finished while running

$c_day = 24*60*60;
$c_year = $c_day * 365.24;
	
# a list of possible x axis separator intervals and -alignments.
@possible_interval = (
	1, 2, 5, 10, 15, 20, 30,		# sec
	60, 120, 300, 600, 900, 1200, 1800,	# 1,2,5,10,15,20,30 min
	3600, 7200, 10800, 14400,		# 1,2,3,4 hours
	21600, 43200,				# 6,12 hours
	$c_day, $c_day*2, $c_day*5, $c_day*7,
	$c_day*15, $c_day*30,			
	$c_day*60, $c_year/4, $c_year/3,
	$c_year/2, $c_year, $c_year*2,
	$c_year*4, $c_year*5, $c_year*10, $c_year*20,
	$c_year*25, $c_year*50, $c_year*100);	# hope this is enough

# a list of possible time labels on x axis of gif image with reasonable
# time limits.
# if duration is under... display time as...
@gif_time_labels = (
	4*60,		'sprintf("%02d\'%02d\'\'", $m, $s)',
	60*60,		'sprintf("%02d", $m)',
	$c_day*2,	'sprintf("%02d:%02d", $h, $m)',
	5*$c_day,	'sprintf("%s %02d", $WD, $h)',
	50*$c_day,	'sprintf("%02d.", $D)',
	2*$c_year,	'sprintf("%02d/%02d", $M, $D)',
	1E37,		'sprintf("%04d", $Y)',
);

$graph_width = 55;
$graph_interval = 60*60;	# seconds
$graph_interval_explicit = 0;	# 1 if the user set it

# gif defaults.
# =()<$gif_width=@<GIF_WIDTH>@;>()=
$gif_width=500;
# =()<$gif_height=@<GIF_HEIGHT>@;>()=
$gif_height=150;
# =()<$gif_index_default_name = "@<GIF_INDEX_DEFAULT>@";>()=
$gif_index_default_name = "index.html";

# gif defaults.
$gif_x_sp = 50;
$gif_x_spr = 3;			# space on right side of gif picture
$gif_y_sp = 15;
$gif_xaxis_sep_per_pix = 0.015;	# seperators on x axis per pixel
$gif_yaxis_sep_per_pix = 0.04;  # separators on y axis per pixel
$gif_yaxix_sep_width = 2;
$gif_xaxix_sep_height = 2;
$gif_font = GD::gdSmallFont;
$gif_font_offset_xax_x = -3;
$gif_font_offset_xax_y = 1;
$gif_font_offset_yax_x = -7;
$gif_font_offset_yax_y = -8;
$gif_average_character_width = 6;
$gif_average_character_width_vert = 4;
$gif_top_label_height = 15;
$gif_index = 0;			# wheather to generate index.html
$gif_normalized = 1;		# normalize gif: show bps (not b per 
				# graph_interval)
$gif_caption_in_index = 0;	# make max: and Average: in html index file
$gif_no_average = 0;		# dont draw dotted horizontal line with average
$gif_asis = 0;			# create 'asis' files (apache)
$gif_average_curve = 0;		# create 'average on n dots around' line

# gif defaults for other fonts.
%gif_defaults_fonts = (
	TINY	=> [
		\$gif_x_sp,			30,
		\$gif_y_sp,			10,
		\$gif_xaxis_sep_per_pix, 	0.03,
		\$gif_yaxis_sep_per_pix, 	0.04,
		\$gif_font_offset_yax_y,	-3,
		\$gif_font_offset_xax_y,	2,
		\$gif_average_character_width,	3,
		\$gif_top_label_height,		10,
	]
);


# calculate time zone offset in seconds - use difference of output of date
# command and time function, round it
$tzoffset = 0;			# ! makeunixtime needs this!
$tzoffset = int( ($now-makeunixtime(`date +"%Y%m%d%H%M%S"`)) / 60) *60;
# get time zone name
$tzname = `date +%Z`; chop $tzname;
# get host name
$hostname = &hostname;

$starttime=0 + $tzoffset;
$endtime=$now;
$starttime_explicit=0;
$endtime_explicit=0;

# configure option parser if the Getopt::Long package is new enough to
# support this.
eval {&Getopt::Long::config("bundling_override")};
$getopt_supports_bundling = $@ ? 0 : 1;
# parse command line. Option values are placed in $opt_X
@GetOptionsControl= (
	"d|dir=s", 
	"debug-current-time=s",
	"e|endtime=s",
	"f|filter=s",
	"gif:s",
	"gif-asis",		\$gif_asis,
	"gif-average-curve=i" =>\$gif_average_curve,
	"gif-no-average" =>	\$gif_no_average,
	"gif-caption-in-index"=>\$gif_caption_in_index,
	"gif-height=i"	=>	\$gif_height,
	"gif-index:s"	=>	\$gif_index,
	"gif-normalize=i"=>	\$gif_normalized,
	"gif-use-smallfont",
	"gif-width=i"	=>	\$gif_width,
	"g|graph",
	"h|help",
	"i|interval|intervall=s",
	"show-run-progression"	=>	\$show_run_progression,
	"r|replace",
	"s|starttime=s",
	"t|timeframe=s",
	"version",
	"x|exact",
);

if (! &GetOptions(@GetOptionsControl)) {
	unless ($getopt_supports_bundling) {
		warn "(Use a space character between option letters and ".
			"their values. Or update to a\nnewer version of the ".
			"Getopt::Long perl module.)\n";
	}
	die "$me: illegal option specified. \"$me --help\" for help.\n";
}

if ($opt_version || $opt_version) {
	print <<EOF;
ipacsum version $version
$rcs_id
EOF
	exit 0;
}

if ($opt_h || $opt_h) {
	&usage_gif if (defined($opt_gif));
	&usage;
}

$now = &makeunixtime($opt_debug_current_time)
	if ($opt_debug_current_time);

if ($opt_s) {
	$starttime_explicit=1;
	$starttime=makeunixtime($opt_s);
	$starttime = $now - &parse_cmd_time($opt_s) if (!$starttime);
}
if ($opt_e) {
	$endtime_explicit=1;
	$endtime=makeunixtime($opt_e);
	$endtime = $now - &parse_cmd_time($opt_e) if (!$endtime);
}
$exact=1 if ($opt_x || $opt_x);

$replace = 1 if ($opt_r || $opt_r);

$graph = 1 if ($opt_g || $opt_g);
$gif = $opt_gif if (defined($opt_gif));
$gif = "." if (defined($gif) && !$gif);
if ($gif && !$have_GD) {
	die "$me: cant draw gif images because perl GD library not found\n";
}

if ($opt_gif_use_smallfont || $opt_gif_use_smallfont) {
	my($i);
	$gif_font = gdTinyFont;
	for ($i=$[; $i <= $#{$gif_defaults_fonts{TINY}}; $i+=2) {
		${${$gif_defaults_fonts{TINY}}[$i]} 
				= ${$gif_defaults_fonts{TINY}}[$i+1];
	}
}

$progression = 1 if ($graph || $gif);
$rule_regex = $opt_f if ($opt_f);

if ($opt_i) {
	die "$me: invalid option --interval without --graph or --gif\n" 
		if (! $graph && ! $gif);
	$graph_interval=parse_cmd_time($opt_i);
	$graph_interval_explicit = 1;
}

if ($opt_t) {
	$starttime_explicit=$endtime_explicit=1;
	&set_time_frame($opt_t);
}

$datdir = $opt_d if ($opt_d);

$endtime = $now if ($endtime > $now);
$mystarttime = makemytime($starttime);
$myendtime = makemytime($endtime);
%rule_firstfile =  %rule_lastfile = ( );

# Loop through dir, identify files
opendir(DIR, $datdir) || die "$me: can't open datdir $datdir\n";
while(defined($file = readdir DIR)) {
	next if ($file =~ /^\./);
	next if ($file lt $mystarttime || $file gt $myendtime);
	push(@files, $file);
}
closedir DIR;

$rulenumber=0;
@files = sort @files;

if ($show_run_progression) {
	$| = 1;
	$run_s = "";
	printf "       (%d files total)\r", $#files+1;
}
for ($ifile=0; $ifile<=$#files; $ifile++) {
	if ($show_run_progression) {
		my($s) = sprintf "%3d %%\r", 
			int($ifile * 100 / ($#files + 1));
		if ($s != $run_s) {
			print($s);
			$run_s = $s;
		}
	}
	&collect_data_from_file($ifile);
}
print "                                          \r" if ($show_run_progression);

@rules_sorted = sort { $rulenames{$a} <=> $rulenames{$b} } keys %rulenames;

&make_one_file_from_many() if $replace;

printf "IP accounting summary\nHost: $hostname / Time created: %s $tzname\n",
	nice_date(makemytime($now));
printf "Data from %s $tzname to %s $tzname\n", 
	nice_date($starttime_explicit || !@files ? $mystarttime : $files[$[]), 
	nice_date($myendtime);

$incomplete_data=0;
foreach (@rules_sorted) {
	if (/$rule_regex/) {
		&print_sum_line($_);
	}
}
if ($incomplete_data) {
	print "* = data incomplete, rule was not there all the time\n";
}

if ($graph || $gif) {
	&out_graph($gif);
}

sub print_sum_line {
	my($f) = shift;
	my($s) = " ";

	if (($#files + 1) > $rule_count{$f}) {
		$incomplete_data++;
		$s="*";
	}

	printf("%s %s: %15s\n", 
		$s,
		$filter{$f}, 
		$exact ? $bytes{$f} : &nice_number($bytes{$f})
	);
}

# read one file - must be called in correct sorted file name order
sub collect_data_from_file {
	my($file, $ifile, $s, $beforedata, $indata, $i, @rulenames);
	my($pkts, $bytes, $iCh);

	$ifile=shift;
	$file=$files[$ifile];
	$indata=0;
	$beforedata=1;

	open(FILE, "$datdir/$file") || die "$me: cant open file $file\n";
	$i=-1;
	while(<FILE>) {
		if ($beforedata) {
			if (/^$datdelim$/) {
				$beforedata=0;
				$indata=1;
				next;
			}
			next if (/^#/);
			chop;
			push(@rulenames, $_);
			$rulenames{$_}=$rulenumber++ if (!defined($rulenames{$_}));
			next;
		}
		if (/^IP accounting rules/) {
			$indata = 1;
			next;
		}
		if ($indata && /^\s*(\d+)\s+(\d+)/) {
			$indata=1;
			$beforedata=0;
			$i++;
			# found accouting data
			($pkts, $bytes) = ($1, $2);

			$s = $rulenames[$i];
			if (!defined($s)) {
				print STDERR "$me: more data than rules in $file - extra ignored\n";
				last;
			}
			&init_filter_id($s, $ifile);
			$bytes{$s} += $bytes;
			$pkts{$s} += $pkts;
			$rule_count{$s}++;
			$rule_lastfile = $file;
			if ($progression) {
				$$prog_bytes{$s}[$ifile] += $bytes;
				$$prog_pkts{$s}[$ifile] += $pkts;
				$prog_bytes_max{$s} = $bytes
					if ($prog_bytes_max{$s} < $bytes);
				$prog_pkts_max{$s} = $pkts
					if ($prog_pkts_max{$s} < $pkts);
			}
		}
	}
	if (defined($rulename[$i+1])) {
		print STDERR "$me: more rules declared than data in file $file?!";
	}
	close FILE;
}

sub init_filter_id {
	my($s, $ifile) = @_;

	if (!defined $bytes{$s}) {
		$bytes{$s}=0;
		$pkts{$s}=0;
		$filter{$s} = sprintf("%-48s", $s);
		$rule_firstfile{$s} = $files[$ifile];
		$rule_lastfile{$s} = "";
		$rule_count{$s} = 0;
		if ($progression) {
			$prog_pkts{$s} = [ ];
			$prog_pkts_max{$s} = 0;
			$prog_bytes{$s} = [ ];
			$prog_bytes_max{$s} = 0;
		}
	}
}

# given a string in format YYYYMMDD[hh[mm[ss]]], make unix time
# use time zone offset $tzoffset (input=wall clock time, output=UTC)
sub makeunixtime {
	my($y, $m, $d, $h, $i, $e);
	$s = shift;

	$h=0; $i=0; $e=0;
	if ($s =~ /^(\d\d\d\d)(\d\d)(\d\d)/) {
		($y, $m, $d) = ($1, $2, $3);
		if ($s =~ /^\d\d\d\d\d\d\d\d-?(\d\d)/) {
			$h=$1;
			if ($s =~ /^\d\d\d\d\d\d\d\d-?\d\d(\d\d)/) {
				$i=$1;
				if ($s =~ /^\d\d\d\d\d\d\d\d-?\d\d\d\d(\d\d)/) {
					$e=$1;
				}
			}
		}
	}
	else {
		return 0;
	}

	$y-=1970;
	$s = (($y)*365) + int(($y+2)/4) + $moff[$m-1] + $d-1;
	$s-- if (($y+2)%4 == 0 && $m < 3);
	$s*86400 + $h*3600 + $i*60 + $e + $tzoffset;
}

sub makemytime {
	my($s)=shift;

	my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
                                                 localtime($s);
	# ugly side effect of this function: set a global scalar
	# containing the day of week number.
	$mmt_wday = $wday;
	return sprintf("%04d%02d%02d-%02d%02d%02d", 1900+$year, $mon+1, $mday,
		$hour, $min, $sec);
}

# parse time as a duration
# syntax is
# cmd_time:		seconds | cmd_time_with_size
# cmd_time_with_size:	cmd_time_atom | cmd_time_with_size cmd_time_atom
# cmd_time_atom:	number size
# size:			"s"|"m"|"h"|"D"|"W"|"M"|"Y"
#			(sec, min, hours, Days, Weeks, Months, Years)
# seconds:		number
# return number of seconds
sub parse_cmd_time {
	my($sec) =0;
	$_=shift;

	return $_ if (/^\d+$/);

	while($_) {
		if (! /^(\d+)\s?([smhDWMY])(.*)$/) {
			die "$me: syntax error in time (duration)\n";
		}
		$_=$3;
		if ($2 eq "s") {
			$sec += $1;
		}
		elsif ($2 eq "m") {
			$sec += $1*60;
		}
		elsif ($2 eq "h") {
			$sec += $1*60*60;
		}
		elsif ($2 eq "D") {
			$sec += $1*60*60*24;
		}
		elsif ($2 eq "W") {
			$sec += $1*60*60*24*7;
		}
		elsif ($2 eq "M") {
			$sec += $1*60*60*24*30;
		}
		elsif ($2 eq "Y") {
			$sec += $1*60*60*24*365;
		}
	}
	$sec;
}

sub set_time_frame {
	my($opt_t) = shift;
	my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
                                                 localtime($now);

	
	if ($opt_t =~ /^this hour/i) {
		$opt_t = "the hour 0 hours ago";
	}
	elsif ($opt_t =~ /^last hour/i) {
		$opt_t = "the hour 1 hour ago";
	}
	elsif ($opt_t =~ /^today/i) {
		$opt_t = "the day 0 days ago";
	}
	elsif ($opt_t =~ /^yesterday/i) {
		$opt_t = "the day 1 day ago";
	}
	elsif ($opt_t =~ /^the day before yesterday/i) {
		$opt_t = "the day 2 days ago";
	}
	elsif ($opt_t =~ /^this week/i) {
		$opt_t = "the week 0 weeks ago";
	}
	elsif ($opt_t =~ /^last week/i) {
		$opt_t = "the week 1 week ago";
	}
	elsif ($opt_t =~ /^the week before last week/i) {
		$opt_t = "the week 2 weeks ago";
	}
	elsif ($opt_t =~ /^this month/i) {
		$opt_t = "the month 0 months ago";
	}
	elsif ($opt_t =~ /^last month/i) {
		$opt_t = "the month 1 month ago";
	}
	elsif ($opt_t =~ /^this year/i) {
		$opt_t = "the year 0 years ago";
	}
	elsif ($opt_t =~ /^last year/i) {
		$opt_t = "the year 1 year ago";
	}

	if ($opt_t =~ /^the hour (\d+) hours? ago/i) {
		$i=$1;
		my($thishour)=makeunixtime(sprintf("%04d%02d%02d%02d0000", 
			1900+$year, $mon+1, $mday, $hour));
		$starttime=$thishour - 60*60 * $i;
		$endtime = $thishour - 60*60 * ($i-1) -1;
	}
	elsif ($opt_t =~ /^the day (\d+) days? ago/i) {
		$i=$1;
		my($thismorning)=makeunixtime(sprintf("%04d%02d%02d000000", 
			1900+$year, $mon+1, $mday));
		$starttime=$thismorning - 60*60*24 * $i;
		$endtime=$thismorning - 60*60*24 * ($i-1) -1;
	}
	elsif ($opt_t =~ /^the week (\d+) weeks? ago/i) {
		$i=$1;
		$mday = $mday-($wday >0 ? $wday-1 : 6);
		if ($mday < 1) {
			$mon--;
			if ($mon < 0) {
				$mon += 12;
				$year--;
			}
			$mday += $mofg[$mon];
		}
		my($monday)=makeunixtime(sprintf("%04d%02d%02d000000", 
			1900+$year, $mon+1, $mday));
		$starttime=$monday - 60*60*24*7 * $i;
		$endtime=$monday- 60*60*24*7 * ($i-1) -1;
	}
	elsif ($opt_t =~ /^the month (\d+) months? ago/i) {
		$mon = $mon - $1;
		while ($mon < 0) {
			$year--;
			$mon += 12;
		}

		$starttime=makeunixtime(sprintf("%04d%02d01000000", 
			1900+$year, $mon+1));
		$endtime=$starttime + 60*60*24*$mofg[$mon] -1;
		$endtime += 60*60*24 if ((1900+$year)%4 ==0 && $mon==1);
	}
	elsif ($opt_t =~ /^the year (\d+) years? ago/i) {
		$i=$1;

		$starttime=makeunixtime(sprintf("%04d0101000000", 
			1900+$year-$i));
		$endtime=makeunixtime(sprintf("%04d0101000000", 
			1900+$year-$i+1))-1;
	}
	else {
		die "$me: Unknown time frame: \"$opt_t\"\n";
	}
}

sub nice_number {
	my($n)=shift;

	if ($n >  10484711424) {	# 9999 MByte
		$n = sprintf("%dG", $n/1073741824);
	}
	elsif ($n > 9999 * 1024) {
		$n = sprintf("%dM", $n/1048576);
	}
	elsif ($n > 9999) {
		$n = sprintf("%dK", $n/1024);
	}
	$n;
}

# format date in format YYYYMMDD-HHMMSS nicely.
sub nice_date {
	$s = shift;
	my($wday);

	$s =~ s@^(\d\d\d\d)(\d\d)(\d\d)-(\d\d)(\d\d)(\d\d)@$1/$2/$3 $4:$5:$6@;
	$s;
}

# Format a number representing seconds into a nice time (duration).
sub nice_time {
	my($s) = shift;
	my($t, $i, $mz);

	$t = "";
	$i = int($s / 31557600);
	$s = $s % 31557600;
	$mz = "s" if ($i > 1);
	$t = "$i year$mz " if ($i);
	$i = int($s / 86400);
	$s = $s % 86400;
	$mz = "s" if ($i > 1);
	$t = "$t$i day$mz " if ($i);
	$i = int($s / 3600);
	$s = $s % 3600;
	$mz = "s" if ($i > 1);
	$t = "$t$i hour$mz " if ($i);
	$i = int($s / 60);
	$s = $s % 60;
	$t = "$t$i min " if ($i);
	$t = "$t$s sec" if ($s);

	$t;
}

# replace: summarize all selected files to one
sub make_one_file_from_many {
	return if (! @files);
	my($fname) = $files[$#files]; # it's sorted

	# we "print" the file in memory
	my(@text);
	push(@text, sprintf("# ipac summary file generated %s\n", makemytime(time)));
	push(@text, sprintf("# source files: %s to %s\n", $files[0], $fname));
	push(@text, "#\n");

	foreach (sort { $rulenames{$a} <=> $rulenames{$b} } keys %rulenames) {
		push(@text, "$_\n");
	}
	push(@text, "$datdelim\n");
	foreach (sort { $rulenames{$a} <=> $rulenames{$b} } keys %rulenames) {
		push(@text, sprintf("%10s %15s\n", $pkts{$_}, $bytes{$_}));
	}
	
	# remove all source files
	foreach(@files) {
		unlink "$datdir/$_";
	}

	# write updated file
	if (! open(FILE, ">$datdir/$fname")) {
		warn "$me: could not open $datdir/$fname, writing to STDOUT\n";
		open(FILE, ">-") || die "$me: cant open STDOUT!\n";
	}
	print FILE @text;
	close FILE;
}

sub out_graph {
	my($gif) = shift;
	my(@rules_filtered, @rules_filtered_data, $i, $lst);

	# Do preparations for all graphs.
	$startt_graph = $starttime;
	$startt_graph = makeunixtime($files[$[]) 
			if (!$starttime_explicit && @files);

	# IF we make a gif and we show throughput instead of absolute values
	# (which is the normal case), AND
	#	the user has not set --interval, OR
	# 	$graph_inverval is smaller than one pixel would be
	# THEN
	# set $graph_interval to one pixel.
	$i = $gif_width-$gif_x_sp-$gif_x_spr; # actual x pixel number
	if ($gif && $gif_normalized && (
			($endtime - $startt_graph)/$graph_interval > $i
			|| ! $graph_interval_explicit)
			) {
		$graph_interval = ($endtime-$startt_graph) / $i;
	}

	# Calculate HTTP Expires: Header.
	# if $endtime is $now, set it to $now + $graph_interval.
	# if it is not $now, omit it.
	undef($expires);
	if ($endtime == $now) {
		$expires = &http_time($now + $graph_interval);
	}
	# HTTP Last-Modified (asis)
	$last_modified = &http_time($now) if ($gif_asis);
	
	$lst = "";
	foreach $rule (@rules_sorted) {
		next if ($rule eq $lst);
		$lst = $rule;
		if ($rule =~ /$rule_regex/) {
			push(@rules_filtered, $rule);
			my($graph_data) = &single_graph($rule, $gif);
			push(@rules_filtered_data, $graph_data);
		}
	}
	&out_index_html(\@rules_filtered, \@rules_filtered_data)
		if ($gif && defined($gif_index));
}

sub single_graph {
	my($rule) = shift;
	my($gif) = shift;
	my($max, $i, $ifile, $inter_st, $inter_end, $value, $iut, $oldiut);
	my($sum, $avg, @values, @valtime, $bytes, %dat);

	print "Graph for rule \"$rule\"\n" unless $gif;
#	$max = $prog_bytes_max{$rule};
	$max=0;
	$sum = 0;

	$inter_st = $startt_graph;
	$inter_end = $inter_st + $graph_interval;
	$value=0;
	$iut=$inter_st;
	for ($ifile=0; $ifile<=$#files; $ifile++) {
		#DBXXX("new file: nr=$ifile, name=$files[$ifile]\n");
		if (defined($$prog_bytes{$rule}[$ifile])) {
			$bytes = $$prog_bytes{$rule}[$ifile];
		}
		else {
			$bytes = 0;
		}
		$oldiut = $iut;
		$iut = makeunixtime($files[$ifile]);

		# if the file is younger than the end of the time frame
		# we are examining, split the count.
		while ($iut > $inter_end) {
			# number of bytes still in current time frame
			$i = int($bytes * ($inter_end-$oldiut)/($iut-$oldiut)+.5);
			#DBXXX(sprintf("- add rest: \$bytes=$bytes, \$value=$value, \$inter_st=%s, \$inter_end=%s,\n", 
			#	makemytime($inter_st), makemytime($inter_end)));
			#DBXXX(sprintf("\t\$iut=%s, \$oldiut=%s; so \$i=$i\n",
			#	makemytime($iut), makemytime($oldiut)));
			$value += $i;
			push(@values, $value);
			$max = $value if ($value > $max);
			$sum += $value;
			$value=0;
			$oldiut=$inter_end;
			$inter_st = $inter_end;
			$inter_end += $graph_interval;
			$bytes -= $i;
		}
		DBXXX("  add \$value += \$bytes: $value + $bytes ="); 
		$value += $bytes;
		DBXXX(" $value\n");
	}
	push(@values, $value);
	$max = $value if ($value > $max);
	$sum += $value;
	$avg = @values ? ($sum / ($#values + 1)) : 0;
	%dat = ( 'max' => $max, 'min' => 0, 'sum' => $sum, 'avg' => $avg );

	if ($gif) {
		my($i) = &draw_gif($gif_width, $gif_height, \%dat, 
			\@values,
			$startt_graph, $rule);
		my($filename)= $gif_asis ? "$rule.asis" : "$rule.gif";
		$filename = &good_filename($filename);
		if (-d $gif) {
			$filename="$gif/$filename";
		}
		open(OUT, ">$filename") || die "$me: can't open \"$filename\": $!\n";
		if ($gif_asis) {
			print OUT "Status: 200 OK\n";
			print OUT "Expires: $expires\n" if ($expires);
			print OUT "Last-Modified: $last_modified\n";
			printf OUT "Content-Length: %d\n", length($i);
			print OUT "Content-Type: image/gif\n";
			print OUT "\n";
		}
		binmode OUT;
		print OUT $i;
		close OUT;
	}
	else {
		&draw_ascii_graph(\@values, $max, $startt_graph);
	}
	# return some interesting data about the graph.
	return \%dat;
}

sub good_filename {
	my($s) = shift;
	$s =~ s@[\s\\/\|\>\<]@_@g;
	$s;
}

sub draw_ascii_graph {
	my($values, $max, $startt_graph) = @_;
	my($s, $i, $inter_st);

	printf "time          bytes 0%s%5s\n", string(" ", $graph_width-6), 
			nice_number($max);

	$inter_st=$startt_graph;
	for ($i=0; $i<=$#{$values}; $i++) {
		$s= $max ? string("*", 
			int(($$values[$i]/$max)*$graph_width+.5)
			) : "";
		printf "%s %s\n", nice_date(makemytime($inter_st)), $s;
		$inter_st += $graph_interval;
	}
}

# repeat string n times
sub string {
	my($s, $i);

	$s="";
	for ($i=1; $i<$_[1]; $i++) {
		$s .= $_[0];
	}
	$s;
}

sub draw_gif {
	return 0 unless $have_GD;
	my($xsiz, $ysiz, $dat, $values, $startt_graph, 
			$rule) = @_;

	my($maxval) = $$dat{'max'};
	my($minval) = $$dat{'min'};
	my($im) = new GD::Image($xsiz, $ysiz);
	# hack to leave pixels blank at the right border.
	$xsiz -= $gif_x_spr;

	my($backg) = $im->colorAllocate(224,224,224);
	my($white) = $im->colorAllocate(255,255,255);
	my($black) = $im->colorAllocate(0,0,0);
	my($blue)  = $im->colorAllocate(0,0,255);
	my($green) = $im->colorAllocate(0,128,0);
	my($red)   = $im->colorAllocate(255,0,0);
	my($gif_arc_size)=1;

	my($x, $y, $i, $s, $xstep, $ymulti, $arc_color, $line_color,
		$font_color, $title_color, $axle_color, $x1, $y1, $duration,
		$normalize_factor, $avg_color);

	$im -> interlaced(1);

	$maxval = 1 unless $maxval;
	$gif_arc_size = 5 unless $gif_normalized;
	# normalize_factor is a multiplier for Y values to get user
	# caption values.
	$normalize_factor = $gif_normalized?$gif_normalized/$graph_interval : 1;
	$$dat{'max_caption'} = $maxval * $normalize_factor;
	$$dat{'avg_caption'} = $$dat{'avg'} * $normalize_factor;
	$xstep = ($xsiz-$gif_x_sp) / ($#{$values} + 1);
	$ymulti = ($ysiz-$gif_y_sp-$gif_top_label_height) / ($maxval-$minval);
	$arc_color = $black;
	$line_color = $black;
	$font_color = $blue;
	$axle_color = $blue;
	$title_color = $green;
	$avg_color = $green;
	$avg_line_color = $red;

	# draw axles.
	$im->line($gif_x_sp, $ysiz-$gif_y_sp, $xsiz, $ysiz-$gif_y_sp, 
			$axle_color);
	$im->line($gif_x_sp, $gif_top_label_height, $gif_x_sp, 
			$ysiz-$gif_y_sp, $axle_color);
	$s = $gif_normalized ? $gif_normalized : $graph_interval;
	if ($s == 1) {
		$s = "bytes / sec";
	}
	elsif ($s == 8) {
		$s = "bit / sec";
	}
	else {
		$s = "bytes / $s sec";
	}
	&centerStringUp($im, $s, $gif_font, 1, 
				$gif_top_label_height,
				$ysiz, $font_color);

	# draw total average line if requested.
	unless ($gif_no_average) {
		$y = ($ysiz - $gif_y_sp) - $$dat{'avg'} * $ymulti;
		$im -> GD::Image::dashedLine($gif_x_sp, $y, $xsiz, $y, 
				$avg_color);
	}

	# compute optimal interval of labels.
	unless (defined($x_labels)) {
		$x_labels = &optimal_label_interval_time(
			int($gif_xaxis_sep_per_pix * ($xsiz-$gif_x_sp) + 0.5), 
			$#{$values}+1, $startt_graph);
	}
	my($y_lab_int) = &optimal_label_interval_2(
			int($gif_yaxis_sep_per_pix * 
			($ysiz-$gif_y_sp-$gif_top_label_height) + 0.5), 
			$maxval*$normalize_factor, 1);

	# draw labels.
	# X AXIS
	$y = $ysiz - $gif_y_sp;
	# time we cover in seconds
	$duration = ($#{$values}+1) * $graph_interval;
	# $%x_labels: keys are times, values are label strings
	foreach $i (keys %$x_labels) {
		$x = $gif_x_sp + (($i - $startt_graph)/$duration *
				($xsiz - $gif_x_sp));
		$im->line($x, $y - $gif_xaxix_sep_height,
				$x, $y + $gif_xaxix_sep_height, $axle_color);
		$im->string($gif_font, 
				$x + $gif_font_offset_xax_x -
					length($$x_labels{$i}) / 2 *
					$gif_average_character_width, 
				$y + $gif_font_offset_xax_y,
				$$x_labels{$i}, $font_color);
	}
		
	# Y AXIS.
	$x = $gif_x_sp;
	for ($i = 0; $i <= $maxval * $normalize_factor; $i += $y_lab_int) {
		$y = ($ysiz-$gif_y_sp) - 
			int(($i / $normalize_factor * $ymulti + 0.5));
		$im->line($x - $gif_yaxix_sep_width, $y,
				$x + $gif_yaxix_sep_width, $y, $axle_color);
		$s = &nice_number($i);
		$im->string($gif_font, 
				$gif_x_sp - $gif_average_character_width*
				length($s)+$gif_font_offset_yax_x, 
				$y + $gif_font_offset_yax_y,
				$s, $font_color);
	}

	# put the first dot
	$i = $[;
	$x = $gif_x_sp;
	$y = ($ysiz - $gif_y_sp) - $$values[$i++] * $ymulti;
	$im -> GD::Image::arc($x, $y, $gif_arc_size, $gif_arc_size,
			0, 360, $black) 
		if ($gif_arc_size >1);
	
	# put all lines and dots.
	$yavg = undef;
	my(@av_line);
	my($gal_offset) = int($gif_average_curve/2+.5);
	while($i <= $#{$values}) {
		$x1 = $x + $xstep;
		if ($gif_average_curve) {
			push(@av_line, $$values[$i]);
			shift(@av_line) if ($#av_line > $gif_average_curve);
			if ($#av_line == $gif_average_curve) {
				# draw it.
				my($ia);
				my($asum)=0;
				for ($ia=0; $ia<$gif_average_curve+1;
								$ia++) {
					$asum += $av_line[$ia];
				}
				$yavg1 = ($ysiz-$gif_y_sp)
						-($asum/$ia)*$ymulti;
				if ($yavg) {
					$im-> GD::Image::line($x-$gal_offset, 
							$yavg,
							$x1-$gal_offset, 
							$yavg1, 
							$avg_line_color);
				}
				$yavg = $yavg1;
			}

		}
		$y1 = ($ysiz - $gif_y_sp) - $$values[$i++] * $ymulti;
		$im -> GD::Image::arc($x1, $y1, $gif_arc_size, $gif_arc_size,
				0, 360, $arc_color)
			if ($gif_arc_size >1);
		$im -> GD::Image::line($x, $y, $x1, $y1, $line_color);
		$x = $x1;
		$y = $y1;
	}

	# put the title string.
	$s = &nice_date(&makemytime($startt_graph)); # sets &mmt_wday
	$s=sprintf("%s Start: %s %s max: %s avg: %s", $rule, 
			$wday[$mmt_wday], $s, 
			&nice_number(int($$dat{'max_caption'}+0.5)),
			&nice_number(int($$dat{'avg_caption'}+0.5)));
	$x1 = length($s)*$gif_average_character_width;
	$x = int($xsiz/2 - $x1/2+0.5);
	$im->string($gif_font, $x, 0, $s, $title_color);

	$im->gif;
}

sub optimal_label_interval {
	my($want_separators, $max, $useKM) = @_;
	my($mat, $exp); 

	return 1 if (! $max || ! $want_separators);
	$mat = $max / $want_separators;
	$exp=0;
	# stupid way, i am not a mathematican.
	while ($mat >= 10) {
		$mat = $mat / 10;
		$exp++;
	}
	while ($mat < 1) {
		$mat = $mat * 10;
		$exp--;
	}
	# $mat is now between 1 and 9.99...
	if ($mat < 1.7) {
		$mat = 1;
	}
	elsif ($mat < 3.7) {
		$mat = 2.5;
	}
	elsif ($mat < 7.5) {
		$mat = 5;
	}
	else {
		$mat = 10;
	}
	$mat * (10 ** $exp);
}

# compute a list of time labels for the x axix. Make about $want_separators
# labels. First column is of time $startt_graph, there are $n values.
sub optimal_label_interval_time {
	my($want_separators, $n, $startt_graph) = @_;
	my($duration, $sps, $div, $interval, $firsttime, $i, %result);
	my($Y, $M, $D, $h, $m, $s, $pfunc);

	# We need to find out what the best label notation and the best label
	# interval is. The magic measure is "seconds per separator", sps.
	$duration = $n * $graph_interval;
	$sps = $duration / $want_separators;

	# find the closest value from @possible_interval. Linear search.
	for ($i=$[; $i<=$#possible_interval && $sps > $possible_interval[$i]; 
			$i++) { }
	$i-- if ($i>$[ && 
		$possible_interval[$i]-$sps > $sps-$possible_interval[$i-1]);
	
	$interval = $possible_interval[$i];

	for ($i=$[; $i<=$#gif_time_labels; $i+=2) {
		last if ($gif_time_labels[$i] > $duration);
	}
	$pfunc = $gif_time_labels[$i+1];

	# this is the first time.
	$firsttime = int(($startt_graph + $interval - $tzoffset) / $interval
				- 0.001) 
			* $interval + $tzoffset;

	# generate a list of labels.
	$n="";
	for ($i = $firsttime; $i < $startt_graph+$duration; $i += $interval) {
		($s,$m,$h,$D,$M,$Y,$WD) = localtime($i);
		$WD = $wday_short[$WD];
		$M++;
		$Y+=1900;
		$result{$i} = eval($pfunc);
		if ($result{$i} eq $n) {
			# do not make two labels with the same value.
			$result{$i} = "";
		}
		else {
			$n = $result{$i};
		}
	}
	\%result;
}

sub optimal_label_interval_2 {
	my($want_separators, $max) = @_;
	my($mat, $x); 

	return 1 if (! $max || ! $want_separators);
	$mat = int($max / $want_separators + 0.5);

	# find the power of 2 which is most close to $mat.
	for ($x = 1; $x < $mat; $x <<= 1) {};
	if ($x-$mat < $mat-($x>>1)) {
		$mat = $x;
	}
	else {
		$mat = $x >> 1;
	}
	$mat ? $mat : 1;
}

sub out_index_html {
	my($rules) = shift;
	my($rules_data) = shift;

	my($filename);
	my($n_now, $n_st, $n_end, $s, $s1, $i);
	my($max, $avg, $tot, $resolution, $text);

	$n_st = nice_date($starttime_explicit || !@files ? $mystarttime : $files[$[]);
	$n_end = nice_date($myendtime);
	$n_now = nice_date(makemytime($now));
	$resolution = &nice_time($graph_interval);

	$filename = $gif_index ? $gif_index : $gif_index_default_name;
	if (-d $gif && $filename !~ m|^/|) {
		$filename="$gif/$filename";
	}
	$text =
qq|<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
|;
	$text .= qq|<META HTTP-EQUIV="Expires" CONTENT="$expires">\n|
		if ($expires);
	$text .= 
qq|<TITLE>ip accounting graph page</TITLE>
</HEAD>
<BODY>
<H1>ip accounting graph page</H1>
<TABLE border=0 cellspacing=0>
 <TR>
  <TD>Host:</TD><TD>$hostname</TD>
 </TR>
 <TR>
  <TD>Time created:</TD><TD>$n_now $tzname</TD>
 </TR>
 <TR>
  <TD>Data Start time:</TD><TD>$n_st $tzname</TD>
 </TR>
 <TR>
  <TD>Data End time:</TD><TD>$n_end $tzname</TD>
 </TR>
 <TR>
  <TD>Resolution (time/pixel):</TD><TD>$resolution</TD>
 </TR>
</TABLE>
|;

	$text .= "<FONT SIZE=-1>";
	foreach(@{$rules}) {
		$s = &good_filename($_);
		$text .= "[&nbsp;<A HREF=\"#$s\">$_</A>&nbsp;]&nbsp; ";
	}
	$text .= "</FONT>\n";

	for ($i=0; $i<=$#{$rules}; $i++) {
		$_ = ${$rules}[$i];
		$dat = ${$rules_data}[$i];
		$s = &good_filename($_);
		$max = &nice_number(int($$dat{'max_caption'} + 0.5));
		$avg = &nice_number(int($$dat{'avg_caption'} + 0.5));
		$tot = &nice_number($bytes{$_});
		$s1="";
		$s1="<BR>Max: $max Average: $avg Total: $tot" 
			if ($gif_caption_in_index);
		$text .= qq|<A NAME="$s"><H3>$_</H3></A>
  <IMG ALT="graph for $_" border=0 SRC="$s.| . ($gif_asis ? "asis":"gif") . 
  			qq|" WIDTH=$gif_width HEIGHT=$gif_height>
  $s1
<HR>
|;
	}

	$text .=
qq|<ADDRESS>
Generated by ipacsum which is part of IPAC version $version. IPAC home page:
<A HREF="http://www.comlink.apc.org/~moritz/ipac.html">
http://www.comlink.apc.org/~moritz/ipac.html</A>
</ADDRESS>
</BODY>
</HTML>
|;

	open(OUT, ">$filename") || die "$me: cant open \"$filename\": $!\n";
	print OUT $text;
	close OUT;
}

# Print a string upwards centered.
sub centerStringUp {
	my($im, $text, $font, $x, $y1, $y2, $color) = @_;
	
	$y1 = $y1 + int(($y2-$y1)/2 + 
		length($text)*$gif_average_character_width_vert/2 + 0.5);
	$im->stringUp($font, $x, $y1, $text, $color);
}

sub http_time {
	my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(shift);
	# Fri, 27 Nov 1998 13:50:41 GMT
	sprintf "%s, %02d %s %04d %02d:%02d:%02d GMT",
		$wday[$wday], $mday, $mons[$mon], $year+1900,
		$hour, $min, $sec;
}
sub usage {
	print <<EOF;
ipacsum V$version $copyright; see file COPYING for license
Generates summary of ip accounting -- Usage: $me [Options]
Options:
 --starttime t, -s t 	Start time, default: The epoch
 --endtime t, -e t	End time, default: now
Times are either absolute in format YYYYMMDD[hh[mm[ss]]] !Note year is 4 digit!
 or relative in format n{s|m|h|D|W|M|Y}... (=sec, min, hours, Days, Weeks...)
 --filter r, -f r	filter output by rule names on regular expression
 --graph, -g		print ascii progression graph for every rule
 --interval n, -i n	specify progression graph (-g) interval; default 1 
			  hour; format: any combination of (number size) pairs, 
			  where size is one of smhDWMY (sec,min,hours,days,...)
 --timeframe t, -t t	Start and End time in one; time_frame is one of
			  today, yesterday, "the day before yesterday",
			  "the day n days ago", "this week", "last week", 
			  "the week n weeks ago" and so on with (months, years)
 --help, -h 		Print this help
 --replace, -r 		replace all summarized accounting files by one.
			  file name will be according to end time 
 --exact, -x		eXact values (dont use K or MByte values)
 --dir d, -d d		specify directory containing the accounting data
 --show-run-progression while running, show number of input files + progression
GIF image creation options: Type $me --help --gif for help.
EOF
	exit 0;
}

sub usage_gif {
	print <<EOF;
ipacsum V$version $copyright; see file COPYING for license

GIF image creation options:
 --gif [DIR]		create gif images for each rule in directory DIR
 			 (default DIR is the current directory)
 --gif-caption-in-index print statistical data in html index file
 --gif-height N		image height in pixels; default: $gif_height
 --gif-index [FILE]	create HTML index file named FILE in gif directory;
 			 default name: $gif_index_default_name
 --gif-no-average	do not draw dotted horizontal line for average value
 --gif-average-curve N	draw extra line with "defocused" values
 --gif-normalize SEC	set Y axis scale to bytes/SEC. Set to 0 to make scale 
			 absolute according to --interval setting; set to 8
			 for bit/sec. Default is 1 (bytes/sec).
 --gif-use-smallfont	use a smaller font in the image
 --gif-width N		image width in pixels; default: $gif_width

EOF
	print <<EOF;
NOTE: Image creation functions work only if you have the perl GD library
EOF
	print "installed. This machine does ";
	if ($have_GD) {
		print "have it, so everything should work fine";
	}
	else {
		print "not have it, so image creation is disabled";
	}
	print ".\n\n";

	exit 0;
}

sub DBXXX {
#	print @_;

}

# EOF
