#!/usr/bin/perl

use strict;

use CGI::Carp qw(fatalsToBrowser);
use Time::Local;
use Time::Piece;
use Time::HiRes qw(time);
#use HTTP::UA::Parser;
use HTTP::BrowserDetect();
use Digest::MD5 qw(md5_hex);

###################
# Initialisierung #
###################

my $gScriptName = $ENV{'SCRIPT_NAME'};
my $gDocumentRoot = $ENV{'DOCUMENT_ROOT'};
my ($gLanguage) = $ENV{'HTTP_ACCEPT_LANGUAGE'} =~ /^([a-z]+)/;

my $gFileNameTemplateHtml = 'showlog.html';
my $gFileNameLogDb = 'pagelog.db';
my $gFileNameLogOverflowDb = 'pagelog-overflow.db';
my $gFileNameLogArchiveDb = 'pagelog-@TIME@.db';
my $gFileNameLogDbPattern = 'pagelog.*\.db';
my $gSettingsDb = 'settings.db';

my $gLastShowTime = 9999999999;
my $gOutputGeneratedTime = time();
my $gStartTimeHiRes = time;
my $gMaxLogLines = 2000;
my $gLogDays = 0;

my @gLines;
my @gLinesTargets;
my @gLinesSessions;

my @gBotStrings = (
#	'AhrefsBot',
#	'Applebot',
#	'bingbot',
	'Dataprovider',
#	'Googlebot',
#	'YandexBot',
	'bot',
	'crawler',
	'spider'
);

my $gBotMarker = '<span style="background-color: yellow;">&nbsp;BOT&nbsp;</span>';
my $gErrorMarker = '<span style="background-color: red; color: white;">&nbsp;%s&nbsp;</span>';

# statistische Werte, durch CreateStatistics in einem ersten Durchlauf ermittelt:
my $gTotalHits = 0;
my %HitsPerTarget;
my %HitsPerSession;
my %HitsPerReferrer;
my %HitsPerLanguage;
my %HitsPerBrowser;
my %HitsPerOS;
my %HitsPerDevice;
my %HitsPerDay;

my $gTotalHitsNew = 0;
my %HitsPerTargetNew;
my %HitsPerSessionNew;
my %HitsPerReferrerNew;
my %HitsPerLanguageNew;
my %HitsPerBrowserNew;
my %HitsPerOSNew;
my %HitsPerDeviceNew;

my @IpHashes; # gesammelte IP-Hashes zur Ermittlung der Besuchernummern
my @Logfiles;

#################
# Hauptprogramm #
#################

my %FORM;
ParseFormData(\%FORM);
SelectLogFile();
PrintHttpHeader();
ReadSettings();
LoadEntries();
EnumerateEntries();
CreateStatistics();
SortEntriesTargets();
SortEntriesSessions();
PrintEntries();
SaveSettings();
exit 0;

##############
# Funktionen #
##############

#
# Überträgt die mittels "POST" an das Formular übergebenen Daten
# in den Hash, dessen Referenz der Funktion übergeben wird. Der
# Zugriff auf die Daten erfolgt dann mit $Hashname{'Feldname'}.
#
sub ParseFormData
{
	my $FormHashRef = shift;

	my $formdata;

	read(STDIN, $formdata, $ENV{'CONTENT_LENGTH'});

	my @pairs = split(/&/, $formdata);

	for my $pair (@pairs)
	{
		my ($name, $value) = split(/=/, $pair);
		$value =~ tr/+/ /;
		$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/meg;
		${$FormHashRef}{$name} = $value;
	}
}

sub SelectLogFile
{
	GetLogfiles(\@Logfiles);
	my $logfile = $FORM{'logfile'};
	if (defined $logfile)
	{
		$gFileNameLogDb = @Logfiles[$logfile];
	}
}

sub PrintHttpHeader
{
	print "Cache-Control: no-cache\n";
	print "Pragma: no-cache\n";
	print "Content-type: text/html; charset=utf-8\n\n";
}

#
# Liest die Einstellungen aus der DB-Datei.
#
sub ReadSettings
{
	if (open(DB, "$gSettingsDb"))
	{
		my @Lines = <DB>;
		close(DB);

		for my $Line (@Lines)
		{
			chomp $Line;
			if (index($Line, 'LastShowTime=') == 0)
			{
				$gLastShowTime = (split(/\=/, $Line))[1];
			}
		}
	}
}

