package TDS::Collection;
# $Id: Collection.pm,v 1.76 2000/12/20 14:59:33 tom Exp $
################################################################

=head1 NAME

TDS::Collection - Tdf Set

=head1 SYNOPSIS

 use TDS::Collection;
 use TDS::Status;

 $status = new TDS::Status;
 $c = new TDS::Collection
 $c->Read;
 print $c->AsHTML({});

=head1 DESCRIPTION

リクエストにより読み込むべきファイルを集め、それらを読み込み、パースする
自動置換もここで行う。


結構複雑です。
$status->mode, $status->year,month,day,part などで要求された
日記を取り出し、解析します。

 読み込み：    
 Read(;$lm):
  collect_files():
   status->mode に従い pickup_*
     pickup_files($y,$m,$d,$min,$max):
     pickup_recent($y,$m,$d,$num)
     pickup_neighbour($y,$m,$d,$p,$n)
       引数で与えられた日記情報集合を返す [$path,$y,$m,$d,$lm]
   pickup_*() した情報集合を返す
  情報集合を元にファイルを読み込み、解析する
   read_files(@files)
    @files の各ファイルを読み込み、解析する
      parse_file($filename, $parser);
	$filename を読み、parse
  読み込んだファイル数を返す

 表示：
 AsHTML($params)
   すべての日記について AsHTML
   自動置換

=cut

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

use strict;

use JConv;
use ObjectTemplate;
use DateTime::Date;
use DateTime::Format;
use CGI::QueryString;

use TDS;
use TDS::System;
use TDS::DirInfo;

#BEGIN { warn times, " Collection start"; }

use vars qw(@ISA $AUTOLOAD
	    $RecentNum $NeighbourNum $AutoReplace
	    $ReverseInRecent $AllowAllMonthly
	    $TdfJcode);
@ISA = qw(ObjectTemplate);

=head1 STATIC VARIABLES

 $RecentNum       ダイジェスト表示数
 $NeighbourNum    近傍表示数
 $AutoReplace     自動置換をするか
 $TdfJcode        日記ソースの漢字コード
 $AllowAllMonthly   

=cut

$RecentNum = 5 unless defined $RecentNum;
$NeighbourNum = 0 unless defined $NeighbourNum;
$AutoReplace = 1 unless defined $AutoReplace;
$TdfJcode = '' unless defined $TdfJcode;
$ReverseInRecent = 0 unless defined $ReverseInRecent;
$AllowAllMonthly = 1 unless defined $AllowAllMonthly;

=head1 MEMBER VARIABLES

 diary          日記ソースオブジェクト配列（TDS::Tdf::Parser の集合）
 last_modified  指定日記の最遅最終更新日
 replacer_url   置換オブジェクト
 replacer_term
 replacer_dictionary

=cut

attributes qw(diarys last_modified
	      replacer_url replacer_term replacer_dictionary
	      files last_day);

################################################################
# auto loading

use AutoLoader;

sub AUTOLOAD {
    $AutoLoader::AUTOLOAD = $AUTOLOAD;
    goto &AutoLoader::AUTOLOAD;
}

package TDS::Collection;
1;

################################################################
__END__

=head1 MEMBER FUNCTIONS

=cut

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

    $self->ClearDiarys();
    $self->SUPER::initialize;
}

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

=head2 $c->Read($lm);

$status の指定通りに読むべき tdf を集め、それらのファイルを読む
読み込んだファイル数を返す。
読み込むべきファイルが一つも無ければ -1 を返す。

$lm として tm を渡すと、
それより新しいファイル

=cut

