# -*- cperl -*-
# -----------------------------------------------------------------------------
# $Id: Pager.pm,v 1.1.1.1 2004/09/27 14:01:00 admin Exp $
# -----------------------------------------------------------------------------
package Pager;
use strict;
use warnings;
use Carp;
use UNIVERSAL qw(isa);
use Error qw(:try);
use Hash::Util qw(lock_keys);
our $VERSION = '0.1';

=pod

 my $pager = Pager->new(
     # 目的のデータを取り出す方法。
     # SQL文を使用する場合:
     fetch => {
	 sql         => q{SELECT foo, bar FROM foo WHERE bar=? AND baz=?},
	 placeholder => [100, 200],
	 dbh         => $dbh,
     },
     # CODEを使用する場合:
     fetch => {
	 code => sub {
	     # $begin: 表示を開始する項目の番號(0オリジン)
	     # $length: 表示する項目の個數
	     # $code_show: 表面的には、show_contentで指定された關數(内部的には違ふ)
	     my ($begin, $length, $code_show) = @_;
	     for ($begin .. ($begin + $length - 1)) {
		 $code_show->("Item [$_]");
	     }
	 },
     },
     # 全項目數を知る方法。
     # fetchがSQL文であるなら、countを省略すれば
     # 自動的にSELECT COUNT(*)文をfetchのSQLから生成する。
     count => {
	 sql         => q{SELECT COUNT(*) FROM foo WHERE bar=? AND baz=?},
	 placeholder => [100, 200],
         dbh         => $dbh,
     },
     # 若しくは
     count => {
	 code => sub {
	     return 100;
	 },
     },
     page_to_show   => 2,  # 表示したいページ(1オリジン); デフォルトは1
     pagenums_limit => 10, # 各ページへのリンクの最大表示個數; デフォルトは10
     items_per_page => 20, # ページ毎の項目の最大表示數; デフォルトは20
     # メタ情報が得られた時に呼ばれる關數。
     show_meta => sub {
	 # 番號は全て1オリジン。
         # total: 総件數
	 # page: 現在のページ番號
	 # last_page: 最終ページ番號
	 # pagelink_begin: 各ページへのリンクの開始ページ番號
	 # pagelink_end: 各ページへのリンクの終了ページ番號(この番號も含む)
         my %meta = @_;
	 print "page: $meta{page}/$meta{last_page}; total: $meta{total}\n";
     },
     # 項目が得られた時に呼ばれる關數。
     show_content => sub {
	 # fetchがSQL文なら、$_[0]はfetchrow_hashrefで得られたハッシュ。
	 # CODEなら、そのCODEの中から明示的に呼ばれなければならない。
	 print "item: $_[0]\n";
     },
     # 項目が一つも無い時に呼ばれる關數。省略可。
     no_content => sub {
         # do what you want to
     },
 );
 $pager->execute;

=cut

sub new {
    my ($class, %args) = @_;

    my $fetch = $args{fetch};
    if (!isa $fetch, 'HASH') {
	croak "Arg{fetch} is not a hashref";
    }
    if (defined $fetch->{sql}) {
	$fetch->{dbh} or croak "Arg{fetch} has a sql, but not dbh";
	$fetch->{code} and croak "Arg{fetch} has both of sql and code";
	# FIXME: LIMITが付いてゐたらcroak
    }
    elsif (defined $fetch->{code}) {
	(isa $fetch->{code}, 'CODE') or croak "Arg{fetch}{code} is not a coderef: ".$fetch->{code};
	$fetch->{sql} and croak "Arg{fetch} has both of sql and code";
    }

    my $count = $args{count};
    if (!defined $count) {
	if (defined $fetch->{sql}) {
	    # fetchのSQL文を改造して作る。
	    (my $sql = $fetch->{sql}) =~ s/^\s*select\s+(.+?)\s+from/select count(*) from/i;
	    $count = $args{count} = {};
	    $count->{sql} = $sql;
	    $count->{placeholder} = $fetch->{placeholder} if $fetch->{placeholder};
	    $count->{dbh} = $fetch->{dbh};
	}
	else {
	    croak "Arg{count} cannot be omitted when the Arg{fetch} is not a SQL";
	}
    }
    elsif (!isa $count, 'HASH') {
	croak "Arg{count} is not a hashref";
    }
    # FIXME: $args{count}のsanity checkを（もっと）

    # FIXME: 以下の三つの變數をsanity check
    $args{page_to_show} ||= 1;
    $args{pagenums_limit} ||= 10;
    $args{items_per_page} ||= 20;

    foreach my $key (qw/show_meta show_content/) {
	if (!defined $args{$key}) {
	    croak "Missing Arg{$key}";
	}
	elsif (!isa $args{$key}, 'CODE') {
	    croak "Arg{$key} is not a coderef: $args{$key}";
	}
    }

    if (defined $args{no_content} and
	  !isa $args{no_content}, 'CODE') {
	croak "Arg{no_content} is not a coderef: $args{no_content}";
    }
    $args{no_content} ||= undef;
    
    my $this = bless \%args => $class;
    lock_keys %$this;
    $this;
}