#
# Schreibt die Einstellungen in die DB-Datei.
#
sub SaveSettings
{
	return if (defined $FORM{'logfile'}); # Nicht, wenn eine (andere) Logdatei ausgewählt wurde!

	my @Lines;
	push @Lines, "LastShowTime=$gOutputGeneratedTime\n"; 

	if (open(DB, ">$gSettingsDb"))
	{
		print DB @Lines;
		close(DB);
	}
}

#
# Liest die Zeilen aus der DB-Datei in ein String-Array ein.
# Wenn die Datei mehr als $gMaxLogLines Zeilen enthält, werden die ältesten Zeilen in eine Archivdatei verschoben.
#
sub LoadEntries
{
	open(PAGELOG_DB, "+<$gFileNameLogDb") or return;
	flock(PAGELOG_DB, 2);
	@gLines = <PAGELOG_DB>;

	my $lineCount = @gLines;
	if ($lineCount > $gMaxLogLines && !defined $FORM{'logfile'})
	{
		my @LinesOverflow = @gLines[0 .. $lineCount - $gMaxLogLines - 1];

		# Die letzen $gMaxLogLines Zeilen in die Logdatei zurückschreiben:
		@gLines = @gLines[$lineCount - $gMaxLogLines .. $lineCount - 1];
		seek(PAGELOG_DB, 0, 0);
		truncate(PAGELOG_DB, 0);
		print PAGELOG_DB @gLines;
		close(PAGELOG_DB);

		# Die übrigen Zeilen in die Overflow-Datei schreiben:
		open(OVERFLOW_DB, ">>$gFileNameLogOverflowDb");
		print OVERFLOW_DB @LinesOverflow;
		close(OVERFLOW_DB);

		# Wenn die Overflow-Datei nun ebenfalls mehr als $gMaxLogLines Zeilen enthält, dann umbenennen:
		open(OVERFLOW_DB, "$gFileNameLogOverflowDb");
		@LinesOverflow = <OVERFLOW_DB>;
		close(OVERFLOW_DB);

		if (@LinesOverflow > $gMaxLogLines)
		{
			my $time = (split(/\|/, @LinesOverflow[@LinesOverflow - 1]))[0]; # Zeit aus der letzten Zeile der Overflow-Datei
			rename($gFileNameLogOverflowDb, ReplaceLine($gFileNameLogArchiveDb, '@TIME@', FormatDateTime_YearToSecond($time)));
		}
	}
	else
	{
		close(PAGELOG_DB);
	}

	# Anzahl der Tage in der Logdatei
	if (@gLines > 0)
	{
		my $timeStart = (split(/\|/, @gLines[0]))[0];
		my $timeEnd = (split(/\|/, @gLines[@gLines - 1]))[0];
		if (defined $timeStart && defined $timeEnd)
		{
			#$gLogDays = FormatTimeSpan($timeEnd - $timeStart);
			$gLogDays = sprintf("%.1f", ($timeEnd - $timeStart) / 86400);
		}
	}
}

#
# Fügt den Zeilen folgende weitere Spalten hinzu:
# - Fortlaufende Zeilennummer (diese entspricht dann der Zeilennummer in der DB-Datei).
# - Browser, OS und Device (anhand des UserAgent ermittelt).
# Außerdem werden die IP-Hashes in einem Array gesammelt, um daraus später die Besuchernummern zu ermitteln.
#
sub EnumerateEntries
{
	my %uaHash; # Cache für Ergebnisse des UA-Parsers
	
#	my $uaParser = HTTP::UA::Parser->new();
	my $lineNo = 1;
	for my $Line (@gLines)
	{
		chomp $Line;

		my ($time, $target, $referrer, $language, $winsize, $userAgent, $ipHash, $errorCode) = split(/\|/, $Line);

		$target = '-' if ($target eq '');
		$referrer = '-' if ($referrer eq '');
		$language = '-' if ($language eq '');

		my $uaKey = md5_hex($userAgent);

		my $browser;
		my $os;
		my $device;

		if (exists $uaHash{$uaKey})
		{
			($browser, $os, $device) = @{$uaHash{$uaKey}}; # Array derefernzieren
		}
		else
		{
#			$uaParser->parse($userAgent);
#			my $browser = $uaParser->ua->family;
#			my $os = $uaParser->os->family;
#			my $device = ($uaParser->device->brand ne '' || $uaParser->device->model ne '') ? $uaParser->device->brand . ' ' . $uaParser->device->model : $uaParser->device->family;
# HTTP::UA::Parser ist sehr langsam, daher HTTP::BrowserDetect verwenden:
			my $uaParser = HTTP::BrowserDetect->new($userAgent);
			$browser = $uaParser->browser_string() || 'Other';
			$os = $uaParser->os_string() || 'Other';
			$device = $uaParser->device_string() || ($uaParser->robot_string() ? $uaParser->robot_string() . $gBotMarker : 'Other');

			$uaHash{$uaKey} = [($browser, $os, $device)]; # Durch [...] wird eine Referenz/Kopie des Arrays im Hash gespeichert
		}

#		$IpHashes{$ipHash} = 0 if ($ipHash ne ''); # (doch kein Hash verwenden, da die Reihenfolge der Elemente dann nicht chronologisch wäre)
		push @IpHashes, $ipHash unless ($ipHash eq '' || $ipHash ~~ @IpHashes);

		$Line = "$lineNo|$time|$target|$referrer|$language|$winsize|$userAgent|$ipHash|$errorCode|$browser|$os|$device\n";
		$lineNo++;
	}
}