sub Read ($;$)
{
    my ($self, $lm) = @_;

    my @files = $self->collect_files;

    return -1 if @files == 0;
    unless ($lm){                # 更新時刻の指定なし
	return scalar($self->read_files(@files));
    } else {
#	print "set: $lm, tdf: " . $self->last_modified . "\n";
	if ($self->last_modified > $lm){ # 指定された更新日より新しい
	    return  scalar($self->read_files(@files));
	} else {                         # 新しく更新された日記が無い
	    return 0;
	}
    }
}
# 読み込んだ日記(diarys)をクリア
sub ClearDiarys ($)
{
    my $self = shift;

    $self->diarys([]);
    $self->files([]);
    $self->last_modified(0);
}
sub GetMonthDirs($;$)
{
    my ($self, $spec_year) = @_;

    my @dirs;

    my $nowym = sprintf("%04d%02d",
			$TDS::Status->start_time->year,
			$TDS::Status->start_time->month);
			
    my $diary_dir = GetDiaryDir();
    opendir(Y_DIR, $diary_dir) || die $diary_dir;
#    while ($_ = readdir(Y_DIR)){
    for (reverse sort readdir(Y_DIR)){
	next unless /^\d{4}$/ && -d "$diary_dir/$_";
	my $year = $_;
	next if ($spec_year && $year != $spec_year);
	opendir(M_DIR, "$diary_dir/$year") || die "$diary_dir/$year";
#	while ($_ = readdir(M_DIR)){
	for (reverse sort readdir(M_DIR)){
	    next unless /^\d{2}$/ && -d "$diary_dir/$year/$_";
	    next if $nowym lt "$year/$_";    # 未来はスキップ
	    push(@dirs, [$year, $_]);
	}
	closedir M_DIR;
    }
    closedir Y_DIR;

    return @dirs;
}
################################################################

=head2 $c->collect_files

読み込む tdf ファイルを集め、
読み込むべき @files を返す

一旦集めたら、$self->files にキャッシュとして格納
次回はそれをそのまま返す

=cut

sub collect_files ($;$)
{
    my ($self, $mode) = shift;

    # ファイルキャッシュを返す
    return @{$self->files} if @{$self->files};

    $mode ||= $TDS::Status->mode;
#    die $mode;
    my @files;
    if ($mode eq 'RECENT'){
	@files = 
	    $self->pickup_recent($TDS::Status->start_time->year, 
				 $TDS::Status->start_time->month,
				 $TDS::Status->start_time->day,
				 $RecentNum);
    } elsif ($mode eq 'N_YEAR'){
	for ($TDS::Status->start_year..
	     $TDS::Status->year){
	    my $year = $_;
	    push(@files,
		 $self->pickup_neighbour($year,
					 $TDS::Status->month,
					 $TDS::Status->day_part));

	}
    } elsif ($mode eq 'PSEUDO_RECENT'){
	@files = $self->pickup_recent($TDS::Status->year,
				      $TDS::Status->month,
				      $TDS::Status->day_part,
				      $RecentNum);
    } else {
	my $day_part = $TDS::Status->day_part;

	if ($day_part eq ''){           # 月別
	    @files = $self->pickup_files($TDS::Status->year,
					 $TDS::Status->month);
	} elsif ($day_part =~ /[abc]/){ # 旬別
	    my ($min_day, $max_day) =
		($day_part eq 'a') ? (1, 9) :
		    ($day_part eq 'b') ? (10, 19) :
			(20, 31);
	    @files = $self->pickup_files($TDS::Status->year,
					 $TDS::Status->month,
					 $min_day, $max_day);
	} else {                        # 日別
	    my $num = $NeighbourNum;
	    @files = $self->pickup_neighbour($TDS::Status->year,
					     $TDS::Status->month,
					     $TDS::Status->day_part,
					     $num, $num);
	}
    }
    if (@files){
	my $day = new DateTime::Date;
	$day->Set(@{$files[scalar(@files)-1]}[1,2,3]);
	$self->last_day($day);
#	die $day->Dump;
    }
    return @{$self->files} = @files;
}
################################################################

=head2 $c->pickup_files($year, $month, $min_day, $max_day);

$year/$month の $min_day から $max_day までの日記ソースを取り出す。
[パス名, 年, 月, 日, 更新日] のデータからなる配列を返す。

=cut

sub pickup_files($$$$$)
{
    my ($self, $year, $month, $min_day, $max_day) = @_;

    $month = sprintf("%02d", $month);

    #print STDERR caller;
    my $dir = GetDiaryDir() . "/$year/$month";
    opendir(DIR, $dir) || return ();
    my @files;
    for (sort readdir(DIR)){
#    for (($rvs)? reverse sort readdir(DIR) : sort readdir(DIR)){
	my $file = $_;
	my $d;
	next unless $file =~ /^(\d\d)\.tdf$/;    # must be DD.tdf
	$d = $1;
	next unless ($d >= 1 && $d <= DaysMonth($year, $month)); # valid date
	next if -z "$dir/$file";                    # must be not empty
	#print "$file, ";
	my $flg;
	if ($min_day == 0){    # monthly
	    $flg = 1;
	} else {               # day or partly
	    $flg = ($d >= $min_day && $d <= $max_day);
	}
	if ($flg){
	    my $lm = (stat("$dir/$file"))[9];
	    if ($self->last_modified < $lm){
		$self->last_modified($lm);
	    }
	    push(@files, ["$dir/$file", $year, $month, $d, $lm]);
	}
    }
    closedir(DIR);

    return @files;
}

