package Catalyst::Engine::HTTP::POE;

use strict;
use warnings;
use base 'Catalyst::Engine::HTTP';
use Data::Dump qw(dump);
use HTTP::Body;
use HTTP::Date ();
use HTTP::Headers;
use HTTP::Response;
use HTTP::Status ();
use POE;
use POE::Filter::Line;
use POE::Filter::Stream;
use POE::Wheel::ReadWrite;
use POE::Wheel::SocketFactory;
use Socket;
use Time::HiRes;

use Catalyst::Engine::HTTP::Restarter::Watcher;

our $VERSION = '0.06';

# Enable for helpful debugging information
sub DEBUG () { $ENV{CATALYST_POE_DEBUG} || 0 }
sub BENCH () { $ENV{CATALYST_POE_BENCH} || 0 }

# Max processes (including parent)
sub MAX_PROC () { $ENV{CATALYST_POE_MAX_PROC} || 1 }

# Keep-alive connection timeout in seconds
sub KEEPALIVE_TIMEOUT () { 300 }

# Benchmark::Stopwatch for profiling
if ( BENCH ) {
    require Benchmark::Stopwatch;
}

$SIG{__WARN__} = sub {
    warn '[' . sprintf( "%.4f", Time::HiRes::time() ) . '] ' . shift;
};

sub run { 
    my ( $self, $class, @args ) = @_;
    
    $self->spawn( $class, @args );
    
    POE::Kernel->run;
}

sub spawn {
    my ( $self, $class, $port, $host, $options ) = @_;
    
    my $addr = $host ? inet_aton($host) : INADDR_ANY;
    if ( $addr eq INADDR_ANY ) {
        require Sys::Hostname;
        $host = lc Sys::Hostname::hostname();
    }
    else {
        $host = gethostbyaddr( $addr, AF_INET ) || inet_ntoa($addr);
    }
    
    $self->{config} = {
        appclass   => $class,
        addr       => $addr,
        port       => $port,
        host       => $host,
        options    => $options,
        children   => {},
        is_a_child => 0,
    };
    
    POE::Session->create(
        object_states => [
            $self => [
                qw/_start
                   _stop
                   shutdown
                   child_shutdown
                   dump_state
                   
                   prefork
                   sig_chld
                   
                   check_restart
                   restart
                   
                   accept_new_client
                   accept_failed

                   client_flushed
                   client_error
                   
                   read_input
                   process_input
                   process

                   handle_prepare
                   prepare_done

                   handle_finalize
                   finalize_done
                   client_done
                   
                   keepalive_timeout
               /
           ],
       ],
   );
   
   return $self;
}

# start the server
sub _start {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];
    
    $kernel->alias_set( 'catalyst-poe' );

    # take a copy of %ENV
    $self->{global_env} = \%ENV;
    
    $self->{listener} = POE::Wheel::SocketFactory->new(
         ( defined ( $self->{config}->{addr} ) 
            ? ( BindAddress => $self->{config}->{addr} ) 
            : () 
         ),
         ( defined ( $self->{config}->{port} ) 
            ? ( BindPort => $self->{config}->{port} ) 
            : ( BindPort => 3000 ) 
         ),
         SuccessEvent   => 'accept_new_client',
         FailureEvent   => 'accept_failed',
         SocketDomain   => AF_INET,
         SocketType     => SOCK_STREAM,
         SocketProtocol => 'tcp',
         Reuse          => 'on',
    );

    # dump our state if we get a SIGUSR1
    $kernel->sig( USR1 => 'dump_state' );

    # shutdown on INT
    $kernel->sig( INT => 'shutdown' );
    
    # Pre-fork if requested
    $self->{config}->{options}->{max_proc} ||= MAX_PROC;
    if ( $self->{config}->{options}->{max_proc} > 1 ) {
        $kernel->sig( CHLD => 'sig_chld' );
        $kernel->yield( 'prefork' );
    }
    
    # Init restarter
    if ( $self->{config}->{options}->{restart} ) {
        my $delay = $self->{config}->{options}->{restart_delay} || 1;
        $kernel->delay_set( 'check_restart', $delay );
    }
    
    my $url = 'http://' . $self->{config}->{host};
    $url .= ':' . $self->{config}->{port}
        unless $self->{config}->{port} == 80;

    print "You can connect to your server at $url\n";
}