#
# Sortiert die Zeilen so um, dass die zu einem Target gehörenden Einträge zusammengefasst (gruppiert) werden.
# Die Sortierung dieser Target-Gruppen erfolgt nach dem Datum des letzten Eintrags, so dass Target-Gruppen
# mit dem neuesten Eintrag weiter oben erscheinen.
#
sub SortEntriesTargets
{
	my %existingTargets;
	for my $Line (@gLines)
	{
		my $time = (split(/\|/, $Line))[1];
		my $target = (split(/\|/, $Line))[2];
		$existingTargets{$target} = $time;
	}

	for my $existingTarget (sort { $existingTargets{$b} <=> $existingTargets{$a} } keys %existingTargets)
	{
		for my $Line (sort { CompareEntries($b, $a) } @gLines)
		{
			my $target = (split(/\|/, $Line))[2];
			if ($target eq $existingTarget)
			{
				push @gLinesTargets, $Line;
			}
		}
	}
}

#
# Sortiert die Zeilen so um, dass die zu einer Session gehörenden Einträge zusammengefasst (gruppiert) werden.
# Die Sortierung dieser Session-Gruppen erfolgt nach dem Datum des letzten Eintrags, so dass Session-Gruppen
# mit dem neuesten Eintrag weiter oben erscheinen.
#
sub SortEntriesSessions
{
	my %existingSessions;
	for my $Line (@gLines)
	{
		my $time = (split(/\|/, $Line))[1];
		my $session = (split(/\|/, $Line))[7];
		$existingSessions{$session} = $time;
	}

	for my $existingSession (sort { $existingSessions{$b} <=> $existingSessions{$a} } keys %existingSessions)
	{
		for my $Line (sort { CompareEntries($b, $a) } @gLines)
		{
			my $session = (split(/\|/, $Line))[7];
			if ($session eq $existingSession)
			{
				push @gLinesSessions, $Line;
			}
		}
	}
}

sub CompareEntries
{
	my $a = shift;
	my $b = shift;

	my $time_a = (split(/\|/, $a))[1];
	my $time_b = (split(/\|/, $b))[1];

	$time_a <=> $time_b;
}

#
# Ermittelt in einem ersten Durchlauf statistische Werte, auf die später beim zweiten Durchlauf zurückgegriffen wird.
# Hierbei werden auch die Besucher durchnumeriert, indem die zuvor in einem Array gesammelen IP-Hashes
# zwecks schnellerem Zugriff in ein Perl-Hash umkopiert und dabei mit einer Besuchernummer versehen werden.
#
sub CreateStatistics
{
	my %IpHashes;
	my $session = 0;
	for my $ipHash (@IpHashes)
	{
		$IpHashes{$ipHash} = ++$session;
	}

	for my $Line (@gLines)
	{
		chomp $Line;

		my ($lineNo, $time, $target, $referrer, $language, $winsize, $userAgent, $ipHash, $errorCode, $browser, $os, $device) = split(/\|/, $Line);
		my $session = ($ipHash eq '' ? '-' : $IpHashes{$ipHash});
		$Line = "$lineNo|$time|$target|$referrer|$language|$winsize|$userAgent|$session|$errorCode|$browser|$os|$device\n";
		my $date = FormatDateTime_YearToDay($time);

		$gTotalHits++;
		$HitsPerTarget{$target}++ if ($target);
		$HitsPerSession{$session}++ if ($session);
		$HitsPerReferrer{$referrer}++ if ($referrer);
		$HitsPerLanguage{$language}++ if ($language);
		$HitsPerBrowser{$browser}++ if ($browser);
		$HitsPerOS{$os}++ if ($os);
		$HitsPerDevice{$device}++ if ($device);
		$HitsPerDay{$date}++;

		if ($time > $gLastShowTime)
		{
			$gTotalHitsNew++;
			$HitsPerTargetNew{$target}++ if ($target);
			$HitsPerSessionNew{$session}++ if ($session);
			$HitsPerReferrerNew{$referrer}++ if ($referrer);
			$HitsPerLanguageNew{$language}++ if ($language);
			$HitsPerBrowserNew{$browser}++ if ($browser);
			$HitsPerOSNew{$os}++ if ($os);
			$HitsPerDeviceNew{$device}++ if ($device);
		}
	}
}