=head2 $c->pickup_recent($year, $month, $day, $num);

最新 $num 日を取り出す。
$num == 0 なら全日記。

=cut

sub pickup_recent($$$$$)
{
    my ($self, $year, $month, $day, $num) = @_;
    my $date = new DateTime::Date(year=>$year, month=>$month, day=>$day);
    
    my @files;

    while ($date->year >= $TDS::Status->start_year && # 存在する年
	   ($num <= 0 ||                                    # 最大が指定されていないか
	    (@files < $num))){                         # 指定された数分
	#print STDERR "collect_files\n";
	push(@files, reverse $self->pickup_files($date->year, $date->month, 1, $day));
	$date -= '1M';
	$day = 31;
    }
    # 必要のないファイルの last_modified も見ているので、
    # もう一回調査
    my $lm;
    # 表示数が指定されていなかったら全部
    unless ($num){
	$num = $#files+1;
    }
    # ファイル数より指定表示数が大きかったらファイル数に（小さい方を取る）
    if ($num > $#files+1){
	$num = $#files+1;
    }
    # lm をチェック
    for (@files[0..$num-1]){
	my $the_lm = $_->[4] || 0;
	if ($lm < $the_lm){
	    $lm = $the_lm;
	}
    }
    $self->last_modified($lm);
    # 該当する @files を返す
    # print STDERR "collect_files done\n";
    return @files[0..$num-1];
}

=head2 $c->pickup_neighbour($year, $month, $day, $prev_num, $next_num);

$year/$month/$day の付近 [$prev_num, $next_num] 分の日記を取り出す。

=cut

sub pickup_neighbour ($$$$$$)
{
    my ($self, $year, $month, $day, $prev_num, $next_num) = @_;
    
    my $min_day = $day - $prev_num;
    my $max_day = $day + $next_num;
    
    my @files;
    my $date = new DateTime::Date(year=>$year, month=>$month);
#    my $min_date = new DateTime::Date(year=>$year, month=>$month, day=>$day);
#    my $max_date = new DateTime::Date(year=>$year, month=>$month, day=>$day);
    
#    $min_date -= $prev_num;
#    $max_date += $next_num;

    # 前月にまたがる
    if ($min_day < 1){
	my $prev_date = new DateTime::Date(year=>$year, month=>$month, day=>$day);
	$prev_date -= '1M';
	
	my $days_month = $prev_date->DaysMonth;
	@files = $self->pickup_files($prev_date->year,
				   $prev_date->month,
				   $days_month + $min_day,
				   $days_month);
	$min_day = 1;
    }
    push(@files, $self->pickup_files($date->year,
				   $date->month,
				   $min_day, $max_day));
    # 次の月にまたがる
    if ($max_day > $date->DaysMonth){
	my $next_date = new DateTime::Date(year=>$year, month=>$month, day=>$day);

	my $days_month = $date->DaysMonth;
	$next_date += '1M';
	push(@files, $self->pickup_files($next_date->year,
					 $next_date->month,
					 1,
					 $max_day-$days_month));
    }
    return @files;
}
################################################################
# ファイルをパースする
sub parse_file ($$$)
{
#    my ($self, $filename, $year, $month, $day) = @_;
    my ($self, $filename, $parser) = @_;

    # check
    die "BUG: TDS::Collection::parse_file: not TDS::Tdf::Parser object"
	unless ref $parser eq 'TDS::Tdf::Parser';

    open(F, "$filename") || die $filename;
    sub conv;
    *conv = mkjconv($TdfJcode, $TDS::System::InternalJcode);
    while (<F>){
	my $tmp = $_;
	$tmp .= "\n" unless $tmp =~ /\n$/;        # thanks to Mr.Takei
	conv(\$tmp);

	# send to parser
	$parser->Parse($tmp);
    }
    close F;
    return $parser;
}
################################################################
# read specified files.
# return number of read files.
sub read_files($@)
{
    my ($self, @files) = @_;

    require TDS::Cache;
    # clear caches if tdf files is updated
    my $total;
    my $reverse = $ReverseInRecent && $TDS::Status->mode =~ /RECENT/;
    # for all files

    for (@files){
	last unless ref $_;
	my ($filename, $year, $month, $day) = @$_;
	next unless $filename;
	my $cache_unused;

#	warn times, " collect: $filename";
#	print "<br>$year/$month/$day:";
	
	# read from cache if possile
	if ($TDS::Cache::EnableCache){
	    require TDS::Cache::Daily;
	    #warn "read cache: $year/$month/$day";
	    my $cache = new TDS::Cache::Daily(reverse=>$reverse);

	    $cache->year($year);
	    $cache->month($month);
	    $cache->day($day);
	    if ($cache->IsFresh){
#		warn times, " use daily cache: ", $cache->GetCacheFilename;
		push(@{$self->diarys}, $cache);
		$TDS::Status->has_secret(1)
		    if $cache->has_secret;
		$cache_unused = 0;
	    } else {
		$cache_unused = 1;
	    }
	} else {
	    $cache_unused = 1;
	}
#	warn "$year/$month/$day: cache unused $cache_unused";
	# if impossible, read tdf file and parse it
	if ($cache_unused){
#	    warn times, " cannot use daily cache, so parse it";
	    my $date_line = "DIARY $year $month $day\n";
	
#	    my $start = (times)[0];
	    require TDS::Tdf::Parser;
	    my $parser = new TDS::Tdf::Parser;
	    $parser->Parse($date_line);

	    $self->parse_file($filename, $parser);
	    $parser->end('DIARY');

	    push(@{$self->diarys}, $parser);
	}
    }
#    return $_ = @files;                # ActivePerl 613 だと modification of a read-only value と怒られる
#    warn times, "done";
    return scalar(@files);
    
#    push(@{$self->diarys}, $parser);
#    print "<br>read done: ", time() - $TDS::Status->start_time->time;    
}
################################################################
sub AsTdf($)
{
    my $self = shift;
    my $lines;
    
    for (@{$self->diarys}){
	$lines = $_->top->AsTdf;
    }
    return $lines;
}

sub AsHTML($;$)
{
    my ($self, $params) = @_;

    $params->{topic} = $TDS::Status->topic;
    $params->{mode} = $TDS::Status->mode;
#    print "AsHTML...\n";
    my $html;

    # for all diarys
    require TDS::Cache;
    my $cache;
    
    my @htmls;
    my $s = (times)[0];
    for (@{$self->diarys}){
	if (ref $_ eq 'TDS::Tdf::Parser'){
	    my $parser = $_;
	    my $daily_html = $parser->top->AsHTML($params);
	    # auto complete
	    if ($AutoReplace){
		require TDS::Replacer;
		;TDS::Replacer->ReplaceLine(\$daily_html);
	    }
	    if ($TDS::Cache::EnableCache){
		require TDS::Cache::Daily;
		$cache = 
		    new TDS::Cache::Daily('reverse'=>$ReverseInRecent &&
					  $TDS::Status->mode =~ /RECENT/);
		$cache->Set($params->{'year'},
			    $params->{'month'},
			    $params->{'day'});
		if ($params->{'has_secret'}){
		    $cache->has_secret(1);
		    $TDS::Status->has_secret(1);
		}
		# if it has secret part, then do NOT write cache
		# return if $self->is_author && $self->has_secret;
		$cache->WriteCache($daily_html);
	    }
	    $html .= $daily_html;
##	    push(@htmls, $daily_html);
	} elsif (ref $_ eq 'TDS::Cache::Daily'){
#	    $html .= "cached" . ($_->prefix) . ($_->html);
	    $html .= $_->ReadCache;
##	    push(@htmls, $_->html);
	} else {
	    die "BUG: Collection::AsHTML: illegal reference";
	}
    }

    unless ($html){
	$html = $self->NoContentHook;
    }
    my $e = (times)[0];
##    warn times, " col AsHTML elasp: ", $e - $s;
    return $html;
##    return join('', @htmls);
}
sub NoContentHook($)
{
}

1;