sub _stop { }

sub shutdown {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];
    
    DEBUG && warn "Shutting down...\n";
    
    if ( my @children = keys %{ $self->{config}->{children} } ) {
        DEBUG && warn "Signaling all children to stop...\n";
        kill INT => @children;
    }
    
    delete $self->{listener};
    delete $self->{clients};
    
    $kernel->alias_remove( 'catalyst-poe' );
    
    return 1;
}

sub child_shutdown {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];

    return 0;
}

sub dump_state {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];

    my $clients = scalar keys %{ $self->{clients} };
    warn "-- POE Engine State --\n";
    warn dump( $self );
    warn "Active clients: $clients\n";
}

sub prefork {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];
    
    return if $self->{config}->{is_a_child};
    
    my $max_proc = $self->{config}->{options}->{max_proc};
    
    DEBUG && warn 'Preforking ' . ( $max_proc - 1 ) . " children...\n";
    
    my $current_children = keys %{ $self->{config}->{children} };
    for ( $current_children + 2 .. $max_proc ) {
        
        my $pid = fork();
        
        unless ( defined $pid ) {
            DEBUG && warn "Server $$ fork failed: $!\n";
            $kernel->delay_set( prefork => 1 );
            return;
        }
        
        # Parent.  Add the child process to its list.
        if ( $pid ) {
            $self->{config}->{children}->{$pid} = 1;
            next;
        }
        
        # Child.  Clear the child process list.
        DEBUG && warn "Child $$ forked successfully.\n";
        $self->{config}->{is_a_child} = 1;
        $self->{config}->{children}   = {};
        
        $kernel->sig( INT => 'child_shutdown' );
        
        return;
    }
}

sub sig_chld {
    my ( $kernel, $self, $child_pid ) = @_[ KERNEL, OBJECT, ARG1 ];

    if ( delete $self->{config}->{children}->{$child_pid} ) {
        DEBUG && warn "Server $$ received SIGCHLD from $child_pid.\n";
        $kernel->yield( 'prefork' );
    }
    return 0;
}

sub check_restart {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];
    
    # Only check in the parent process
    return if $self->{config}->{is_a_child};
    
    my $options = $self->{config}->{options};
    
    # Init watcher object with no delay
    if ( !$self->{watcher} ) {        
        $self->{watcher} = Catalyst::Engine::HTTP::Restarter::Watcher->new(
            directory => ( 
                $options->{restart_directory} || 
                File::Spec->catdir( $FindBin::Bin, '..' )
            ),
            regex     => $options->{restart_regex},
            # current Cat versions will 'sleep 1' if this is 0
            delay     => 0.00000000001,
        );
    }
    
    my @changed_files = $self->{watcher}->watch();
    
    # Restart if any files have changed
    if (@changed_files) {
        my $files = join ', ', @changed_files;
        print STDERR qq/File(s) "$files" modified, restarting\n\n/;
        
        $kernel->yield( 'restart' );
    }
    else {
        # Schedule next check
        $kernel->delay_set( 'check_restart', $options->{restart_delay} );
    }
}

sub restart {
    my ( $kernel, $self ) = @_[ KERNEL, OBJECT ];
    
    $kernel->call( 'catalyst-poe', 'shutdown' );
    
    ### if the standalone server was invoked with perl -I .. we will loose
    ### those include dirs upon re-exec. So add them to PERL5LIB, so they
    ### are available again for the exec'ed process --kane
    use Config;
    $ENV{PERL5LIB} .= join $Config{path_sep}, @INC;
    
    my $options = $self->{config}->{options};
    exec $^X . ' "' . $0 . '" ' . join( ' ', @{ $options->{argv} } );
}

