# -*- cperl -*-
# -----------------------------------------------------------------------------
# $Id: Add.pm,v 1.5 2005/05/25 10:26:45 admin Exp $
# -----------------------------------------------------------------------------
package Celaenito::Httpd::Add;
use strict;
use warnings;
use HTTP::Status;
use HTML::Entities;
use Template;
use Celaenito::Config;
use Celaenito::Template;
use Celaenito::StaticFile;
use Celaenito::Permission;
use Celaenito::DB;
use Celaenito::Logger;
use Celaenito::Httpd;
use URI;
use URI::QueryParam;
use Data::Dumper;
use POE::Kernel;

sub handler {
    my ($req, $resp) = @_;

    if ($req->uri->path eq '/-/add') {
	not CAN_ADD($req) and
	  return need_auth_handler(@_);
    }
    elsif ($req->uri->path eq '/-/edit') {
	not CAN_EDIT($req) and
	  return need_auth_handler(@_);
    }
    
    my $mode = $req->uri->query_param('mode');
    if (!$mode) {
	if ($req->uri->path eq '/-/add') {
	    return addform_handler(@_);
	}
	elsif ($req->uri->path eq '/-/edit') {
	    return editform_handler(@_);
	}
    }
    elsif ($mode eq 'js') {
	return addform_js_handler(@_);
    }
    elsif ($mode eq 'add') {
	return addform_add_handler(@_);
    }
    elsif ($mode eq 'edit') {
	return editform_edit_handler(@_);
    }
    else {
	return notfound_handler(@_);
    }
}

sub addform_handler {
    my ($req, $resp) = @_;
    my $confirm = $req->uri->query_param('confirm');
    
    my $t = {};
    
    $t->{confirm} = $confirm;

    if (my @error = $req->uri->query_param('error')) {
	$t->{error} = [
	    map {
		encode_entities($_, "<&>");
	    } reverse @error,
	   ];
    }
    $req->uri->query_param_delete('error');

    if (my @warning = $req->uri->query_param('warning')) {
	$t->{warning} = [
	    map {
		encode_entities($_, "<&>");
	    } reverse @warning,
	   ];
    }
    $req->uri->query_param_delete('warning');

    # デフォルト値をセット
    # FIXME: このデフォルト値はユーザ毎の設定であるべき
    my %defaults = (
	url         => 'http://',
	priority    => 'normal',
	overwrite   => 'off',
	au          => 'off',
	au_day      => 30,
	au_hour     => 0,
	au_minute   => 0,
	au_keep_dis => 'off',
	au_inherit  => 'on',
	rcr         => 'off',
	rcr_level   => 0,
	rcr_no_par  => 'on',
	rcr_go_frn  => 'off',
	rcr_strip_q => 'on',
       );
    while (my ($key, $value) = each %defaults) {
	defined $req->uri->query_param($key) or
	  $req->uri->query_param($key => $value);
    }

    $t->{add_mode} = 1;
    $t->{title} = config('ui.title');
    %$t = (%$t, %{$req->uri->query_form_hash});
    
    if ($confirm) {
	# disabledとなつてゐる項目はsubmitされないので、hiddenを追加する。
	$req->uri->query_param_delete('mode');
	$req->uri->query_param_delete('confirm');
	$t->{hidden} = $req->uri->query_form_hash;
    }

    my $out;
    TMPL->process('add.tt', $t, \$out)
      or die Template->error;
    
    $resp->content($out);
    return RC_OK;
}

