use Object::Pad;

package Frame::Server::Protocol;

class Frame::Server::Protocol : isa(Net::Async::HTTP::Server::Protocol) :
  does(Frame::Base);

use utf8;
use v5.40;

use Carp;
use Plack::Util;
use Const::Fast;
use List::Util 'any';
use Scalar::Util 'weaken';
use IO::Async::Timer::Countdown;
use HTTP::Parser::XS 'parse_http_request';

const our $MAX_REQUEST_SIZE => 131072;
const our $CHUNK_SIZE       => 64 * 1024;
const our $HEADRE           => qr/^(.*?\r\n)/s;

# const our $PIPELINERE => qr/^(?:GET|HEAD)/;

const our @CHUNKRE => ( qr/^(([0-9a-fA-F]+).*\r\n)/, qr/^\r\n/ );

field $read_timeout : accessor : weak;
field $req_header_timeout : accessor : weak;
field $keep_alive_timeout : accessor : weak;
field $inactivity_timeout : accessor : weak;
field $reset_req_header_timeout = 0;    # Gotta think of a better name for this

method on_read ( $buffref, $eof ) {
    return 0
      if $eof
      || $$buffref !~ $HEADRE
      ;    # TODO: See if this is faster than letting it hit parse_http_request

    if ($reset_req_header_timeout) {
        $self->restart_timeout(qw/req_header read/);
        $reset_req_header_timeout = 0;
    }

    $inactivity_timeout->reset;

    my $readh = $self->read_handle;

    my %env = (
        SERVER_PORT            => $readh->sockport,
        SERVER_NAME            => $readh->sockhost,
        SCRIPT_NAME            => '',
        REMOTE_ADDR            => $readh->peerhost,
        REMOTE_PORT            => $readh->peerport || 0,
        'psgi.version'         => [ 1, 1 ],
        'psgi.errors'          => \*STDERR,
        'psgi.url_scheme'      => $$self{ssl} ? 'https' : 'http',
        'psgi.run_once'        => Plack::Util::FALSE,
        'psgi.multithread'     => Plack::Util::FALSE,
        'psgi.multiprocess'    => Plack::Util::TRUE,
        'psgi.streaming'       => Plack::Util::TRUE,
        'psgi.nonblocking'     => Plack::Util::TRUE,
        'psgix.harakiri'       => Plack::Util::TRUE,
        'psgix.input.buffered' => Plack::Util::TRUE,
        'psgix.io'             => $readh,
        'io.async.loop'        => $self->loop
    );

    my $reqlen = parse_http_request( $$buffref, \%env );

    if ( $reqlen < 0 ) {
        $self->close_now if $reqlen == -1;
        return 0;
    }

    $req_header_timeout->stop;
    $reset_req_header_timeout = 1;
    $keep_alive_timeout->stop;

    $$buffref = substr $$buffref, $reqlen;

    my $req = $self->parent->make_request( $self, \%env );

    $self->write( "HTTP/1.1 100 Continue\r\n\r\n",
        on_write => sub { $$req{bytes_written} += $_[1] } )
      if $env{HTTP_EXPECT} && lc $env{HTTP_EXPECT} eq '100-continue';

    if ( $env{HTTP_TRANSFER_ENCODING}
        && lc delete $env{HTTP_TRANSFER_ENCODING} eq 'chunked' )
    {
        my $chunkbuff = '';
        $env{CONTENT_LENGTH} = 0;

        return sub ( $, $buffref, $eof ) {
            $inactivity_timeout->reset;

            return 0 unless $$buffref =~ s/$CHUNKRE[0]//;
            my ( $trailer, $chunklen ) = ( $1, hex $2 );

            return $self->handle_request( $req, \$chunkbuff, \%env,
                $env{CONTENT_LENGTH} )
              if $chunklen == 0;

            if ( length $$buffref < $chunklen + 2 ) {
                $$buffref = "$trailer$$buffref";
                return 0;
            }

            $chunkbuff .= substr $$buffref, 0, $chunklen, '';
            $$buffref =~ s/$CHUNKRE[1]//;
            $env{CONTENT_LENGTH} += $chunklen;

            1;
        }
    }

    my $cl = $env{CONTENT_LENGTH} // 0;

    sub ( $, $buffref, $eof ) {
        $inactivity_timeout->reset;
        length $$buffref >= $cl
          ? $self->handle_request( $req, $buffref, \%env, $cl )
          : 0;
    }
}

method handle_request ( $req, $buffref, $env, $length ) {
    open my $stdin, '<', \substr $$buffref, 0, $length, '';
    $$env{'psgi.input'} = $stdin;

    push $self->{requests}->@*, $req;
    weaken $self->{requests}[-1];

    $self->parent->_received_request($req);

    undef;
}

method write : override ($data, %params) {
    $inactivity_timeout->reset;
    $self->SUPER::write( $data, %params );
}

method on_closed : override {
    foreach my $name (qw/req_header keep_alive read inactivity/) {
        my $field = "$name\_timeout";
        $self->$field->remove_from_parent;
    }

    $self->SUPER::on_closed;
}

method restart_timeout (@names) {
    foreach my $name (
        scalar @names ? @names : qw/req_header keep_alive read inactivity/ )
    {
        my $field = "$name\_timeout";
        $self->$field->is_running ? $self->$field->reset : $self->$field->start;
    }
}

1