sub accept_new_client {
    my ( $kernel, $self, $socket, $peeraddr, $peerport ) 
        = @_[ KERNEL, OBJECT, ARG0 .. ARG2 ];
    
    my $stopwatch;
    if ( BENCH ) {
        $stopwatch = Benchmark::Stopwatch->new->start;
    }

    $peeraddr = inet_ntoa($peeraddr);
    
    my $wheel = POE::Wheel::ReadWrite->new(
        Handle       => $socket,
        Filter       => POE::Filter::Stream->new,
        FlushedEvent => 'client_flushed',
        ErrorEvent   => 'client_error',
        HighMark     => 128 * 1024,
        HighEvent    => sub {}, # useless, never gets called
        LowMark      => 8 * 1024,
        LowEvent     => sub {}, # also useless, we can use FlushedEvent
    );

    # get the local connection information
    my $local_sockaddr = getsockname($socket);
    my ( undef, $localiaddr ) = sockaddr_in($local_sockaddr);
    my $localaddr = inet_ntoa($localiaddr) || '127.0.0.1';
    my $localname = gethostbyaddr( $localiaddr, AF_INET ) || 'localhost';
    
    my $ID = $wheel->ID;
    
    $self->{clients}->{$ID} = {
        wheel     => $wheel,
        socket    => $socket,
        peeraddr  => $peeraddr,
        peerport  => $peerport,
        localaddr => $localaddr,
        localname => $localname,
        
        requests  => 0,
        inputbuf  => '',
        written   => 0,
        
        stopwatch => $stopwatch,
    };
    
    DEBUG && warn "[$ID] [$$] New connection (wheel $ID from $peeraddr:$peerport)\n";

    # Wait for some data to read
    $poe_kernel->select_read( $socket, 'read_input', $ID );
}

sub accept_failed {
    my ( $kernel, $self, $op, $errnum, $errstr ) = @_[ KERNEL, OBJECT, ARG0 .. ARG2 ];
    
    warn "Unable to start server: $op error $errnum: $errstr\n";
    
    $kernel->yield('shutdown');
}

sub client_error {
    my ( $kernel, $self, $op, $errnum, $errstr, $ID ) = @_[ KERNEL, OBJECT, ARG0 .. ARG3 ];
    
    DEBUG && warn "[$ID] [$$] Wheel generated $op error $errnum: $errstr\n";
    
    delete $self->{clients}->{$ID};
}

sub read_input {
    my ( $kernel, $self, $handle, $ID ) = @_[ KERNEL, OBJECT, ARG0, ARG2 ];
    
    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap('read_input');
    
    # Clear the keepalive timeout timer if set
    if ( my $timer = delete $client->{_timeout_timer} ) {
        $kernel->alarm_remove( $timer );
    }
    
    # Read some data from the driver
    my $driver = $client->{wheel}->[ $client->{wheel}->DRIVER_BOTH ];
    my $buffer_ref = $driver->get( $handle );
        
    if ( !$buffer_ref ) {
        # Error, stop reading and shut down this client
        DEBUG && warn "[$ID] [$$] Error reading, disconnecting\n";
        $kernel->select_read( $handle );
        delete $self->{clients}->{$ID};
        return;
    }
        
    $client->{inputbuf} .= join '', @{$buffer_ref};
        
    DEBUG && warn "[$ID] [$$] read_input (" . length( $client->{inputbuf} ) . " bytes in buffer)\n";
    
    $kernel->yield( 'process_input', $ID );
}