sub editform_handler {
    my ($req, $resp) = @_;
    my $confirm = $req->uri->query_param('confirm');

    my $t = {};

    my $urlid = $req->uri->query_param('urlid');
    if (!defined $urlid) {
	return notfound_handler(@_);
    }
    
    $t->{confirm} = $confirm;

    if (my @error = $req->uri->query_param('error')) {
	$t->{error} = [
	    map {
		encode_entities($_, "<&>");
	    } reverse @error,
	   ];
    }
    $req->uri->query_param_delete('error');

    if (my @warning = $req->uri->query_param('warning')) {
	$t->{warning} = [
	    map {
		encode_entities($_, "<&>");
	    } reverse @warning,
	   ];
    }
    $req->uri->query_param_delete('warning');

    # デフォルト値をセット
    my $urlinfo = DB->selectrow_hashref(
	q{SELECT * FROM target_url WHERE urlid = ?}, undef, $urlid);
    if (!$urlinfo) {
	return notfound_handler(@_);
    }
    my %opts = %{ eval($urlinfo->{options} || '{}') };
    
    foreach my $key (qw[url priority label notation]) {
	defined $req->uri->query_param($key) or
	  $req->uri->query_param($key => $urlinfo->{$key});
    }

    my %flags = (
	overwrite                => 'overwrite',
	auto_update_interval     => 'au',
	keep_update_on_disappear => 'au_keep_dis',
	inherit_auto_update      => 'au_inherit',
	recursive_fetch          => 'rcr',
	no_parent                => 'rcr_no_par',
	span_hosts               => 'rcr_go_frn',
	strip_query_string       => 'rcr_strip_q',
       );
    while (my ($key, $value) = each %flags) {
	defined $req->uri->query_param($value) or
	  $req->uri->query_param(
	      $value => ($opts{$key} ? 'on' : 'off'));
    }
    defined $req->uri->query_param('rcr_level') or
      $req->uri->query_param(
	  rcr_level => $opts{recursion_level} || 0);
    if ($_ = $opts{auto_update_interval} and
	  !defined $req->uri->query_param('au_day')) {
	local *split_interval = \&Celaenito::Httpd::Index::split_interval;
	my ($day, $hour, $min) = split_interval($_);
	$req->uri->query_param(au_day    => $day  || 0);
	$req->uri->query_param(au_hour   => $hour || 0);
	$req->uri->query_param(au_minute => $min  || 0);
    }

    $t->{add_mode} = undef;
    $t->{title} = config('ui.title');
    %$t = (%$t, %{$req->uri->query_form_hash});

    if ($confirm) {
	$req->uri->query_param_delete('mode');
	$req->uri->query_param_delete('confirm');
	$req->uri->query_param_delete('urlid');
	$t->{hidden} = $req->uri->query_form_hash;
    }

    my $out;
    TMPL->process('add.tt', $t, \$out)
      or die Template->error;
    
    $resp->content($out);
    return RC_OK;
}

sub addform_js_handler {
    my ($req, $resp) = @_;
    $resp->header('Content-Type' => 'text/javascript; charset=UTF-8');
    $resp->content(static_file('./tmpl/add.js'));
    return RC_OK;
}

sub _check_ao_interval {
    my ($u, $err_array) = @_;
    my @error;

    if ($u->query_param('au') eq 'on') {
	my $nan;
	my $negative;
	my $zerocount = 0;
	foreach (qw[day hour minute]) {
	    my $val = $u->query_param("au_$_");
	    if ($val !~ m/^-?\d+$/) {
		$nan = 1;
		last;
	    }
	    elsif ($val < 0) {
		$negative = 1;
		last;
	    }
	    elsif ($val == 0) {
		$zerocount++;
	    }
	}
	if ($nan) {
	    push @error,
	      'Interval of Auto Update contains some value which are not a number.';
	}
	if ($negative) {
	    push @error,
	      'Interval of Auto Update contains some negative value.';
	}
	if ($zerocount == 3) {
	    push @error,
	      'You cannot set 0 days 0 hours 0 minutes to the interval of Auto Update.';
	}
    }

    push @$err_array, @error;
}

sub _check_rcr_level {
    my ($u, $err_array) = @_;
    my @error;
    
    if ($u->query_param('rcr') eq 'on') {
	my $level = $u->query_param('rcr_level');
	if ($level !~ m/^-?\d+$/) {
	    push @error,
	      'Recursion Level is not a numeric value.';
	}
	elsif ($level < 0) {
	    push @error,
	      'Recursion Level is negative.';
	}
    }

    push @$err_array, @error;
}

