package POE::Component::Server::HTTP::KeepAlive;
use strict;
use HTTP::Status;
use Carp;

use Exporter ();
use vars qw(@ISA @EXPORT $VERSION);
@ISA = qw(Exporter);

use POE;

$VERSION = "0.0307";

my $N++;

use constant DEBUG => 0;

use Carp;

################################################
sub new 
{
    my $class = shift;
    my $self = bless {@_}, $class;

    unless( defined $self->{total_max} ) {
        $self->{total_max} = 10;
    }

    unless( defined $self->{timeout} ) {
        $self->{timeout} = 60;
    }
    unless( defined $self->{max} ) {
        $self->{max} = 10;
    }
    if( $self->{max} > $self->{total_max} ) {
        $self->{max} = $self->{total_max};
    }

    unless( $self->{http_alias} ) {
        $self->{http_alias} = $self->{http_ID} =
                $POE::Kernel::poe_kernel->get_active_session->ID;
    }

    $self->{connections} = {};
    die "Must have a session alias" unless $self->{http_alias};
    $self->create_events;
    return $self;
}

################################################
sub create_events
{
    my( $self ) = @_;

    my $id = $self;
    $self->{timeout_event} = "$self TIMEOUT";
    $POE::Kernel::poe_kernel->state( $self->{timeout_event}, $self, 'timeout_event' );
    $self->{close_event} = "$self CLOSE";
    $POE::Kernel::poe_kernel->state( $self->{close_event}, $self, 'close_event' );
}

################################################
## A request has started.
sub start
{
    my( $self, $req, $resp ) = @_;
    my $c = $self->conn_from_resp( $resp );
    return unless $c;

    # remove timeout for the connection
    my $ka = $self->get( $self->conn_ID( $c ) );
    return unless $ka;          # this is normal; first req of connection

    # Make sure the connection doesn't timeout while a request is active
    if( $ka->{alarm} ) {
        $POE::Kernel::poe_kernel->alarm_remove( delete $ka->{alarm} );
    }
}

################################################
## A request has ended
## Make a descision about keep-alive
sub finish
{
    my( $self, $req, $resp ) = @_;
    my $c = $self->conn_from_resp( $resp );

    my $id = $self->conn_ID( $c );

# Lifted from apache :
#     *   IF  we have not marked this connection as errored;
    if( !$resp->is_error and
#     *   and the response body has a defined length due to the status code
#     *       being 304 or 204, the request method being HEAD, already
#     *       having defined Content-Length or Transfer-Encoding: chunked, or
#     *       the request version being HTTP/1.1 and thus capable of being set
#     *       as chunked [we know the (r->chunked = 1) side-effect is ugly];
#        ( defined $resp->content_length ) and
#     *   and the server configuration enables keep-alive;
        ( $self->{total_max} > 0 ) and
#     *   and the server configuration has a reasonable inter-request timeout;
        ( $self->{timeout} > 0 ) and
#     *   and there is no maximum no requests or the max hasn't been reached;
        ( $self->{max} <= 0 or $self->{max} > $self->conn_ka( $c ) ) and
#     *   and the response status does not require a close;
        ( not $self->status_close( $resp ) ) and
#     *   and the response generator has not already indicated close;
        ( not $self->connection( $resp, 'close' ) ) and
#     *   and the client did not request non-persistence (Connection: close);
        ( not $self->connection( $req, 'close' ) ) and
#     *   and    we haven't been configured to ignore the buggy twit
#     *       or they're a buggy twit coming through a HTTP/1.1 proxy
        ( 1 ) and # ???
#     *   and    the client is requesting an HTTP/1.0-style keep-alive
#     *       or the client claims to be HTTP/1.1 compliant (perhaps a proxy);
        ( $self->connection( $req, 'keep-alive' ) or $req->protocol eq 'HTTP/1.1' )
#     *   THEN we can be persistent, which requires more headers be output.
#     *
      ) {
        # warn "max=$self->{max} conn_ka=", $self->conn_ka( $c );
        DEBUG and 
            warn "Keepalive: finish keep id=$id";
        $self->keep( $req, $c );
        $self->keep_response( $req, $resp, $c );
        return 1;
    }
    else {
        DEBUG and 
            warn "Keepalive: finish drop id=$id";
        $self->drop( $id );
        $self->drop_response( $req, $resp, $c );
        return;
    }

}

################################################
sub status_close
{
    my( $self, $resp ) = @_;
    my $status = $resp->code;

    return (($status == RC_BAD_REQUEST) or
            ($status == RC_REQUEST_TIMEOUT) or
            ($status == RC_LENGTH_REQUIRED) or
            ($status == RC_REQUEST_ENTITY_TOO_LARGE) or
            ($status == RC_REQUEST_URI_TOO_LARGE) or
            ($status == RC_INTERNAL_SERVER_ERROR) or
            ($status == RC_SERVICE_UNAVAILABLE) or
            ($status == RC_NOT_IMPLEMENTED)
           );
}


