# -*- cperl -*-
# -----------------------------------------------------------------------------
# $Id: Httpd.pm,v 1.2 2004/09/30 17:00:40 admin Exp $
# -----------------------------------------------------------------------------
package Celaenito::Httpd;
use strict;
use warnings;
use POE::Session;
use POE::Component::Server::HTTP;
use CGI::Pretty qw(:standard);
use HTTP::Status;
use Celaenito::Template;
use Celaenito::Config;
use Celaenito::StaticFile;
use Celaenito::Logger;
use Hash::Util qw(lock_keys);
use Carp;
use UNIVERSAL qw(VERSION);
use constant ALIAS => 'httpd';
require Exporter;
BEGIN {
    our @ISA = qw(Exporter);
    our @EXPORT = qw(
        notfound_handler
	need_auth_handler
	redirect_handler
	internal_server_error_handler);
}
use Celaenito::Httpd::Index;
use Celaenito::Httpd::Add;
use Celaenito::Httpd::Show;
use Celaenito::Httpd::List;
use Celaenito::Httpd::URLInfo;
use Celaenito::Httpd::Draft;
use Celaenito::Httpd::General;
use Celaenito::Httpd::UserInfo;
use Celaenito::Httpd::Search;

BEGIN {
    VERSION('CGI', 2.69); # XHTMLサポートが必要
}

sub run {
    my $class = shift;
    my $this = bless {} => $class;
    
    $this->{httpd} = undef; # PoCo::Server::HTTPのセッションID

    lock_keys %$this;
    
    POE::Session->create(
	heap => $this,
	inline_states => {
	    _start     => \&_start_handler,
	    _child     => \&_child_handler,
	    _stop      => \&_stop_handler,
	    shutdown   => \&shutdown_handler,
	    _signal_stop => sub {
		$_[KERNEL]->sig_handled;
		$_[KERNEL]->yield('shutdown');
	    },
	},
       );
}

sub _start_handler {
    my $this = $_[HEAP];
    $_[KERNEL]->alias_set(ALIAS);
    foreach (qw[HUP INT QUIT ABRT TERM]) {
	$_[KERNEL]->sig($_ => '_signal_stop');
    }

    my $barrier = sub {
	my $code = $_[0];
	return sub {
	    my ($req, $resp) = @_;

	    # 次の條件を全て滿す時、contentを$reqのqueryに複製する。
	    # 1. POSTリクエスト
	    # 2. Content-Typeがapplication/x-www-form-urlencoded
	    # 3. 元の$reqのqueryが空
	    if ($req->method eq 'POST' and
		  $req->headers->content_type eq 'application/x-www-form-urlencoded' and
		    !defined $req->uri->query) {
		$_ = $req->content;
		tr/\x0d\x0a//d;
		$req->uri->query($_);
	    }
	    
	    my $last_exception;
	    my $retval = eval {
		local $SIG{__DIE__} = sub {
		    print $_[0];
		    $last_exception = Carp::longmess(@_);
		};
		    
		$code->(@_);
	    }; if ($@) {
		$retval = internal_server_error_handler(
		    $req, $resp, $last_exception);
	    }
	    
	    # 應答を補完
	    $resp->code or
	      $resp->code($retval);
	    $resp->message or
	      $resp->message(status_message($resp->code));
	    
	    if (!length $resp->content) {
		if ($resp->code >= 200 && $resp->code < 300) {
		    $retval = internal_server_error_handler(
			$req, $resp, 'Content handler did not set any content.');
		}
		elsif ($resp->code >= 300 && $resp->code < 400) {
		    # FIXME: リダイレクト先にリンクしているHTMLを補完
		}
	    }
	    
	    # Content-Typeのデフォルトはtext/html
	    $resp->header('Content-Type') or
	      $resp->header('Content-Type' => 'text/html; charset=UTF-8');

	    # Content-Lengthを補完
	    # PoCo::Server::HTTPにこれを任せると、HEADでcontentを消した時に
	    # Content-Lengthを0にされてしまふ。
	    $resp->header('Content-Length') or
	      $resp->header('Content-Length' => length($resp->content));

	    # HEADならcontentを消す
	    if ($req->method eq 'HEAD') {
		$resp->content(undef);
	    }
	    
	    return $retval;
	};
    };
	
    $this->{httpd} = POE::Component::Server::HTTP->new(
	Port => config('httpd.port'),
	ContentHandler => {
	    '/'
	      => $barrier->(\&Celaenito::Httpd::Index::handler),
	    '/-/'
	      => $barrier->(\&notfound_handler),
	    '/-/css/'
	      => $barrier->(\&css_handler),
	    '/-/add/'
	      => $barrier->(\&Celaenito::Httpd::Add::handler),
	    '/-/edit/'
	      => $barrier->(\&Celaenito::Httpd::Add::handler),
	    '/-/queue/'
	      => $barrier->(\&Celaenito::Httpd::Index::handler),
	    '/-/log/'
	      => $barrier->(\&Celaenito::Httpd::Index::handler),
	    '/-/list/'
	      => $barrier->(\&Celaenito::Httpd::List::handler),
	    '/-/info/'
	      => $barrier->(\&Celaenito::Httpd::URLInfo::handler),
	    '/-/draft/'
	      => $barrier->(\&Celaenito::Httpd::Draft::handler),
	    '/-/search/'
	      => $barrier->(\&Celaenito::Httpd::Search::handler),
	    '/-/login/'
	      => $barrier->(\&Celaenito::Httpd::UserInfo::handler),
	    '/-/logout/'
	      => $barrier->(\&Celaenito::Httpd::UserInfo::handler),
	    '/-/userconf/'
	      => $barrier->(\&Celaenito::Httpd::UserInfo::handler),
	    '/latest/'
	      => $barrier->(\&Celaenito::Httpd::Show::handler),
	},
	Headers => {
	    Server => 'Celaenito UI (Powered by PoCo::Server::HTTP)',
	    Connection => 'close',
	},
       );
    
    logdebug "started running...";
}