sub _check_warnings {
    my ($u, $warn_array) = @_;
    my @warn;

    if ($u->query_param('rcr') eq 'on') {
	# レベル無制限であり、且つ次の何れか状態である場合には警告する。
	# * rcr_no_parがoffである
	# * rcr_go_frnがonである
	# * rcr_strip_qがoffである
	my $level = $u->query_param('rcr_level');
	if (defined $level and $level =~ m/^\d+$/ and $level == 0) {
	    if ($u->query_param('rcr_no_par') eq 'off') {
		push @warn,
		  '* Ascend to the parent directory';
	    }
	    if ($u->query_param('rcr_go_frn') eq 'on') {
		push @warn,
		  '* Go to foreign hosts';
	    }
	    if ($u->query_param('rcr_strip_q') eq 'off') {
		push @warn,
		  '* Follow links without stripping their query string';
	    }
	    if (@warn) {
		unshift @warn,
		  'With recursive fetch enabled:';
		push @warn,
		  'This may cause enormous fetching.';
	    }
	}
    }

    push @$warn_array, @warn;
}

sub _make_options {
    my $u = shift;
    
    my $options = {};
    if ($u->query_param('overwrite') eq 'on') {
	$options->{overwrite} = 1;
    }
    if ($u->query_param('au') eq 'on') {
	my $intv =
	  $u->query_param('au_day') * 24 * 60 +
	    $u->query_param('au_hour') * 60 +
	      $u->query_param('au_minute');
	$options->{auto_update_interval} = $intv;

	if ($u->query_param('au_keep_dis') eq 'on') {
	    $options->{keep_update_on_disappear} = 1;
	}

	if ($u->query_param('au_inherit') eq 'on') {
	    $options->{inherit_auto_update} = 1;
	}
    }
    if ($u->query_param('rcr') eq 'on') {
	$options->{recursive_fetch} = 1;
	$options->{recursion_level} = $u->query_param('rcr_level');
	if ($u->query_param('rcr_no_par') eq 'on') {
	    $options->{no_parent} = 1;
	}
	if ($u->query_param('rcr_go_frn') eq 'on') {
	    $options->{span_hosts} = 1;
	}
	if ($u->query_param('rcr_strip_q') eq 'on') {
	    $options->{strip_query_string} = 1;
	}
    }
    $options;
}

sub _dump_options {
    my $options = shift;

    if (%$options) {
	Data::Dumper->new([$options])
	    ->Indent(0)
	      ->Purity(1)
		->Useqq(1)
		  ->Terse(1)
		    ->Dump;
    }
    else {
	undef;
    }
}