sub ceil {
    my $val = $_[0];
    my $floor = int($val);
    $floor == $val ? $floor : $floor + 1;
}

sub min {
    my $min = $_[0];
    foreach (@_) {
	$min = $_ if $min > $_;
    }
    $min;
}

sub max {
    my $max = $_[0];
    foreach (@_) {
	$max = $_ if $max < $_;
    }
    $max;
}

sub execute {
    my $this = shift;

    my $count;
    if (defined $this->{count}{sql}) {
	try {
	    $count = $this->{count}{dbh}->selectrow_array(
		$this->{count}{sql}, undef, @{$this->{count}{placeholder} || []});
	} otherwise {
	    throw Error::Simple "Failed to do count by sql: $_[0]";
	};
    }
    else {
	try {
	    $count = $this->{count}{code}->();
	} otherwise {
	    throw Error::Simple "Failed to do count by code: $_[0]";
	};
    }

    # この情報を基に、メタデータを生成
    my $last_page = max(1, ceil($count / $this->{items_per_page}));
    my $page = min($this->{page_to_show}, $last_page);
    my $pagelink_begin = max(1, int($page - $this->{pagenums_limit} / 2));
    my $pagelink_end = min($last_page, $pagelink_begin + $this->{pagenums_limit} - 1);

    # show_metaを呼ぶ
    $this->{show_meta}->(
	count          => $count,
	page           => $page,
	last_page      => $last_page,
	pagelink_begin => $pagelink_begin,
	pagelink_end   => $pagelink_end);

    # fetch實行
    my $fetch_begin = ($page - 1) * $this->{items_per_page};
    my $fetch_length = min($count - $fetch_begin,
			   $this->{items_per_page});
    if (defined $this->{fetch}{sql}) {
	try {
	    # FIXME: これより増しなLIMIT生成方法を
	    my $sql = $this->{fetch}{sql} . " LIMIT $fetch_begin, $fetch_length";
	    
	    my $sth = $this->{fetch}{dbh}->prepare($sql);
	    $sth->execute(@{$this->{fetch}{placeholder} || []});
	    while ($_ = $sth->fetchrow_hashref) {
		$this->{show_content}->($_);
	    }
	    if ($sth->rows == 0) {
		$this->{no_content} and
		  $this->{no_content}->();
	    }
	} otherwise {
	    throw Error::Simple "Failed to do fetch by sql: $_[0]";
	};
    }
    else {
	# $fetch_length囘呼ばれたかどうかをチェックする
	my $called_count = 0;
	my $checked_show = sub {
	    $called_count++;
	    goto &{$this->{show_content}};
	};
	try {
	    $this->{fetch}{code}->(
		$fetch_begin, $fetch_length, $checked_show);
	} otherwise {
	    throw Error::Simple "Failed to do fetch by code: $_[0]";
	};
	if ($called_count != $fetch_length) {
	    carp "Fetch coderef didn't call show_content exactly $fetch_length times (call \$length($fetch_length) times)";
	}
	if ($fetch_length == 0) {
	    $this->{no_content} and
	      $this->{no_content}->();
	}
    }

    $this;
}

1;