################################################
# It turns out the Connection header can contain multiple
# comma separated values
sub connection
{
    my( $self, $r, $keyword ) = @_;
    my $conn = $r->header( 'Connection' );
    return 0 unless $conn;
    $conn = lc $conn;
    return( ( 0 <= index ",$conn,", lc ",$keyword," ) ? 1 : 0 );
}

################################################
sub timeout
{
    my( $self, $req ) = @_;

    my $timeout = $self->{timeout};

    # find out how long the client wants us to keep it open
    my $ka_header = $req->header( 'keep-alive' );
    if( $ka_header and ( $ka_header =~ /^(\d+)$/ or 
                         $ka_header =~ /timeout=(\d+)/ ) ) {
        if( $1 > 0 && $1 < $timeout ) {
            $timeout = $1;
        }
    }
    return $timeout;
}

################################################
## Add headers to HTTP response that marks this conneciton
## as keep-alive
sub keep_response
{
    my( $self, $req, $resp, $c ) = @_;
    
    my $timeout = $self->timeout( $req );

    if( $self->connection( $req, 'keep-alive' ) ) {
        my $left = $self->{max} - $self->conn_ka( $c );
        $left = $self->{total_max} if $self->{total_max} < $left;
        $resp->header( 'Keep-Alive' => "timeout=$timeout, max=$left" );
        my $conn = $resp->header( 'Connection' );
        if( $conn ) {
            unless( $self->connection( $resp, 'Keep-Alive' ) ) {
                $conn .= ",Keep-Alive";
            }
        }
        else {
            $conn = "Keep-Alive";
        }
        $resp->header( Connection => $conn );
        # XXX: a Connection header might be a problem for HTTP/0.9 
    }
}

################################################
## Add headers to HTTP response that marks this conneciton
## as NOT keep-alive
sub drop_response
{
    my( $self, $req, $resp ) = @_;
    

    $resp->remove_header( 'Keep-Alive' );
    unless( $self->connection( $resp, 'close' ) ) {
        my $conn = $resp->header( 'Connection' );
        if( $conn and $conn =~ s/\bKeep-Alive\b/close/i ) {
            # yep yep
        }
        elsif( $conn ) {
            $conn .= ",close";
        }
        else {
            $conn = "close";
        }
        $resp->header( Connection => $conn );
        # XXX: a Connection header might be a problem for HTTP/0.9 
    }
}


################################################
sub keep
{
    my( $self, $req, $c ) = @_;
    my $id = $self->conn_ID( $c );
    DEBUG and 
        warn "Keepalive: Connection id=$id keep";

    # Note that $id shouldn't be in {connection}... start() called
    # ->drop() on it.
    my $ka = { id=>$id, N=>$N++ };
    $self->add( $ka );
    $self->conn_ka_inc( $c );
    $self->conn_on_close( $c, $id );
    $self->enforce;

    DEBUG and
        $self->dump;

    # setup a timeout
    my $timeout = $self->timeout( $req );
    if( $timeout ) {
        $ka->{alarm} = $POE::Kernel::poe_kernel->delay_set( 
                                               $self->{timeout_event},
                                               $timeout, 
                                               $id
                                             );
        DEBUG and 
            warn "Keepalive: timeout for id=$id is alarm=$ka->{alarm}";
    }
}

################################################
## Add an keep-alive struct to the connection list
sub add
{
    my( $self, $ka ) = @_;
    $self->{connections}{ $ka->{id} } = $ka;
}

################################################
## Make sure the connection list doesn't grow to big
sub enforce
{
    my( $self ) = @_;
    return unless $self->{total_max} > 0;

    my $n = keys( %{ $self->{connections} } ) - $self->{total_max};
    return unless $n > 0;

    # find $n connections to drop
    my @remove;
    foreach my $ka ( sort { $a->{N} <=> $b->{N} } 
                    values %{ $self->{connections} } ) {
        push @remove, $ka;
        last if $n == 0+@remove;
    }
    return unless @remove;

    foreach my $ka ( @remove ) {
        # Because ->enforce could be called multiple times before
        # the connection is actually closed, we mark $ka as dropped
        # and don't call ->conn_close more then once
        next if $ka->{drop};
        $ka->{drop} = 1;
        my $drop = $self->conn_get( $ka->{id} );
        $self->conn_close( $drop, $ka->{id} );
    }
}

################################################
## Remove a struct from the connection list
sub remove
{
    my( $self, $id ) = @_;
    return delete $self->{connections}{ $id };
}

################################################
## Find a struct from the connection list
sub get
{
    my( $self, $id ) = @_;
    return $self->{connections}{ $id };
}


################################################
## We want to remove all internal state regarding a connection
## Note, we must not die nor even warn on bad happenings
sub drop
{
    my( $self, $id ) = @_;

    DEBUG and 0 and do {
                warn "Keepalive: Going to drop id=$id";
                $self->dump();
            };

    my $ka = $self->remove( $id );
    unless( $ka ) {
        DEBUG and do {
                warn "Keepalive: Can't find id=$id";
                $self->dump();
            };
        # Note: not finding $id is normal for the first request of a connection
        return;
    }

    DEBUG and 
        warn "Keepalive: drop id=$id alarm=", ($ka->{alarm}||'');
    DEBUG and
            $self->dump;

    if( $ka->{alarm} ) {
        $POE::Kernel::poe_kernel->alarm_remove( delete $ka->{alarm} );
    }

    return;
}