#
# Erzeugt die HTML-Ausgabe anhand des HTML-Templates.
#
sub PrintEntries
{
	# HTML-Template einlesen:
	open(TPL, "$gFileNameTemplateHtml") or die "File not found: $gFileNameTemplateHtml";
	my @TemplateLines = <TPL>;
	close(TPL);

	# HTML-Templates
	# - Seitenkopf
	my $strTemplatePageHeader = ExtractAndReplaceLines(\@TemplateLines, '<page-header>', '</page-header>', '@PAGE_HEADER@');
	# - Statistik-Tabelle
	my $strTemplateStatData = ExtractAndReplaceLines(\@TemplateLines, '<template-stat-data>', '</template-stat-data>', '@STAT_DATA@');
	my $strTemplateStatTable = ExtractAndReplaceLines(\@TemplateLines, '<template-stat-table>', '</template-stat-table>', '@STAT_TABLE@');
	# - Haupt-Tabelle (Session)
	my $strTemplateSessionHeader = ExtractAndReplaceLines(\@TemplateLines, '<template-session-header>', '</template-session-header>', '@SESSION_TABLE@');
	my $strTemplateSessionData = ExtractAndReplaceLines(\@TemplateLines, '<template-session-data>', '</template-session-data>', '');
	# - Details-Tabelle (Session)
	my $strTemplateSessionDetailsHeader = ExtractAndReplaceLines(\@TemplateLines, '<template-session-details-header>', '</template-session-details-header>', '@DETAILS@');
	my $strTemplateSessionDetailsData = ExtractAndReplaceLines(\@TemplateLines, '<template-session-details-data>', '</template-session-details-data>', '');
	my $strTemplateSessionDetails = ExtractAndReplaceLines(\@TemplateLines, '<template-session-details>', '</template-session-details>', '');
	# - Haupt-Tabelle (Target)
	my $strTemplateTargetHeader = ExtractAndReplaceLines(\@TemplateLines, '<template-target-header>', '</template-target-header>', '@MAIN_TABLE@');
	my $strTemplateTargetData = ExtractAndReplaceLines(\@TemplateLines, '<template-target-data>', '</template-target-data>', '');
	# - Details-Tabelle (Target)
	my $strTemplateTargetDetailsHeader = ExtractAndReplaceLines(\@TemplateLines, '<template-target-details-header>', '</template-target-details-header>', '@DETAILS@');
	my $strTemplateTargetDetailsData = ExtractAndReplaceLines(\@TemplateLines, '<template-target-details-data>', '</template-target-details-data>', '');
	my $strTemplateTargetDetails = ExtractAndReplaceLines(\@TemplateLines, '<template-target-details>', '</template-target-details>', '');
	# - Balkendiagramm
	my $strTemplateHitsPerDayColumn = ExtractAndReplaceLines(\@TemplateLines, '<template-hitsperday-column>', '</template-hitsperday-column>', '@HITSPERDAY_COLUMNS@');

	# HTML-Ausgabe: Statistik-Tabellen
	my $tableStat = '';

	my $nNewTargets = 0;
	PrintStatistics('Target URL', \%HitsPerTarget, \%HitsPerTargetNew, \$nNewTargets, \$strTemplateStatData, \$strTemplateStatTable, \$tableStat);

	my $nNewReferrers = 0;
	PrintStatistics('Referrer URL', \%HitsPerReferrer, \%HitsPerReferrerNew, \$nNewReferrers, \$strTemplateStatData, \$strTemplateStatTable, \$tableStat);

	my $nNewLanguages = 0;
	PrintStatistics('Language', \%HitsPerLanguage, \%HitsPerLanguageNew, \$nNewLanguages, \$strTemplateStatData, \$strTemplateStatTable, \$tableStat);

	my $nNewBrowsers = 0;
	PrintStatistics('Browser', \%HitsPerBrowser, \%HitsPerBrowserNew, \$nNewBrowsers, \$strTemplateStatData, \$strTemplateStatTable, \$tableStat);

	my $nNewOSes = 0;
	PrintStatistics('Operating System', \%HitsPerOS, \%HitsPerOSNew, \$nNewOSes, \$strTemplateStatData, \$strTemplateStatTable, \$tableStat);

	my $nNewDevices = 0;
	PrintStatistics('Device', \%HitsPerDevice, \%HitsPerDeviceNew, \$nNewDevices, \$strTemplateStatData, \$strTemplateStatTable, \$tableStat);

	@TemplateLines = ReplaceLines(\@TemplateLines, '@STAT_TABLE@', $tableStat);

	# HTML-Ausgabe: Balkendiagramm
	my $columnsHitsPerDay = '';
	my $maxHitsPerDay = 0;
	my $previousDate = 0;
	for my $date (sort keys %HitsPerDay)
	{
		# Fehlende Tage ergänzen (0 Hits)
		while ($previousDate ne 0 && DaysBetweenDates($previousDate, $date) > 1)
		{
			$previousDate = AddDaysToDate($previousDate, 1);
			$HitsPerDay{$previousDate} = 0;
		}
		$previousDate = $date;
		# Maximale Hits ermitteln (entspricht dann 100%)
		my $hitsPerDay = $HitsPerDay{$date};
		$maxHitsPerDay = $hitsPerDay if ($hitsPerDay > $maxHitsPerDay);
	}
	for my $date (sort keys %HitsPerDay)
	{
		# HTML-Ausgabe
		my $hitsPerDay = $HitsPerDay{$date};

		my $html = $strTemplateHitsPerDayColumn;
		$html = ReplaceLine($html, '@DATE@', $date);
		$html = ReplaceLine($html, '@DAY@', FormatDateTime_WeekdayDate($date));
		$html = ReplaceLine($html, '@HITS@', $hitsPerDay);
		$html = ReplaceLine($html, '@PERCENT@', 100 * $hitsPerDay / $maxHitsPerDay);
		$columnsHitsPerDay .= $html;
	}
	@TemplateLines = ReplaceLines(\@TemplateLines, '@HITSPERDAY_COLUMNS@', $columnsHitsPerDay);

	# HTML-Ausgabe: Haupt-Tabelle (Session)
	my $lastSession = '';
	my $tableSession = '';
	my $tableSessionDetails = '';

	$tableSession .= $strTemplateSessionHeader;

	for my $Line (@gLinesSessions)
	{
		chomp $Line;

		my ($lineNo, $time, $target, $referrer, $language, $winsize, $userAgent, $session, $errorCode) = split(/\|/, $Line);

		if ($session ne $lastSession)
		{
			$lastSession = $session;

			if ($tableSessionDetails ne '')
			{
				my $html = $strTemplateSessionDetails;
				$html = ReplaceLine($html, '@DETAILS@', "$tableSessionDetails");
				$tableSession .= $html;

				$tableSessionDetails = '';
				$tableSession .= $strTemplateSessionHeader;
			}

			my $hitsPerSession = $HitsPerSession{$session};
			my $hitsPerSessionNew = $HitsPerSessionNew{$session};
			my $bIsNew = ($hitsPerSession == $hitsPerSessionNew);
			
			my $html = $strTemplateSessionData;
			$html = ReplaceLine($html, '@HITS@', $hitsPerSession || '-');
			$html = ReplaceLine($html, '@NEW@', $hitsPerSessionNew || '-');
			$html = ReplaceLine($html, '@DAYS@', int(($gOutputGeneratedTime - $time) / 86400) || '-');
			$html = ReplaceLine($html, '@SESSION@', $session || '-');
			$html = ReplaceLine($html, '@LANGUAGE@', TextToHtml($language));
			$html = ReplaceLine($html, '@WINSIZE@', TextToHtml($winsize));
			$html = ReplaceLine($html, '@USERAGENT@', (IsBot($userAgent) ? $gBotMarker : '') . TextToHtml($userAgent));
			$html = ReplaceLine($html, '@CLASS_NEW@', $bIsNew ? 'new' : '');
			$tableSession .= $html;

			$tableSessionDetails .= $strTemplateSessionDetailsHeader;
		}

		my $html = $strTemplateSessionDetailsData;
		$html = ReplaceLine($html, '@DATE@', FormatDateTime_YearToDay($time));
		$html = ReplaceLine($html, '@TIME@', FormatDateTime($time));
		$html = ReplaceLine($html, '@TARGET@', ($errorCode ? sprintf($gErrorMarker, $errorCode) : '') . TextToHtml($target));
		$html = ReplaceLine($html, '@REFERRER@',TextToHtml($referrer));
		$html = ReplaceLine($html, '@CLASS_NEW@', $time > $gLastShowTime ? 'new' : '');
		$tableSessionDetails .= $html;
	}

	if ($tableSessionDetails ne '')
	{
		my $html = $strTemplateSessionDetails;
		$html = ReplaceLine($html, '@DETAILS@', "$tableSessionDetails");
		$tableSession .= $html;
	}

	# HTML-Ausgabe: Haupt-Tabelle (Target)
	my $lastTarget = '';
	my $tableTarget = '';
	my $tableTargetDetails = '';

	$tableTarget .= $strTemplateTargetHeader;

	for my $Line (@gLinesTargets)
	{
		chomp $Line;

		my ($lineNo, $time, $target, $referrer, $language, $winsize, $userAgent, $session, $errorCode) = split(/\|/, $Line);

		if ($target ne $lastTarget)
		{
			$lastTarget = $target;

			if ($tableTargetDetails ne '')
			{
				my $html = $strTemplateTargetDetails;
				$html = ReplaceLine($html, '@DETAILS@', "$tableTargetDetails");
				$tableTarget .= $html;

				$tableTargetDetails = '';
				$tableTarget .= $strTemplateTargetHeader;
			}

			my $hitsPerTarget = $HitsPerTarget{$target};
			my $hitsPerTargetNew = $HitsPerTargetNew{$target};
			my $bIsNew = ($hitsPerTarget == $hitsPerTargetNew);
			
			my $html = $strTemplateTargetData;
			$html = ReplaceLine($html, '@HITS@', $hitsPerTarget || '-');
			$html = ReplaceLine($html, '@NEW@', $hitsPerTargetNew || '-');
			$html = ReplaceLine($html, '@DAYS@', int(($gOutputGeneratedTime - $time) / 86400) || '-');
			$html = ReplaceLine($html, '@TARGET@', ($errorCode ? sprintf($gErrorMarker, $errorCode) : '') . TextToHtml($target));
			$html = ReplaceLine($html, '@CLASS_NEW@', $bIsNew ? 'new' : '');
			$tableTarget .= $html;

			$tableTargetDetails .= $strTemplateTargetDetailsHeader;
		}

		my $html = $strTemplateTargetDetailsData;
		$html = ReplaceLine($html, '@DATE@', FormatDateTime_YearToDay($time));
		$html = ReplaceLine($html, '@TIME@', FormatDateTime($time));
		$html = ReplaceLine($html, '@REFERRER@',TextToHtml($referrer));
		$html = ReplaceLine($html, '@SESSION@', TextToHtml($session));
		$html = ReplaceLine($html, '@LANGUAGE@', TextToHtml($language));
		$html = ReplaceLine($html, '@WINSIZE@', TextToHtml($winsize));
		$html = ReplaceLine($html, '@USERAGENT@', (IsBot($userAgent) ? $gBotMarker : '') . TextToHtml($userAgent));
		$html = ReplaceLine($html, '@CLASS_NEW@', $time > $gLastShowTime ? 'new' : '');
		$tableTargetDetails .= $html;
	}

	if ($tableTargetDetails ne '')
	{
		my $html = $strTemplateTargetDetails;
		$html = ReplaceLine($html, '@DETAILS@', "$tableTargetDetails");
		$tableTarget .= $html;
	}

	# Auswahl Logfiles
	my $htmlOptionsLogfile = '';
	my $logfileIndex = 0;
	for my $logfile (@Logfiles)
	{
		my $selected = ($logfile eq $gFileNameLogDb) ? 'selected' : '';
		$htmlOptionsLogfile .= '<option value="' . $logfileIndex++ . '" ' . $selected . '>' . $logfile . '</option>';
	}

	# HTML-Ausgabe: Seiten-Header
	$strTemplatePageHeader = ReplaceLine($strTemplatePageHeader, '@OPTIONS_LOGFILE@', $htmlOptionsLogfile);
	$strTemplatePageHeader = ReplaceLine($strTemplatePageHeader, '@LOG_DAYS@', $gLogDays);
	$strTemplatePageHeader = ReplaceLine($strTemplatePageHeader, '@TOTAL_HITS@', $gTotalHits || 0);
	$strTemplatePageHeader = ReplaceLine($strTemplatePageHeader, '@NEW_HITS@', $gTotalHitsNew);
	$strTemplatePageHeader = ReplaceLine($strTemplatePageHeader, '@NEW_TARGETS@', $nNewTargets);
	$strTemplatePageHeader = ReplaceLine($strTemplatePageHeader, '@NEW_REFERRERS@', $nNewReferrers);

	# HTML-Ausgabe: Seite
	@TemplateLines = ReplaceLines(\@TemplateLines, '@PAGE_HEADER@', $strTemplatePageHeader);
	@TemplateLines = ReplaceLines(\@TemplateLines, '@TIME_LASTSHOW@', FormatDateTime($gLastShowTime));
	@TemplateLines = ReplaceLines(\@TemplateLines, '@TIME_GENERATED@', FormatDateTime($gOutputGeneratedTime));
	@TemplateLines = ReplaceLines(\@TemplateLines, '@SPAN_GENERATED@', time - $gStartTimeHiRes);
	@TemplateLines = ReplaceLines(\@TemplateLines, '@SESSION_TABLE@', "$tableSession");
	@TemplateLines = ReplaceLines(\@TemplateLines, '@MAIN_TABLE@', "$tableTarget");
	print @TemplateLines;
}