sub process_input {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];
    
    my $client = $self->{clients}->{$ID} || return;
    
    # Have we started processing the body?
    if ( exists $client->{_read} ) {
        
        _process_chunk( $client );
                
        return;
    }
    
    # Have we already parsed headers
    return if $client->{_headers};
    
    # Have we read enough to include all headers?
    if ( $client->{inputbuf} =~ /(\x0D\x0A?\x0D\x0A?|\x0A\x0D?\x0A\x0D?)/s ) {
        $client->{_headers} = 1;

        # Copy the buffer for header parsing, and remove the header block
        # from the content buffer.
        my $buf = $client->{inputbuf};
        $client->{inputbuf} =~ s/.*?(\x0D\x0A?\x0D\x0A?|\x0A\x0D?\x0A\x0D?)//s;
        
        # Parse the request line.
        if ( $buf !~ s/^(\w+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\012]*\012// ) {
            # Invalid request
            DEBUG && warn "[$ID] [$$] Bad request: $buf\n";

            my $status   = 400;
            my $message  = HTTP::Status::status_message($status);
            my $response = HTTP::Response->new( $status => $message );
            $response->content_type( 'text/plain' );
            $response->content( "$status $message" );
            # XXX: fix to use CRLF
            $client->{wheel}->put( $response->as_string );
            return;
        }
        
        my $method = $1;
        my $uri    = $2;
        my $proto  = $3 || 'HTTP/0.9';
        
        DEBUG && warn "[$ID] [$$] process_input: Parsing headers ($method $uri $proto)\n";
        
        # Initialize CGI environment
        my ( $path, $query_string ) = split /\?/, $uri, 2;
        my %env = (
            PATH_INFO       => $path         || '',
            QUERY_STRING    => $query_string || '',
            REMOTE_ADDR     => $client->{peeraddr},
            REMOTE_HOST     => $client->{peeraddr},
            REQUEST_METHOD  => $method || '',
            SERVER_NAME     => $client->{localname},
            SERVER_PORT     => $self->{config}->{port},
            SERVER_PROTOCOL => $proto,
            %{ $self->{global_env} },
        );
        
        # Parse headers
        my $headers = HTTP::Headers->new;
        my ($key, $val);
        HEADER:
        while ( $buf =~ s/^([^\012]*)\012// ) {
            $_ = $1;
            s/\015$//;
            if ( /^([\w\-~]+)\s*:\s*(.*)/ ) {
                $headers->push_header( $key, $val ) if $key;
                ($key, $val) = ($1, $2);
            }
            elsif ( /^\s+(.*)/ ) {
                $val .= " $1";
            }
            else {
                last HEADER;
            }
        }
        $headers->push_header( $key, $val ) if $key;
        
        DEBUG && warn "[$ID] [$$] " . dump($headers) . "\n";

        # Convert headers into ENV vars
        $headers->scan( sub {
            my ( $key, $val ) = @_;
            
            $key = uc $key;
            $key = 'COOKIE' if $key eq 'COOKIES';
            $key =~ tr/-/_/;
            $key = 'HTTP_' . $key
                unless $key =~ m/\A(?:CONTENT_(?:LENGTH|TYPE)|COOKIE)\z/;
                
            if ( exists $env{$key} ) {
                $env{$key} .= ", $val";
            }
            else {
                $env{$key} = $val;
            }
        } );
        
        $client->{env} = \%env;
        
        $kernel->yield( 'process', $ID );
    }
}

sub process {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];

    my $class = $self->{config}->{appclass};

    # This request may be executing within another request,
    # so we must localize all of NEXT so it doesn't get confused about what's
    # already been called
    local $NEXT::NEXT{ $class, 'prepare' };
    local $NEXT::NEXT{ $class, 'prepare_request' };
    local $NEXT::NEXT{ $class, 'prepare_connection' };
    local $NEXT::NEXT{ $class, 'prepare_query_parameters' };
    local $NEXT::NEXT{ $class, 'prepare_headers' };
    local $NEXT::NEXT{ $class, 'prepare_cookies' };
    local $NEXT::NEXT{ $class, 'prepare_path' };
    local $NEXT::NEXT{ $class, 'prepare_body' };

    local $NEXT::NEXT{ $class, 'finalize_uploads' };
    local $NEXT::NEXT{ $class, 'finalize_error' };
    local $NEXT::NEXT{ $class, 'finalize_headers' };
    local $NEXT::NEXT{ $class, 'finalize_body' };
    
    # pass flow control to Catalyst
    my $status = $class->handle_request( $ID );
}