sub _stop_handler {
    logdebug "STOPPED.";
}

sub _child_handler {
    #my ($this, $event, $child_session) = @_[HEAP, ARG0, ARG1];
}

sub shutdown_handler {
    my $this = $_[HEAP];
    logdebug "Shutdown time has arrived. Telling http server to stop...";

    $_[KERNEL]->alias_remove(ALIAS);
    $_[KERNEL]->post($this->{httpd} => 'shutdown');
}

sub notfound_handler {
    my ($req, $resp) = @_;
    my $t = {
	code    => 404,
	message => 'Not Found',
	url     => $req->uri->as_string,
    };

    my $out = '';
    TMPL->process('error.tt', $t, \$out)
      or die Template->error;

    $resp->content($out);
    return RC_NOT_FOUND;
}

sub css_handler {
    my ($req, $resp) = @_;
    (my $path = $req->uri->path) =~ s|^/-/css/||;
    my $content = static_file('./css/'.$path);

    if (defined $content) {
	$resp->header('Content-Type' => 'text/css; charset=UTF-8');
	$resp->content($content);
	return RC_OK;
    }
    else {
	return notfound_handler(@_);
    }
}

sub internal_server_error_handler {
    my ($req, $resp, $error) = @_;
    
    my $style = {
	-style => 'white-space: pre;',
    };
		    
    my $html = join '',
      start_html(
	  -title    => 'Internal Server Error',
	  -encoding => 'UTF-8',
	 ),
        h1('Internal Server Error'),
	  h2('Request'),
	    p($style, $req->as_string),
	  h2('Error'),
	    p($style, $error),
      end_html;
    
    $resp->code(RC_INTERNAL_SERVER_ERROR);
    $resp->message(status_message($resp->code));
    $resp->header('Content-Type' => 'text/html; charset=UTF-8');
    $resp->content($html);
    return RC_INTERNAL_SERVER_ERROR;
}

sub redirect_handler {
    # $req : HTTP::Request
    # $resp: HTTP::Response
    # $path: リダイレクト先パス文字列
    # $query_hash: クエリ(undef可)
    my ($req, $resp, $path, $query_hash) = @_;
    
    $resp->header(Location => do {
	my $uri = $req->uri->clone;
	$uri->path($path);
	$uri->query(undef);
	$query_hash and
	  $uri->query_form_hash($query_hash);
	$uri->fragment(undef);
	$uri->as_string;
    });
    return RC_SEE_OTHER;
}

sub need_auth_handler {
    # REDFLAG: implement this

    # この畫面でログインも受け付ける。index は CAN_SEE でなければ見られ
    # ない爲。
    return RC_INTERNAL_SERVER_ERROR;
}

1;