sub addform_add_handler {
    my ($req, $resp) = @_;
    my @error;
    my @warn;
    my $u = $req->uri;

    if ($u->query_param('cancel')) {
	# キャンセル。
	# confirmが付いてゐればフォーム画面へ戻り、さうでなければindexへ戻る。
	if ($u->query_param('confirm')) {
	    $u->query_param_delete('confirm');
	    return addform_handler(@_);
	}
	else {
	    return redirect_handler($req, $resp, '/');
	}
    }

    # 取敢へず今はhttpのみサポート。
    my $uri = URI->new($u->query_param('url'))->canonical;
    # fragmentは消す。
    $uri->fragment(undef);
    # 再利用する時の為に、$reqにこれを入れておく。
    $u->query_param(url => $uri->as_string);
    
    if (!$uri->host) {
	push @error, 'Invalid URL.';
    }
    elsif ($uri->scheme ne 'http') {
	push @error, 'Only http is currently supported.';
    }

    # 全く同じURLが既にtarget_url内に存在したらエラー。
    # 代わりにURL更新/削除画面へのリンクを出す。
    my ($duplicated) = DB->selectrow_array(
	q{SELECT COUNT(*) FROM target_url WHERE url=?}, undef,
	$uri->as_string);
    if ($duplicated) {
	push @error,
	  'The requested URL is already added. <a href="%s">[Edit/Delete]</a>',
	    do {
		my $info = URI->new('/-/info', 'http');
		$info->query_param(url => $uri->as_string);
		$info->canonical->as_string;
	    };
    }

    # 自動更新がOnなら、その間隔をチェック。
    _check_ao_interval($u, \@error);

    # 再歸取得がOnなら、そのレベルをチェック。
    _check_rcr_level($u, \@error);

    # 警告すべき事があるかどうかをチェック。
    _check_warnings($u, \@warn);
    
    if (@warn) {
	# 警告事項があれば表示。
	$u->query_param(warning => @warn);
    }

    if (@error) {
	# エラーがあればフォームを再表示。
	$u->query_param(error => @error);
	return addform_handler(@_);
    }

    # confirm => 1でなければ確認画面へ
    if (!$u->query_param('confirm')) {
	$u->query_param(confirm => 1);
	return addform_handler(@_);
    }

    # optionsハッシュを作る。
    my $options_dumped =
      _dump_options(_make_options($u));

    # target_urlにINSERT
    my $undef = sub { length $_[0] ? $_[0] : undef };
    DB->do(q{
INSERT INTO target_url
       (url, priority, label, added_date, options)
VALUES ( ? ,    ?    ,   ?  ,    NOW()  ,    ?   )
},
	   undef,
	   $u->query_param('url'),
	   $u->query_param('priority'),
	   $undef->($u->query_param('label')),
	   $options_dumped
	  );
    my ($urlid) = DB->selectrow_array(q{SELECT LAST_INSERT_ID()});
    
    lognotice "Added to the target_url:\n".
      "URL ID  : ".$urlid."\n".
      "URL     : ".$u->query_param('url')."\n".
      "Priority: ".$u->query_param('priority')."\n".
      "Label   : ".$u->query_param('label')."\n".
      "Options : ".($options_dumped || '');
    
    # INSERTに成功したら、これをjob_queueに入れる。
    # 自分で入れるのではなく JobQueueManager に入れさせる。
    $poe_kernel->post(
	'jobqueue', enqueue => $urlid, time);

    # トップページにリダイレクト。
    # FIXME: このURLの詳細情報へ飛ばした方が良い？
    return redirect_handler($req, $resp, '/');
}

sub editform_edit_handler {
    my ($req, $resp) = @_;
    my @error;
    my @warn;
    my $u = $req->uri;
    
    my $urlid = $req->uri->query_param('urlid');
    if (!defined $urlid) {
	return notfound_handler(@_);
    }

    if ($u->query_param('cancel')) {
	# キャンセル。
	# confirmが付いてゐればフォーム画面へ戻り、さうでなければURL Infoへ戻る。
	if ($u->query_param('confirm')) {
	    $u->query_param_delete('confirm');
	    return editform_handler(@_);
	}
	else {
	    return redirect_handler(
		$req, $resp, '/-/info', {urlid => $urlid});
	}
    }

    # 自動更新がOnなら、その間隔をチェック。
    _check_ao_interval($u, \@error);

    # 再歸取得がOnなら、そのレベルをチェック。
    _check_rcr_level($u, \@error);

    # 警告すべき事があるかどうかをチェック。
    _check_warnings($u, \@warn);
    
    if (@warn) {
	# 警告事項があれば表示。
	$u->query_param(warning => @warn);
    }

    if (@error) {
	# エラーがあればフォームを再表示。
	$u->query_param(error => @error);
	return editform_handler(@_);
    }

    # confirm => 1でなければ確認画面へ
    if (!$u->query_param('confirm')) {
	$u->query_param(confirm => 1);
	return editform_handler(@_);
    }

    # optionsハッシュを作る。
    my $options = _make_options($u);
    my $options_dumped = _dump_options($options);

    # target_urlをUPDATE
    #
    # FIXME: label または notation が變はったのでない限り
    # last_modified を更新する必要は無い。
    my $undef = sub { $_[0] =~ m/\S/ ? $_[0] : undef };
    DB->do(q{
	UPDATE target_url
	   SET priority = ?,
	       label = ?,
               notation = ?,
               last_modified = NULL,
	       options = ?
	 WHERE urlid = ?
    }, undef, (
	$u->query_param('priority'),
	$undef->($u->query_param('label')),
	$undef->($u->query_param('notation')),
	$options_dumped,
	$urlid,
       ));

    # URL Infoにリダイレクト。
    return redirect_handler(
	$req, $resp, '/-/info', {urlid => $urlid});
}

1;