# Prepare handles the entire prepare stage so we can yield to each step
sub prepare {
    my ( $self, $c, $ID ) = @_;

    DEBUG && warn "[$ID] [$$] - prepare\n";

    # store our ID in context
    $c->{_POE_ID} = $ID;
    
    my $client = $self->{clients}->{$ID} || return;
    $client->{context} = $c;

    $client->{_prepare_done} = 0;
    
    BENCH && $client->{stopwatch}->lap('prepare');
    
    $poe_kernel->yield( 'handle_prepare', 'prepare_request', $ID );
    $poe_kernel->yield( 'handle_prepare', 'prepare_connection', $ID );
    $poe_kernel->yield( 'handle_prepare', 'prepare_query_parameters', $ID );
    $poe_kernel->yield( 'handle_prepare', 'prepare_headers', $ID );
    $poe_kernel->yield( 'handle_prepare', 'prepare_cookies', $ID );
    $poe_kernel->yield( 'handle_prepare', 'prepare_path', $ID );
    
    # If parse_on_demand is set, prepare_body will be called later
    unless ( $c->config->{parse_on_demand} ) {
        $poe_kernel->yield( 'handle_prepare', 'prepare_body', $ID );
        
         # prepare_body calls prepare_done after reading all data
    }
    else {
        # Parse on demand will call prepare_body later, so we're done with 
        # the rest of the prepare cycle now
        $poe_kernel->yield( 'prepare_done', $ID );
    }

    # Wait until all prepare processing has completed, or we will return too
    # early
    # XXX: Is there a better way to handle this?
    while ( !$client->{_prepare_done} ) {
        $poe_kernel->run_one_timeslice();
    }

    return $c;
}

# handle_prepare localizes our per-client %ENV and calls $c->$method
# Allows plugins to do things during each step
sub handle_prepare {
    my ( $kernel, $self, $method, $ID ) = @_[ KERNEL, OBJECT, ARG0, ARG1 ];
    
    DEBUG && warn "[$ID] [$$] - $method\n";
    
    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap(" - $method");
    
    {
        local (*ENV) = $client->{env};
        $client->{context}->$method();
    }
}

sub prepare_body {
    my ( $self, $c ) = @_;

    my $ID = $c->{_POE_ID};
    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap('prepare_body');
    
    # Initialize the HTTP::Body object
    my $type   = $c->request->header('Content-Type');
    my $length = $c->request->header('Content-Length') 
        || length( $client->{inputbuf} ) 
        || 0;
    
    # Catalyst >= 5.7007 has support for bypassing HTTP::Body object
    if ( !$length && $Catalyst::VERSION ge '5.7007' ) {
        # Defined but will cause all body code to be skipped
        $c->request->{_body} = 0;
        
        $poe_kernel->yield( 'prepare_done', $ID );
        return;
    }

    unless ( $c->request->{_body} ) {
        $c->request->{_body} = HTTP::Body->new( $type, $length );
    }

    if ( !$length ) {
        # Nothing to parse, we're done
        $poe_kernel->yield( 'prepare_done', $ID );
        return;
    }
   
    DEBUG && warn "[$ID] [$$] Processing body data (total length: $length)\n";

    # Read some more data
    $client->{_prepare_body_done} = 0;
    $client->{_read}              = 0;
    $client->{_read_length}       = $length;
    
    # First process any data that is already in our input buffer
    if ( $client->{inputbuf} ) {
        _process_chunk( $client );
    }
    
    # If we have more body data to read, this will be processed in process_input

    # We need to wait until all body data is read before returning
    while ( !$client->{_prepare_body_done} ) {
        $poe_kernel->run_one_timeslice();
    }
    
    $poe_kernel->yield( 'prepare_done', $ID );
}

sub prepare_done {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];
    
    my $client = $self->{clients}->{$ID} || return;
    
    DEBUG && warn "[$ID] [$$] prepare_done\n";
#    DEBUG && warn dump( $self->{clients}->{$ID}->{context} );

    $client->{_prepare_done} = 1;
    
    BENCH && $client->{stopwatch}->lap('prepare_done');
}

# Finalize handles the entire finalize stage
sub finalize {
    my ( $self, $c ) = @_;

    my $ID = $c->{_POE_ID};
    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap('finalize');

    $client->{_finalize_done} = 0;

    $poe_kernel->yield( 'handle_finalize', 'finalize_uploads', $ID );

    if ( $#{ $c->error } >= 0 ) {
        $poe_kernel->yield( 'handle_finalize', 'finalize_error', $ID );
    }

    $poe_kernel->yield( 'handle_finalize', 'finalize_headers', $ID );

    $poe_kernel->yield( 'handle_finalize', 'finalize_body', $ID );

    $poe_kernel->yield( 'finalize_done', $ID );

    while ( !$client->{_finalize_done} ) {
        $poe_kernel->run_one_timeslice();
    }
    
    return $c->response->status;
}