################################################
sub close_event
{
    my( $self, $id ) = @_[OBJECT, ARG0];
    DEBUG and  
        warn "Keepalive: close_event id=$id";
    $self->drop( $id );
}

################################################
sub timeout_event
{
    my( $self, $id ) = @_[OBJECT, ARG0];
    DEBUG and  
        warn "Keepalive: timeout_event id=$id";
    my $c = eval { $self->conn_get( $id ) };

    unless( $c ) {
        warn "Keepalive: timeout_event unknown connection id=$id";
        return;
    }

    my $ka = $self->get( $id );
    unless( $ka ) {
        DEBUG and warn "Keepalive: timeout_event connection id=$id wasn't kept-alive";
        return;
    }
    delete $ka->{alarm};

    # conn_close should provoke a close_event, which then calls ->drop
    return if $self->conn_close( $c, $id );
    # conn_close returning false means the connection was active
    # Which is highly strange...
}

################################################
sub dump
{
    my( $self ) = @_;
    warn "Keepalive: total_max=$self->{total_max} [", 
                ( join ', ', map { "id=$_->{id}" } 
                        sort { $a->{N} <=> $b->{N} } 
                        values %{ $self->{connections} } ), 
            "]";
}


############################################################################
## Here is where we strap on the big boots and stomp all over the object
## encapsulation.  Because, damnit, the HTTP modules don't provide the access
## we need to get our job done
## Look for STOMP for particularly egregarious bits

################################################
## Get the heap of the HTTP session
sub get_heap
{
    my( $self ) = @_;

    # http_alias could be an alias, or a session ID..
    my $session;
    if( $self->{http_ID} ) {
        $session = 
            $POE::Kernel::poe_kernel->ID_id_to_session( $self->{http_ID} );
    }
    else {
        $session = 
            $POE::Kernel::poe_kernel->alias_resolve( $self->{http_alias} );
    }
    croak "Session $self->{http_alias} no longer exists" unless $session;
    return $session->get_heap;
}

################################################
sub conn_ID
{
    my( $self, $c ) = @_;
    return $c->ID;
}

################################################
sub conn_from_resp
{
    my( $self, $resp ) = @_;
    return $resp->connection;
}

################################################
## Get the connection, based on its ID
sub conn_get
{
    my( $self, $id ) = @_;
    my $heap = $self->get_heap;
    if( $heap->{c}->{$id} ) {               # STOMP
        return $heap->{c}->{ $id };
    }
    die "$heap doesn't have id=$id";
    return;
}

################################################
## Get the connection's wheel, based on its ID
sub conn_wheel
{
    my( $self, $id ) = @_;
    my $heap = $self->get_heap;
    if( $heap->{wheels}->{$id} ) {               # STOMP
        return $heap->{wheels}->{ $id };
    }
    die "$heap doesn't have id=$id";
    return;
}

################################################
## Close the connection.  Must provoke an on_close()
sub conn_close
{
    my( $self, $c, $id ) = @_;
    $id ||= $self->conn_ID( $c );

    # tell the httpd poco that the connection is closed
    # We avoid a race condition by making sure the connection isn't active
    unless( $c->{request} ) {
        my $wheel = $self->conn_wheel( $id );
        # Hope this provokes an error event!
        eval { local $^W = 0; 
               shutdown( $wheel->[0], 0 );    # STOMP
             };
        return 1;
    }
    DEBUG and warn "Keepalive: close, but request is active";
    return 0;
}


################################################
## Register an event that is called when the connection is closed by
## the component
sub conn_on_close
{
    my( $self, $c, $id ) = @_;
    $id ||= $self->conn_ID( $c );
    $c->on_close( $self->{close_event}, $id );
}


################################################
## Increment a connection's request count
sub conn_ka_inc
{
    my( $self, $c ) = @_;
    $c->{keepalives}++;             # STOMP
}

################################################
## Return the connections's request count
sub conn_ka
{
    my( $self, $c ) = @_;
    return $c->{keepalives}||0;     # STOMP
}


1;
__END__


=head1 NAME

POE::Component::Server::HTTP::KeepAlive - HTTP keep-alive support

=head1 SYNOPSIS

=head1 DESCRIPTION

=head2 Handlers

=head1 EVENTS

=head1 See Also

Please also take a look at L<POE::Component::Server::HTTP> and
L<POE::Component::Server::SimpleHTTP>.

=head1 TODO

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008-2011 by Philip Gwyn

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.8 or,
at your option, any later version of Perl 5 you may have available.
=head1 AUTHOR

Additional hacking by Philip Gwyn, poe-at-pied.nu

=cut