use strict;
use warnings;

package Net::IMP::Adaptor::STREAM2HTTPConn;
use base 'Net::IMP::Base';
use Net::IMP::HTTP; # constants
use Net::IMP;       # constants
use Net::Inspect::L7::HTTP;
use Carp;

use fields (
    'inner_factory',  # factory with IMP_DATA_HTTP interface
    'inner_analyzer', # analyzer with IMP_DATA_HTTP interface
    'http_parser',    # HTTP parser based on Net::Inspect::L7::HTTP
    'gap',            # true when last data where a gap, per dir
    'buf',            # data received but not processed by http_parser, per dir
);

sub new_factory {
    my ($class,%args) = @_;
    my $factory = fields::new($class);
    $factory->{inner_factory} = $args{factory};
    $factory->{inner_factory}->set_interface([ IMP_DATA_HTTP, undef ])
	or croak("inner interface does not support http data");
    return  $factory;
}

sub INTERFACE {
    my $factory = shift;
    my @if;
    for my $if ( $factory->get_interface ) {
	my ($dt,$rt) = @$if;
	push @if, [ IMP_DATA_STREAM, $rt ] if $dt == IMP_DATA_HTTP;
    }
    return @if;
}

sub new_analyzer {
    my ($factory,%args) = @_;
    my $analyzer = fields::new(ref($factory));
    %$analyzer = %$factory;

    $analyzer->{inner_analyzer} = $factory->{inner_factory}->new_analyzer(%args);
    $analyzer->{http_parser} = Net::IMP::Adaptor::STREAM2HTTPConn::Conn
	->new(Net::IMP::Adaptor::STREAM2HTTPConn::Request->new)
	->new_connection($args{meta} || {},$analyzer);
    $analyzer->{gap}     = [0,0];
    $analyzer->{buf}     = ['',''];

    return $analyzer;
}

sub data {
    my ($analyzer,$dir,$data,$offset,$type) = @_;
    $type == IMP_DATA_STREAM or 
	croak("invalid type in ${analyzer}::data - $type");

    if ( $offset ) {
	my $gap = $offset - $analyzer->{http_parser}->offset($dir);
	$analyzer->{http_parser}->in($dir,{ gap => $gap });
    }

    $analyzer->{buf}[$dir] .= $data;
    my $processed = $analyzer->{http_parser}->in(
	$dir,$analyzer->{buf}[$dir], $data eq '');
    substr($analyzer->{buf}[$dir],0,$processed,'') if $processed;
}

for my $sub (qw(set_callback poll_results add_results run_callback)) {
    no strict 'refs';
    *$sub = eval "sub { shift->{inner_analyzer}->$sub(\@_); }";
}

sub tell {
    my ($analyzer,$dir) = @_;
    return $analyzer->{http_parser}->offset($dir);
}



# callback from Net::IMP::Adaptor::STREAM2HTTPConn::Request
sub _data {
    my ($analyzer,$dir,$data,$type) = @_;

    if ( ref $data ) { # gap
	my $gapsize = $data->{gap} or die "invalid gapsize";
	$type < 0 or croak("gaps not supported for type $type");
	$analyzer->{gap}[$dir] = 1;
	return;
    }

    if ( $analyzer->{gap}[$dir] ) {
	$analyzer->{gap}[$dir] = 0;
	return $analyzer->{inner_analyzer}->data(
	    $dir,$data,$analyzer->tell($dir)+length($data),$type);
    } else {
	return $analyzer->{inner_analyzer}->data($dir,$data,0,$type);
    }
}


###########################################################################
# interface as request object, called from Net::Inspect::L7::HTTP
# this gets translated to the internal interface, which then calls the
# methods of the official Net::IMP::HTTP::Base API
###########################################################################

package Net::IMP::Adaptor::STREAM2HTTPConn::Conn;
use base 'Net::Inspect::L7::HTTP';
use fields qw(analyzer);

use Scalar::Util 'weaken';

sub new_connection {
    my ($self,$meta,$analyzer) = @_;
    my $obj = $self->SUPER::new_connection($meta) or return;
    weaken($obj->{analyzer} = $analyzer);
    return $obj;
}

package Net::IMP::Adaptor::STREAM2HTTPConn::Request;
use base 'Net::Inspect::Flow';
use fields qw(conn meta);

use Scalar::Util 'weaken';
use Net::IMP;
use Net::IMP::HTTP; # constants
use Carp;

sub new_request {
    my ($self,$meta,$conn) = @_;
    my $obj = $self->new;
    weaken( $obj->{conn} = $conn );
    $obj->{meta} = $meta;
    return $obj;
}

sub in_request_header {
    my ($self,$hdr) = @_;
    $self->{conn}{analyzer}->_data(0,$hdr,IMP_DATA_HTTP_HEADER);
}

sub in_request_body {
    my ($self,$data,$eof) = @_;
    $self->{conn}{analyzer}->_data(0,$data,IMP_DATA_HTTP_BODY);
    $self->{conn}{analyzer}->_data(0,'',IMP_DATA_HTTP_BODY) 
	if $eof and $data ne '';
}

sub in_response_header {
    my ($self,$hdr) = @_;
    $self->{conn}{analyzer}->_data(1,$hdr,IMP_DATA_HTTP_HEADER);
}

sub in_response_body {
    my ($self,$data,$eof) = @_;
    $self->{conn}{analyzer}->_data(1,$data,IMP_DATA_HTTP_BODY);
    $self->{conn}{analyzer}->_data(1,'',IMP_DATA_HTTP_BODY) 
	if $eof and $data ne '';
}

sub in_chunk_header {
    my ($self,$hdr) = @_;
    $self->{conn}{analyzer}->_data(1,$hdr,IMP_DATA_HTTP_CHKHDR);
}

sub in_chunk_trailer {
    my ($self,$trailer) = @_;
    $self->{conn}{analyzer}->_data(1,$trailer,IMP_DATA_HTTP_CHKTRAILER);
}

sub in_data {
    my ($self,$dir,$data,$eof) = @_;
    $self->{conn}{analyzer}->_data($dir,$data,IMP_DATA_HTTP_DATA);
    $self->{conn}{analyzer}->_data($dir,'',IMP_DATA_HTTP_DATA) 
	if $eof and $data ne '';
}

sub in_junk {
    my ($self,$dir,$data,$eof) = @_;
    return $self->{conn}{analyzer}->_data($dir,$data,IMP_DATA_HTTP_JUNK);
    return $self->{conn}{analyzer}->_data($dir,'',IMP_DATA_HTTP_JUNK) 
	if $eof and $data ne '';
}

sub fatal {
    my ($self,$reason) = @_;
    $self->{conn}{analyzer}->run_callback([ IMP_DENY,0,$reason ]);
}

1;
__END__

=head1 NAME 

Net::IMP::Adaptor::STREAM2HTTPConn - translate IMP_DATA_STREAM data type to HTTP
connection data types

=head1 SYNOPSIS

    # use automatically
    package myHTTP_IMP_Plugin;
    use base 'Net::IMP';
    sub INTERFACE { return (
	[ IMP_DATA_HTTP, \@rtypes ],
	# automatically insert adaptor if we need to use stream data
	[ IMP_DATA_STREAM, \@rtypes, 'Net::IMP::Adaptor::STREAM2HTTPConn' ],
    )}

    # or by hand
    my $stream_factory = Net::IMP::Adaptor::STREAM2HTTPConn->new_factory;
    my $http_factory = Net::IMP::HTTP::someAnalyzer->new_factory;
    # this will create inner analyzer by calling $http_factory->new_analyzer
    my $analyzer = $stream_factory->new_analyzer(
	factory => $http_factory
    );

=head1 DESCRIPTION

This module translates between IMP_DATA_STREAM data type and HTTP connection
specific data types as defined in L<Net::IMP::HTTP> by interpreting the stream
as HTTP requests with the help of L<Net::Inspect::L7::HTTP>.

It works like a normal IMP plugin understanding only IMP_DATA_STREAM types.
C<new_analyzer> gets an argument C<factory> with the factory object for the
inner analyzer.