sub handle_finalize {
    my ( $kernel, $self, $method, $ID ) = @_[ KERNEL, OBJECT, ARG0, ARG1 ];
    
    DEBUG && warn "[$ID] [$$] - $method\n";

    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap(" - $method");

    # Set the response body to null when we're doing a HEAD request.
    # Must be done here so finalize_headers can still set the proper
    # Content-Length value
    if ( $method eq 'finalize_body' ) {
        if ( $client->{context}->request->method eq 'HEAD' ) {
            $client->{context}->response->body('');
        }
    }

    $client->{context}->$method();
}

sub finalize_headers {
    my ( $self, $c ) = @_;

    my $client = $self->{clients}->{ $c->{_POE_ID} } || return;
    
    BENCH && $client->{stopwatch}->lap('finalize_headers');
    
    my $protocol = 'HTTP/1.0'; # We're not HTTP/1.1 (yet)
    my $status   = $c->response->status;
    my $message  = HTTP::Status::status_message($status);

    my @headers;
    push @headers, "$protocol $status $message";
    
    $c->response->headers->header( Date => HTTP::Date::time2str(time) );

    # Some notes: I found that to get keepalive mode to perform well under ab,
    # I had to send all data in a single put() call, so the second put in write() below is
    # what caused keepalive to be so slow.  Not sure if this is just a quirk with ab
    # or really a performance problem. :(
    
    # Should we keep the connection open?
    my $connection = $c->request->header('Connection');
    if ( $connection && $connection =~ /^keep-alive$/i ) {
        $c->response->headers->header( Connection => 'keep-alive' );
        $client->{_keepalive} = 1;
    }
    else {
        $c->response->headers->header( Connection => 'close' );
    }
    
    push @headers, $c->response->headers->as_string("\x0D\x0A");
    
    # Buffer the headers so they are sent with the first write() call
    # This reduces the number of TCP packets we are sending
    $client->{_header_buf} = join("\x0D\x0A", @headers, '');
}

sub finalize_done {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];
    
    DEBUG && warn "[$ID] [$$] finalize_done\n";

    my $client = $self->{clients}->{$ID} || return;
    
    # If we did not send our headers yet (we had no body), send them now
    if ( my $headers = delete $client->{_header_buf} ) {
        $client->{wheel}->put( $headers );
    }

    $client->{_finalize_done} = 1;
    
    BENCH && $client->{stopwatch}->lap('finalize_done');
}

sub write {
    my ( $self, $c, $buffer ) = @_;

    my $ID = $c->{_POE_ID};
    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap('write');
    
    # keep track of the amount of data we've sent
    $client->{_written} += length $buffer;
    DEBUG && warn "[$ID] [$$] written: " . $client->{_written} . "\n";
    
    # Add headers to the first write() call
    if ( my $headers = delete $client->{_header_buf} ) {
        $client->{_highmark_reached} = $client->{wheel}->put( $headers . $buffer );
    }
    else {
        $client->{_highmark_reached} = $client->{wheel}->put( $buffer );
    }    

    # if the output buffer has reached the highmark, we have a
    # lot of outgoing data.  Don't return until it's been sent
    while ( $client && $client->{_highmark_reached} ) {
        $poe_kernel->run_one_timeslice();
    }

    # always return 1, we can't detect failures here
    return 1;
}

# client_flushed is called when all data is done being written to the browser
sub client_flushed {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];

    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && $client->{stopwatch}->lap('client_flushed');

    # Are we done writing?
    if ( $client->{context} ) {
        my $cl = $client->{context}->response->content_length;
        if ( $cl && $client->{_written} >= $cl ) {
            DEBUG && warn "[$ID] [$$] client_flushed, written full content-length\n";
            $kernel->yield( 'client_done', $ID );
            return;
        }
    }

    # if we get this event because of the highmark being reached
    # don't clean up but reset the highmark value to 0
    if ( $client->{_highmark_reached} ) {
        $client->{_highmark_reached} = 0;
        return;
    }

    # we may have not had a content-length...
    $kernel->yield( 'client_done', $ID );
}