#
# Gibt statistische Infos aus.
#
sub PrintStatistics
{
	my $strTitle = shift;
	my $refHashHits = shift;
	my $refHashHitsNew = shift;
	my $refNewCount = shift;
	my $refTemplateData = shift;
	my $refTemplateTable = shift;
	my $refTable = shift;
	
	my $data = '';

#	my $bIsUserAgent = ($strTitle eq 'UserAgent');

	for my $key (sort { ${$refHashHits}{$b} <=> ${$refHashHits}{$a} || ${$refHashHitsNew}{$b} <=> ${$refHashHitsNew}{$a} || $a cmp $b } keys %{$refHashHits})
	{
		my $hits = ${$refHashHits}{$key};
		my $hitsNew = ${$refHashHitsNew}{$key};
		my $bIsNew = $hits == $hitsNew;
		
		my $html = ${$refTemplateData};
#		$html = ReplaceLine($html, '@DATA_NAME@', ($bIsUserAgent && IsBot($key) ? $gBotMarker : '') . TextToHtml($key));
		$html = ReplaceLine($html, '@DATA_NAME@', $key);
		$html = ReplaceLine($html, '@DATA_VALUE@', $hits || '-');
		$html = ReplaceLine($html, '@DATA_VALUE_NEW@', $hitsNew || '-');
		$html = ReplaceLine($html, '@CLASS_NEW@', $bIsNew ? 'new' : '');
		$data .= $html;

		${$refNewCount}++ if ($bIsNew);
	}

	my $html = ${$refTemplateTable};
	$html = ReplaceLine($html, '@TITLE@', 'Hits per ' . $strTitle);
	$html = ReplaceLine($html, '@HEADER_NAME@', $strTitle);
	$html = ReplaceLine($html, '@HEADER_VALUE@', 'Hits');
	$html = ReplaceLine($html, '@HEADER_VALUE_NEW@', 'New');
	$html = ReplaceLine($html, '@FOOTER_VALUE@', $gTotalHits);
	$html = ReplaceLine($html, '@FOOTER_VALUE_NEW@', $gTotalHitsNew);
	$html = ReplaceLine($html, '@STAT_DATA@', $data);
	${$refTable} .= $html;
}

###################
# Hilfsfunktionen #
###################

sub GetLogfiles
{
	my $ArrayRef = shift;
	
	push @{$ArrayRef}, $gFileNameLogDb;
	if(opendir(DIR, '.'))
	{
		for my $item (sort { $b cmp $a } readdir(DIR))
		{
			next if ($item eq $gFileNameLogDb);
			push @{$ArrayRef}, $item if ($item =~ /$gFileNameLogDbPattern/);
		}
	}
}

sub ReplaceLines
{
	my $refLines = shift;
	my $strSearch = shift;
	my $strReplace = shift;

	my @ReturnLines = @{$refLines};

	for my $Zeile (@ReturnLines)
	{
		$Zeile =~ s/$strSearch/$strReplace/g;
	}
	
	@ReturnLines;
}

sub ReplaceLine
{
	my $strZeile = shift;
	my $strSearch = shift;
	my $strReplace = shift;

	my $ReturnZeile = $strZeile;

	$ReturnZeile =~ s/$strSearch/$strReplace/g;
	
	$ReturnZeile;
}

sub ExtractAndReplaceLines
{
	my $refLines = shift;
	my $strStart = shift;
	my $strEnd = shift;
	my $strReplace = shift;

	my @LinesNew;
	my $strReturn;

	my $bFound = 0;
	my $bOccurrences = 0;

	for my $Zeile (@{$refLines})
	{
		if($bFound)
		{
			if($Zeile =~ /$strEnd/)
			{
				$bFound = 0;
			}
			else
			{
				$strReturn .= $Zeile if($bOccurrences == 1);
			}
		}
		else
		{
			if($Zeile =~ /$strStart/)
			{
				$bFound = 1;
				$bOccurrences++;
				push @LinesNew, $strReplace;
			}
			else
			{
				push @LinesNew, $Zeile;
			}
		}
	}

	@{$refLines} = @LinesNew;

	$strReturn;
}

sub TextToHtml
{
	my $text = shift;

	$text =~ s/\&/&amp;/g;
	$text =~ s/\"/&quot;/g;
	$text =~ s/\</&lt;/g;
	$text =~ s/\>/&gt;/g;

	$text =~ s/\r\n/<br>/g;
	$text =~ s/\r/<br>/g;
	$text =~ s/\n/<br>/g;

	$text =~ s/\|/&#124;/g;		# Feldtrenner für DB-Dateien

	$text;
}

sub FormatDateTime
{
	my $nSeconds = shift;

	my ($nSecond, $nMinute, $nHour, $nDay, $nMonth, $nYear) = (localtime($nSeconds))[0..5];

	my $strDate = ($gLanguage eq 'de')
	?	sprintf("%02d.%02d.%04d", $nDay, $nMonth + 1, $nYear + 1900)
	:	sprintf("%02d/%02d/%04d", $nMonth + 1, $nDay, $nYear + 1900);

	my $strTime = sprintf("%02d:%02d:%02d", $nHour, $nMinute, $nSecond);

	"$strDate&nbsp;$strTime";
}

sub FormatDateTime_YearToSecond
{
	my $nSeconds = shift;

	my ($nSecond, $nMinute, $nHour, $nDay, $nMonth, $nYear) = (localtime($nSeconds))[0..5];

	my $strDate = sprintf("%04d%02d%02d-%02d%02d%02d", $nYear + 1900, $nMonth + 1, $nDay, $nHour, $nMinute, $nSecond);

	$strDate;
}

sub FormatDateTime_WeekdayDate
{
	my $date = shift;

	my $nSeconds = Time::Piece->strptime($date, '%Y-%m-%d');
	my ($nSecond, $nMinute, $nHour, $nDay, $nMonth, $nYear, $nWeekday) = (localtime($nSeconds))[0..6];

	my $strDate = ($gLanguage eq 'de')
	?	sprintf("%s, %02d.%02d.%04d", ('So','Mo','Di','Mi','Do','Fr','Sa')[$nWeekday], $nDay, $nMonth + 1, $nYear + 1900)
	:	sprintf("%s, %02d.%02d.%04d", ('Sun','Mon','Tue','Wed','Thu','Fri','Sat')[$nWeekday], $nDay, $nMonth + 1, $nYear + 1900);

	$strDate;
}

sub FormatDateTime_YearToDay
{
	my $nSeconds = shift;

	my ($nSecond, $nMinute, $nHour, $nDay, $nMonth, $nYear) = (localtime($nSeconds))[0..5];

	my $strDate = sprintf("%04d-%02d-%02d", $nYear + 1900, $nMonth + 1, $nDay);

	$strDate;
}

sub FormatTimeSpan
{
	my $nSeconds = shift;

	my $nDays = int($nSeconds / 86400); $nSeconds -= $nDays * 86400;
	my $nHours = int($nSeconds / 3600); $nSeconds -= $nHours * 3600;
	my $nMinutes = int($nSeconds / 60); $nSeconds -= $nMinutes * 60;

	my $span = '';
	$span .= sprintf("%d/", $nDays) if ($nDays gt 0);
	$span .= sprintf("%02d:%02d:%02d", $nHours, $nMinutes, $nSeconds);
	$span;
}

sub DaysBetweenDates
{
	my $date1 = shift;
	my $date2 = shift;

	(Time::Piece->strptime($date2, '%Y-%m-%d') - Time::Piece->strptime($date1, '%Y-%m-%d')) / 86400;
}

sub AddDaysToDate
{
	my $date = shift;
	my $days = shift;
	
	FormatDateTime_YearToDay(Time::Piece->strptime($date, '%Y-%m-%d') + $days * 86400);
}

sub IsBot
{
	my $bResult = 0;

	for my $ua (@_)
	{
		for my $botString (@gBotStrings)
		{
			if($ua =~ m/$botString/i)
			{
				$bResult = 1;
				last;
			}
		}
	}
	$bResult;
}

########
# Ende #
########
