package TDS::AccessLog::Analysis;
# $Id: Analysis.pm,v 1.72.4.1 2001/03/01 16:39:49 tom Exp $
################################################################

=head1 NAME

TDS::AccessLog::Analysis

=head1 SYNOPSIS

 use TDS::AccessLog::Analysis;

 $a = new TDS::AccessLog::Analysis;
 $a->Read;
 $a->ReadLogFile;
 print $a->AsHTML;

=head1 DESCRIPTION

if year, month are not specified,
use current one.

=cut

################################################################

use strict;
use vars qw(@ISA @EXPORT $MaxRecent $SkeltonFile $RecentHour);
use Exporter;
use FileHandle;

use ObjectTemplate;
use DateTime::Date;
use DateTime::Time;
use DateTime::Format;
use CGI::QueryString;
use CGI::Tools;
use SimpleDB::Hash;
use SearchEngine;
use Template;
use Url;

use TDS::System;
use TDS::Skelton;
use TDS::DirInfo;
use TDS::AccessLog::Item;
use TDS::AccessLog::DirInfo;
use TDS::ID_Map;
use TDS::Admin::Skelton;
use TDS::Admin::Authorize;
use TDS::IdentInfo;
use TDS::RequestMode;

@ISA = qw(TDS::Admin::Skelton TDS::Admin::Authorize TDS::AccessLog::DirInfo);
@EXPORT = qw(attributes);

=head1 STATIC VARIABLES

 $SkeltonFile    
 $RecentHour     hour which regard as recent
 $MaxRecent      number of recent showing

=cut

$SkeltonFile = "log_skelton.html";
$RecentHour = 12;
$MaxRecent = 20;

=head1 MEMBER VARIABLES

 year, month   
 part          

=cut

attributes qw(year month tail_days part
	      chase_id
	      total last_day
	      day_trans weeks hours recent_hours
	      recent_referer today_referer newer_referer referers
	      accounted_referer referer_file
	      keywords search_engines
	      remote_hosts uris ym
	      recent_visitor luckey_visitor recent_visitor_start_num
	      visitors visitors_total
	      recent_mapped_visitor
	      huas huas_name
	      antenna
	      map);

# part is unused.

################################################################
sub initialize($)
{
    my $self = shift;

    if (1){
	$self->filename($SkeltonFile) unless $self->filename;
    } else {
	# check skelton file on styledir
	my $style_skelton = GetStyleDir() . "/log_skelton.html";
	unless ($self->filename){
	    if (-f $style_skelton){
		$self->filename($style_skelton);
	    } else {
		$self->filename($SkeltonFile);
	    }
	}
    }

    $self->map(new TDS::ID_Map);
    
    # unless year, month specified, use current time
    if (!$self->year && !$self->month){
	$self->year($TDS::Status->start_time->year);
	$self->month($TDS::Status->start_time->month);
    }
    $self->day_trans([]);
    $self->weeks([]);
    $self->hours([]);
    $self->recent_hours([]);

    $self->remote_hosts({});
    $self->uris({});
    $self->ym({});
    
    $self->newer_referer({});
    $self->recent_referer({});
    $self->today_referer({});
    $self->referers({});
    $self->SetRefererFile();
    my %h;
    tie %h, 'SimpleDB::Hash', $self->referer_file;
    $self->accounted_referer(\%h);
#	untie %{$self->accounted_referer};

    $self->keywords({});
    $self->search_engines({});
    
    $self->recent_visitor([]);
    $self->recent_mapped_visitor([]);
    $self->luckey_visitor([]);
    $self->visitors({});
    $self->visitors_total({});
    
    $self->huas({});
    $self->huas_name({});

    # antenna
    $self->antenna({});
    {
	require TDS::CapturedAntenna;
	my $ca = new TDS::CapturedAntenna;
	$ca->Read;
	for (@{$ca->content}){
	    $self->antenna->{$_->Value('url')}->{name} = $_->Value('name');
	}
    }
    # cal super class
    $self->TDS::Skelton::initialize;
    $self->TDS::Admin::Authorize::initialize;
    $self->TDS::AccessLog::DirInfo::initialize;
}


=head2 $a->ReadLogFile

read logfile

=cut

sub ReadLogFile($;$)
{
    my ($self, $tail_days) = @_;

#    warn times, ", Read Start";
    my $start = (times)[0];
    
    # get current date, time
    my $now_date = new DateTime::Date;
    $now_date->SetTime(time(), $TDS::System::TZ);
    my $now_time = new DateTime::Time;
    $now_time->SetTime(time(), $TDS::System::TZ);
    my $now_time_tm = $now_time->GetTime($TDS::System::TZ);
    my $now_hour = $now_time->hour;

    $self->tail_days($tail_days);
    
    # last analysis time
    my $last_time = new DateTime::Time;
    $last_time->SetTime((stat($self->referer_file))[9], $TDS::System::TZ);
    my $last_time_tm = $last_time->GetTime($TDS::System::TZ);

    # do read
    my $filename = $self->GetLogFilename($self->year, $self->month);
    open(F, $filename) || die "can't open logfile: $filename";
    my $total = 0;
#    my $today = 0;
    
    my $item = new TDS::AccessLog::Item;
    
    my $limit_dt = $TDS::Status->start_time->Dup;
    $limit_dt->Decrement("1D");
    unless ($limit_dt->min == 0 && $limit_dt->sec == 0){
	$limit_dt->Increment("1h");
	$limit_dt->min(0), $limit_dt->sec(0);
    }
    my $limit_dt_tm = $limit_dt->GetTime($TDS::System::TZ);
    
    my $se = new SearchEngine;
    my $recent_hour_sec = $RecentHour*3600;
    
    my $tail_date = $now_time->Dup;
    my $tail_date_str;
    if ($tail_days){
	my $tmp = $tail_days - 1;
	$tail_date->Decrement("${tmp}D")
	    if $tmp;
	$tail_date_str = Expand("%y/%0m/%0d", $tail_date->GetParams);
    }
    my $tail_date_tm = $tail_date->GetTime($TDS::System::TZ);
    my $tail_cmp_date = new DateTime::Date;

    while (<F>){
	next if /^$/;
	chomp;
	
	my $line = $_;
 	if ($tail_days){
	    if (1){
		$line =~ m!^[^\t]*\t[^\t]*\t(\d\d\d\d/\d\d/\d\d)!;
		next if $1 lt $tail_date_str;
	    } else {
		my ($y, $m, $d) = TDS::AccessLog::Item::GetYmdByLine($line);
		$tail_cmp_date->Set($y, $m, $d);
		my $tm = $tail_cmp_date->GetTime($TDS::System::TZ);
		next if $tm < $tail_date_tm;
	    }
 	}
	$item->Set($line) || next;   # skip if illegal data
	
	# ID
	if ($self->chase_id &&                # if ID chase mode
	    $item->id ne $self->chase_id){    # get only the ID's data
	    next;
	}
	# date
	my $item_tm = $item->time->GetTime($TDS::System::TZ);
	{
	    my $hour = $item->time->hour;
	    $self->day_trans->[$item->time->day]++;
	    $self->hours->[$hour]++;
	    $self->weeks->[$item->time->week]++;
	    
	    if ($limit_dt_tm <= $item_tm){
		my $h = $now_hour - $hour;
		if ($h < 0){
		    $h += 24;
		}
		$self->recent_hours->[$h]++;
	    }
	}
	# referer
	my $referer = $item->referer->uri;
	if ($referer){
	    $self->recent_referer->{$referer}++
		if $now_time_tm - $item_tm < $recent_hour_sec;
	    $self->today_referer->{$referer}++
		if $item_tm == $now_time_tm;
	    # new referer(not accounted yet)
	    if ($referer &&
		$item_tm > $last_time_tm){
#		$self->newer_referer->{$referer}++;
		unless ($self->accounted_referer->{$referer}){
		    $self->newer_referer->{$referer}++;
		}
	    }
	    $self->referers->{$item->referer->QueryOmitted}++;
	}
	# remote host
	$self->remote_hosts->{$item->remote_host_short}++;
	# URI and objective files
	{
	    my $uri = $item->uri;
	    my $u = $uri->uri;
	    my $query = $uri->Query;
	    $query =~ s/TsDiary.cgi//;
	    if ($query =~ /^\d+$/){
		if ($query lt "19000000"){
		    $u = $uri->QueryOmitted;
		}
	    }
	    $self->uris->{$u}++;

	}
	# visitor
	{
	    my $id = $item->id;
	    my $it = {id=>$id,
		      counter=>$item->counter,
		      times=>$item->times,
		      tm=>$item_tm,
		      uri=>$item->uri->uri,
		      referer=>$item->referer->uri};
		      
	    $self->recent_visitor->[$total%$MaxRecent] = $it;
	    $self->visitors->{$id}++;
	    $self->visitors_total->{$id} = $item->times;
	    # mapped visitor
	    if ($self->map->Map($id)){
		my $num = @{$self->recent_mapped_visitor};
		if ($num >= $MaxRecent){
		    shift(@{$self->recent_mapped_visitor});
		}
		push(@{$self->recent_mapped_visitor}, $it);
	    }
	    # luckey visitor
	    if ($_ = $item->counter){
		if (is_luckey_number($_)){
		    push(@{$self->luckey_visitor}, $it);
		}
	    }
	}
	# search engine
	if ( $item->referer->query &&
	     $item->referer->query =~ /=/){
	    my @keywords = $se->GetKeyword($item->referer->uri,
					   $item->referer->query);
	    for (@keywords){
		$self->keywords->{$_}->{times}++;
		$self->keywords->{$_}->{match_names}->{$se->match_name}++;
	    }
	    $self->search_engines->{$se->match_name}++;
	}
	# HUA
	{
	    $self->huas->{$item->hua}++;
	    my $name_version = $item->hua;
	    my ($name) = $name_version =~ m!^([^/]*)!;
	    $self->huas_name->{$name}++;
	}

	$total++;
    }
    close (F);
    
#    warn times;
    my $end = (times)[0];
    my $orig = 4;
    my $elasp = $end-$start;
#    warn "elasp: ", $elasp, ", ", ($orig-$elasp)/$orig*100, "%";
    # account new referer
    for (keys %{$self->newer_referer}){
	$self->accounted_referer->{$_} += $self->newer_referer->{$_};
    }
    $self->recent_visitor_start_num($total%$MaxRecent);
    $self->total($total || 1);
    $self->last_day(($item)?$item->time->day:1);

#    warn times;
    # refresh count files
#    my $counter = new TDS::AccessLog::Counter;
#    $counter->Set($total, $today);
}


sub SetMacros($)
{
    my $self = shift;
    ################
    # Date topic
    
    # translating of date
    $self->SetMacro("(DAY|DAILY)_TRANSITION", sub {
	my $self = shift;
	my @html;
	my ($year, $month) = ($self->year, $self->month);
	my $days = $self->tail_days || $self->last_day || 1;
	push(@html, sprintf("<p>Total: %d(%6.2f)</p>",
			    $self->total,
			    $self->total/$days));
	push(@html, "<table border=1>");
	
	for (reverse(1..31)){
	    next unless $self->day_trans->[$_];
	    push(@html, sprintf(qq(<tr><td>%04d/%02d/%02d</td><td>%d</td><td>%s</tr>),
				$year, $month, $_,
				$self->day_trans->[$_],
				bar($self->day_trans->[$_])));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    # by hour
    $self->SetMacro("HOUR", sub {
	my $self = shift;

	my @html = ("<table border=1>");
	for (0..23){
	    push(@html, sprintf(qq(<tr><td>%s</td><td>%d</td><td>%s</tr>),
				$_,
				$self->hours->[$_],
				bar($self->hours->[$_])));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });

    # by week
    $self->SetMacro("WEEK", sub {
	my $self = shift;

	my @html = ("<table border=1>");
	for (0..6){
	    push(@html, sprintf(qq(<tr><td>%s</td><td>%d<td>%s</tr>),
				&DateTime::Date::GetWeekString('ABBR', $_),
				$self->weeks->[$_],
				bar($self->weeks->[$_])));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    $self->SetMacro("RECENT_HOURS", sub {
	my $self = shift;

	my $time = new DateTime::Time;
	$time->SetTime(time(), $TDS::System::TZ);

	my $update_log;
	require TDS::AccessLog::Logging;
	if ($TDS::AccessLog::Logging::UpdateLogFlag){
	    require TDS::AccessLog::AnalysisUpdate;
	    $update_log = new TDS::AccessLog::AnalysisUpdate(limit=>"24h");
	    $update_log->ReadLogFile;
	}
	my @html;
	my $template = qq(<tr><td>%timestr</td><td>%times</td><td>%times_bar</td></tr>);
	if ($update_log){
	    $template = qq(<tr><td>%timestr</td><td>%times</td><td>%updated</td><td>%times_bar</td></tr>);
	}
	for (0..23){
#	    print ", $_: ", $update_log->hours->[$_];
	    my $timestr = time2str("%Y/%m/%d %H", $time->GetTime($TDS::System::TZ));
	    my $params = {"timestr"=>$timestr,
			  "times"=>$self->recent_hours->[$_],
			  "times_bar"=>bar($self->recent_hours->[$_])};
	    if ($update_log){
		$params->{'updated'} =
		    ($update_log->hours->[$time->hour]) ? 'U' : '';
	    }
	    unshift(@html, Expand($template, $params));
	    $time -= "1h";
	}
	return join("\n", "<table border=1>", @html, "</table>");
    });

    ################
    # Remote Host
    $self->SetMacro("REMOTE_HOST", sub {
	my ($self, $cmd, $var) = @_;

	my $cnt;
	my $max = ($var) ? $var : 999999;
	my @html = ("<table border=1>");
	for (sort {$self->remote_hosts->{$b} <=> $self->remote_hosts->{$a}}
	     keys %{$self->remote_hosts}){
	    my ($host, $times) = (Escape($_), $self->remote_hosts->{$_});
	    last if $times == 1;
	    push(@html, qq(<tr><td>$times</td><td>$host</td></tr>\n));
	    $cnt++;
	    last if $cnt >= $max;
	}
	push(@html, "</table>");
	return join("\n", @html);
    });

    ################
    # URI
    $self->SetMacro("URI", sub {
	my $self = shift;

	my $cnt;
	my @html = ("<table border=1>");
	for (sort {$self->uris->{$b} <=> $self->uris->{$a}}
	     keys %{$self->uris}){
	    my ($uri, $times) = (Escape($_), $self->uris->{$_});
#	    last if $times == 1;    # Æó²ó°Ê¾å
	    push(@html, sprintf(qq(<tr><td>$times</td><td>%5.2f</tr><td>$uri</td>\n),
				($times/$self->total)*100));
	    $cnt++;
	    last if $cnt >= 20;
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    ################
    # referer
    # recent referer
    $self->SetMacro("RECENT_REFERER", sub {
	my ($self, $cmd, $var) = @_;
	my @html;
	push(@html, "<table border=1>");

	for (sort {$self->recent_referer->{$b} <=> $self->recent_referer->{$a}}
	     keys %{$self->recent_referer}){
	    my ($referer_uri, $times) = (Escape($_),
					 $self->recent_referer->{$_});
	    my $decoded_referer = $self->GetDecodedUri($referer_uri);
	    push(@html, qq(<tr><td>$times</td><td><a href="$referer_uri">$decoded_referer</a></td></tr>\n));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    
    # today's referer
    $self->SetMacro("TODAY_REFERER", sub {
	my $self = shift;
	my @html;
	push(@html, "<table border=1>");

	for (sort {$self->today_referer->{$b} <=> $self->today_referer->{$a}}
	     keys %{$self->today_referer}){
	    my ($referer_uri, $times) = (Escape($_),
					 $self->today_referer->{$_});
	    my $decoded_referer = $self->GetDecodedUri($referer_uri);
	    push(@html, qq(<tr><td>$times</td><td><a href="$referer_uri">$decoded_referer</a></td></tr>\n));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    # referer newer than last analysis
    $self->SetMacro("NEWER_REFERER", sub {
	my $self = shift;
	my @html;
	push(@html, "<table border=1>");

	for (sort {$self->newer_referer->{$b} <=> $self->newer_referer->{$a}}
	     keys %{$self->newer_referer}){
	    my ($referer_uri, $times) = (Escape($_),
					 $self->newer_referer->{$_});
	    my $decoded_referer = $self->GetDecodedUri($referer_uri);
	    push(@html, qq(<tr><td>$times</td><td><a href="$referer_uri">$decoded_referer</a></td></tr>\n));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    
    # all referer
    $self->SetMacro("REFERER", sub {
	my ($self, $cmd, $var) = @_;

	my $cnt;
	my $max = ($var) ? $var : 999999;
	my @html = ("<table border=1>");
	for (sort {$self->referers->{$b} <=> $self->referers->{$a}}
	     keys %{$self->referers}){
	    my ($referer, $times) = (Escape($_), $self->referers->{$_});
	    last if $times == 1;    # at least twice
	    my $decoded_referer = $self->GetDecodedUri($referer);
	    push(@html, qq(<tr><td>$times</td><td><a href="$referer">$decoded_referer</a></td></tr>\n));
	    $cnt++;
	    last if $cnt >= $max;
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    # search engine
    $self->SetMacro("SEARCH_ENGINE_KEYWORD", sub {
	my $self = shift;
	my @html = ("<table border=1>");
	for (sort {$self->keywords->{$b}->{times}
				<=> $self->keywords->{$a}->{times}}
	     keys %{$self->keywords}){
	    my $raw_keyword = $_;
	    my ($keyword, $times) = (Escape($raw_keyword),
				     $self->keywords->{$raw_keyword}->{times});
	    last if $times == 0;

	    my @links;
	    for (sort { $self->keywords->{$raw_keyword}->{match_names}->{$b} <=>
			    $self->keywords->{$raw_keyword}->{match_names}->{$a} }
		 keys %{$self->keywords->{$raw_keyword}->{match_names}}){
		push(@links, sprintf("%s(%s), ",
				     &SearchEngine::CreateLink($_, $raw_keyword, $_),
				     $self->keywords->{$raw_keyword}->{match_names}->{$_}));
		
	    }
	    my $links = join(', ', @links);
	    push(@html, qq(<tr><td>$times</td><td>$keyword</td>
			   <td>$links</tr>\n));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    $self->SetMacro("SEARCH_ENGINES", sub {
	my $self = shift;

	my $html = qq(<table border="1">);
	for (sort { $self->search_engines->{$b} <=> $self->search_engines->{$a}} keys %{$self->search_engines}){
	    $html .= sprintf("<tr><td>%s<td>%s",
			     $_, $self->search_engines->{$_});
	}
	$html .= "</table>";
	return $html;
    });
	
    ################
    # Visitor
    # recent visitor
    $self->SetMacro("RECENT_VISITOR", sub {
	my $self = shift;
	my @recent;
	for (reverse(0..$self->recent_visitor_start_num-1)){
	    push(@recent, $self->recent_visitor->[$_]);
	}

	for (reverse($self->recent_visitor_start_num .. $MaxRecent-1)){
	    push(@recent, $self->recent_visitor->[$_]);
	}
	my @html = ("<table border=1>");
	push(@html, "<tr><th>Cnt<th>Date<th>ID<th>Times<th>URI<th>Referer</tr>");
	my $TZ = $TDS::System::TZ;
	require TDS::IdentInfo;
	my $path = TDS::IdentInfo->Get('url');
	$path =~ s!^http://[^/]+!!;
	$path =~ s!/?[^/]+$!!;

	for (@recent){
	    next unless ref $_;
	    my $visitor = $_;
	    my $name = $self->map->Map($visitor->{id}) || $visitor->{id};
	    if (0){
		# date, cnt, times, uri, id, refere
		my $params;
		$params->{'time'} = time2str("%Y/%m/%d %X",
					     $visitor->{tm}+$TZ, $TZ);
		$params->{'counter'} = $visitor->{counter};
		$params->{'times'} = $visitor->{times};
		$params->{'chase'} = $self->ChaseLink($visitor->{id}, $name);
		$params->{'id'} = $self->map->Map($visitor->{id}) || $visitor->{id};				
		$params->{'uri'} = Escape($visitor->{uri});
		my $uri = $visitor->{uri};
		$uri =~ s/^$path//;
		$uri ||= "./";
		$params->{'decoded_uri'} = Escape($self->GetDecodedUri($uri));
		$params->{'referer'} = Escape($visitor->{referer});
		$params->{'decoded_referer'} = Escape($self->GetDecodedUri($visitor->{referer}));
		my $template = qq(<tr><td>%time<td>%counter<td>%times<td><a href="%uri">%decoded_uri</a><td>%chase</a><td><a href="%referer">%decoded_referer</a>\n);
		push(@html, Expand($template, $params));
	
		
	    } else {
		push(@html, sprintf(qq(<tr><td>%d<td>%s<td>%s<td>%d<td><a href="%s">%s</a><td><a href="%s">%s</a></tr>),
				$visitor->{counter},
				time2str("%Y/%m/%d %X",
					 $visitor->{tm}+$TZ, $TZ),
				$self->ChaseLink($visitor->{id}, $name),
				$visitor->{times},
				Escape($visitor->{uri}),
				Escape($self->GetDecodedUri($visitor->{uri})),
				Escape($visitor->{referer}),
				Escape($self->GetDecodedUri($visitor->{referer}))));
	    }
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    $self->SetMacro("RECENT_MAPPED_VISITOR", sub {
	my $self = shift;

	my @html = ("<table border=1>");
	push(@html, "<tr><th>Cnt<th>Time<th>ID<th>times<th>URI<th>Referer</tr>");
	my $TZ = $TDS::System::TZ;
	for (reverse @{$self->recent_mapped_visitor}){
	    next unless ref $_;
	    my $visitor = $_;
	    my $name = $self->map->Map($visitor->{id}) || $visitor->{id};
	    push(@html, sprintf(qq(<tr><td>%d<td>%s<td>%s<td>%d<td><a href="%s">%s</a><td><a href="%s">%s</a></tr>),
				$visitor->{counter},
				time2str("%Y/%m/%d %X",
					 $visitor->{tm}+$TZ, $TZ),
				$self->ChaseLink($visitor->{id}, $name),
				$visitor->{times},
				Escape($visitor->{uri}),
				Escape($self->GetDecodedUri($visitor->{uri})),
				Escape($visitor->{referer}),
				Escape($self->GetDecodedUri($visitor->{referer}))));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
	
    $self->SetMacro("LUCKEY_VISITOR", sub {
	my $self = shift;

	my @html = ("<table border=1>");
	push(@html, "<tr><th>Cnt<th>Time<th>ID<th>times<th>URI<th>Referer</tr>");
	my $TZ = $TDS::System::TZ;
	for (@{$self->luckey_visitor}){
	    next unless ref $_;
	    my $visitor = $_;
	    my $name = $self->map->Map($visitor->{id}) || $visitor->{id};
	    push(@html, sprintf(qq(<tr><td>%d<td>%s<td>%s<td>%d<td><a href="%s">%s</a><td><a href="%s">%s</a></tr>),
				$visitor->{counter},
				time2str("%Y/%m/%d %X",
					 $visitor->{tm}+$TZ, $TZ),
				$self->ChaseLink($visitor->{id}, $name),
				$visitor->{times},
				Escape($visitor->{uri}),
				Escape($self->GetDecodedUri($visitor->{uri})),
				Escape($visitor->{referer}),
				Escape($self->GetDecodedUri($visitor->{referer}))));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });

    # visitor ranking
    $self->SetMacro("VISITOR", sub {
	my $self = shift;
	my $cnt;
	my @html = ("<table border=1>");
	push(@html, "<tr><th>Time<th>ID</tr>");
	for (sort {$self->visitors->{$b} <=> $self->visitors->{$a}}
	     keys %{$self->visitors}){
	    push(@html, sprintf(qq(<tr><td>%d<td>%s</tr>),
				$self->visitors->{$_},
				$self->ChaseLink($_, $self->map->Map($_) || $_)));
	    last if (++$cnt >= 20);
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    $self->SetMacro("VISITOR_TOTAL", sub {
	my $self = shift;
	my $cnt = 0;
	my @html = ("<table border=1>");
	push(@html, "<tr><th>Time<th>ID</tr>");
	for (sort {$self->visitors_total->{$b} <=> $self->visitors_total->{$a}}
	     keys %{$self->visitors_total}){
	    last if $self->visitors_total->{$_} < 2;
	    next if (++$cnt > 20);
	    push(@html, sprintf(qq(<tr><td>%d<td>%s</tr>),
				$self->visitors_total->{$_},
				$self->ChaseLink($_, $self->map->Map($_) || $_)));
	}
	push(@html, "</table>");
	push(@html, "<p>people visited over twice : $cnt</p>");
	return join("\n", @html);
    });

    ################
    # comment
    $self->SetMacro("COMMENT", sub {
	my $self = shift;

	require TDS::Comment;
	my $com = new TDS::Comment;
	$com->Read;
	my $template =
	    qq(<tr><td>%date</td><td>%chase</td><td>%comment</td></tr>\n);
#	return $com->AsHTML(-1);
	my $item;
	my $html;
	for $item (@{$com->content}){
	    $item->{chase} = $self->ChaseLink($item->{id}, $item->{name});
	    $html .= Expand($template, $item);
	}
	return qq(<table border="1">$html</table>\n);
    });
    ################
    # ID Map
    $self->SetMacro("ID_MAP", sub {
	my @html = (q(<table border=1>), q(<tr><th>name<th>ID));
	for (sort { $self->map->Map($a) cmp $self->map->Map($b) }
	     $self->map->All_ID){
	    push(@html, sprintf(qq(<tr><td>%s<td>%s),
				$self->map->Map($_),
				$self->ChaseLink($_, $_)));
	}
	push(@html, qq(</table>));
	return join("", @html);
    });
	    
    ################
    # HTTP User Agent
    # HUA
    $self->SetMacro("HUA\$", sub {
	my $self = shift;

	my @html = ("<table border=1>");
	for (sort { $self->huas->{$b} <=> $self->huas->{$a} }
	     keys %{$self->huas}){
	    push(@html, sprintf(qq(<tr><td>%s<td>%d<td>%3.1f<td>%s</tr>),
				$_,
				$self->huas->{$_},
				$self->huas->{$_}/$self->total*100,
				bar($self->huas->{$_})));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
	
    # HUA_NAME
    $self->SetMacro("HUA_NAME", sub {
	my $self = shift;

	my @html = ("<table border=1>");
	for (sort { $self->huas_name->{$b} <=> $self->huas_name->{$a} }
	     keys %{$self->huas_name}){
	    push(@html, sprintf(qq(<tr><td>%s<td>%d<td>%3.1f<td>%s</tr>),
				$_,
				$self->huas_name->{$_},
				$self->huas_name->{$_}/$self->total*100,
				bar($self->huas_name->{$_})));
	}
	push(@html, "</table>");
	return join("\n", @html);
    });
    $self->SUPER::SetMacros;
}
sub AsHTML($)
{
    my $self = shift;

    return $self->SUPER::AsHTML;
}
################################################################
sub ChaseLink($$$)
{
    my ($self, $id, $content) = @_;

    return sprintf(qq(<a href="log.cgi?year=%d&amp;month=%d&amp;id=%s">%s</a>),
		   $self->year, $self->month,
		   UrlEncode($id), $content);
}
sub SetRefererFile($)
{
    my $self = shift;
    
    $self->referer_file(GetLogDir() . "/referer.dat");
}
sub GetDecodedUri($$)
{
    my ($self, $uri) = @_;

    my $decoded;
#    if ($_ = $self->antenna->{$uri}){
    my $antenna;
    for (keys %{$self->antenna}){
	if ($uri =~ /$_/i){
	    $antenna = $self->antenna->{$_};
	    last;
	}
    }
    if ($antenna){
	$decoded = $antenna->{'name'};
    } else {
	my $url = new Url(uri=>$uri);
	$decoded = $url->QueryDecoded;
    }
    return $decoded;
}
################################################################
sub bar ($){
    my $cnt = shift;
    my ($rtn, $i);

    my $j = int($cnt/100);
    for (1..$j){
	$rtn .= bar_img(100);
    }
    $j = int(($cnt%100)/10);
    for (1..$j){
	$rtn .= bar_img(10);
    }
    $j = $cnt%10;
    for (1..$j){
	$rtn .= bar_img(1);
    }
    $rtn;
}
sub bar_img($)
{
    my $cnt = shift;
    my $alt;

    for (1..$cnt){ $alt .= "*"; }
#    my $url = TDS::IdentInfo->Get('url');
#    return qq(<img src="${url}image/bar$cnt.jpg" alt="$alt">);
    return qq(<img src="../image/bar$cnt.jpg" alt="$alt">);    
}
sub is_luckey_number($)
{

    my $d = shift;
    my @a = split("", $d);

    return 0 if $d < 100;    # should be greater than 99
    return 0 if $d == 0;
    
    my $zero = 1;
    my $zoro = 1;
    my $cont = 1;

    $zero = $d =~ /^\d\d?00+$/;         # n0000, nn000, ...
    if ($zero){
	return 'ZERO';
    } else {
	$zoro = $d =~ /^(\d)\1+$/;      # nnnnnn
    }
    if ($zoro){
	return 'ZORO';
    } else {
	# continuas sequence
	for (1..$#a){                   # 12345, 4567
	    if ($a[$_] -1 != $a[$_-1]){
		$cont = 0;
		last;
	    }
	}
	for (0..$#a-1){                   # 98765..
	    if ($a[$_] -1 != $a[$_+1]){
		$cont = 0;
		last;
	    }
	}
    }
    if ($cont){
	return 'CONT';
    } else {
	return 0;
    }
}
1;