sub client_done {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];
    
    my $client = $self->{clients}->{$ID} || return;
    
    BENCH && warn "[$ID] [$$] Stopwatch:\n" . $client->{stopwatch}->stop->summary;

    # clean up everything about this client unless we are using keepalive
    if ( $client->{_keepalive} ) {
        DEBUG && warn "[$ID] [$$] client_done, keepalive enabled, waiting for more requests\n";
        $client->{requests}++;
        
        # Clear important variables from the previous state
        delete $client->{_headers};
        delete $client->{_written};
        delete $client->{_read};
        
        if ( BENCH ) {
            $client->{stopwatch} = Benchmark::Stopwatch->new->start;
        }

        # timeout idle connection after some seconds
        $client->{_timeout_timer} = $kernel->delay_set( 'keepalive_timeout', KEEPALIVE_TIMEOUT, $ID );
    }
    else {
        DEBUG && warn "[$ID] [$$] client_done, closing connection\n";
        delete $self->{clients}->{$ID};
    }
}

sub keepalive_timeout {
    my ( $kernel, $self, $ID ) = @_[ KERNEL, OBJECT, ARG0 ];
    
    DEBUG && warn "[$ID] [$$] Timing out idle keepalive connection\n";
    
    delete $self->{clients}->{$ID};
}

# Process a chunk of body data
sub _process_chunk {
    my $client = shift;
    
    # Read no more than content-length
    my $cl = $client->{env}->{CONTENT_LENGTH} || length( $client->{inputbuf} ) || 0;

    my $buf  = substr $client->{inputbuf}, 0, $cl, '';
    my $read = length($buf);
    
    return unless $read;
        
    $client->{context}->prepare_body_chunk( $buf );

    $client->{_read} += $read;
    
    if ( DEBUG ) {
        my $ID   = $client->{wheel}->ID;
        my $togo = $client->{_read_length} - $client->{_read};
        warn "[$ID] [$$] prepare_body: Read $read bytes ($togo to go)\n";
    }
    
    # Is that all the body data?
    if ( $client->{_read} >= $client->{_read_length} ) {
        # Some browsers (like MSIE 5.01) send extra CRLFs after the content
        # so we need to strip it away
        $client->{inputbuf} =~ s/^\s+//;
        
        $client->{_prepare_body_done} = 1;
    }
}

1;
__END__

=head1 NAME

Catalyst::Engine::HTTP::POE - Single-threaded multi-tasking Catalyst engine

=head1 SYNOPIS

    CATALYST_ENGINE='HTTP::POE' script/yourapp_server.pl
    
    # Prefork 5 children
    CATALYST_POE_MAX_PROC=6 CATALYST_ENGINE='HTTP::POE' script/yourapp_server.pl

=head1 DESCRIPTION

This engine allows Catalyst to process multiple requests in parallel within a
single process.  Much of the internal Catalyst flow now uses POE yield calls.
Application code will still block of course, but all I/O, header processing, and
POST body processing is handled asynchronously.

A good example of the engine's power is the L<Catalyst::Plugin::UploadProgress> demo
application, which can process a file upload as well as an Ajax polling request
at the same time in the same process.

This engine requires at least Catalyst 5.67.

=head1 RESTART SUPPORT

As of version 0.05, the -r flag is supported and the server will restart itself when any
application files are modified.

=head1 PREFORKING

As of version 0.05, the engine is able to prefork a set number of child processes to distribute
requests.  Set the CATALYST_POE_MAX_PROC environment variable to the total number of processes
you would like to run, including the parent process.  So, to prefork 5 children, set this value
to 6.  This value may also be set by modifying yourapp_server.pl and adding max_proc to the
options hash passed to YourApp->run().

=head1 DEBUGGING

To enable trace-level debugging, set the environment variable CATALYST_POE_DEBUG.

At any time you can get a dump of the internal state of the engine by sending a
USR1 signal to the running process.

=head1 EXPERIMENTAL STATUS

This engine should still be considered experimental and likely has bugs,
however as it's only intended for development, please use it and report bugs.

The engine has been tested with the UploadProgress demo, the Streaming example,
and one of my own moderately large applications.  It also fully passes the Catalyst
test suite.

=head1 AUTHOR

Andy Grundman, <andy@hybridized.org>

=head1 COPYRIGHT

This program is free software, you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut
