use strict;
use warnings;
no if $] >= 5.017011, warnings => 'experimental::smartmatch';

package Net::IMP::Filter;
use Net::IMP;
use Net::IMP::Debug;
use Hash::Util 'lock_ref_keys';
use Scalar::Util 'weaken';


############################################################################
#  these need to be redefined in subclass
############################################################################
# analyzed data output
sub out {
    my ($self,$dir,$data) = @_;
    return;
}

sub deny {
    my ($self,$msg,$dir) = @_;
    $DEBUG && debug("deny $msg");
    return;
}

sub fatal {
    my ($self,$msg) = @_;
    $DEBUG && debug("fatal $msg");
    return;
}

sub log {
    my ($self,$level,$msg,$dir,$offset,$len) = @_;
    $DEBUG && debug("log [$level] $msg");
    return;
}

sub acctfld {
    my ($self,$key,$value) = @_;
    $DEBUG && debug("acctfld $key=$value");
    return;
}

############################################################################
#  Implementation
############################################################################
sub new {
    my ($class,$imp,%args) = @_;
    my $self = lock_ref_keys( bless {
	%args,
	imp   => $imp,    # analyzer object
	buf   => [
	    # list of buffered data [ offset,buf,type ] per dir
	    # buffers for same streaming type will be concatenated
	    [ [0,'',0] ],
	    [ [0,'',0] ],
	],
	pass    => [0,0], # may pass up to this offset
	prepass => [0,0], # may prepass up to this offset
	skipped => [0,0], # flag if last data got not send to analyzer
			  # because of pass into future
	eof     => [0,0], # flag if eof received
	dead    => 0,     # set if deny|fatal received
    },(ref $class || $class) );

    if ($imp) {
	weaken( my $weak = $self );
	$imp->set_callback(\&_imp_cb,$weak);
    }
    return $self;
}

# data into analyzer
sub in {
    my ($self,$dir,$data,$type) = @_;
    $self->{dead} and return;

    $type ||= IMP_DATA_STREAM;
    $DEBUG && debug("in($dir,$type) %d bytes",length($data));

    $self->{eof}[$dir] = 1 if $data eq '';
    return $self->out($dir,$data,$type) if ! $self->{imp};

    my $buf = $self->{buf}[$dir];

    # (pre)pass as much as possible
    for my $w (qw(pass prepass)) {
	my $maxoff = $self->{$w}[$dir] or next;
	@$buf == 1 and ! $buf->[0][2] or die "buf should be empty";
	if ( $maxoff == IMP_MAXOFFSET
	    or $maxoff > $buf->[-1][0] + length($data) ) {
	    $DEBUG && debug("can $w everything");
	    my $lastoff = $self->{skipped}[$dir] && $buf->[0][0];
	    $buf->[0][0] += length($data);
	    $self->out($dir,$data,$type);
	    if ($w eq 'prepass') {
		$self->{imp}->data($dir,$data,$lastoff,$type);
		$self->{skipped}[$dir] = 0;
	    } elsif ( $data eq '' and $maxoff != IMP_MAXOFFSET ) {
		$self->{imp}->data($dir,$data,$lastoff,$type);
		$self->{skipped}[$dir] = 0;
	    } else {
		$self->{skipped}[$dir] = 1;
	    }
	    return;
	}

	my $canfw = $maxoff - $buf->[-1][0];
	if ( $type > 0 and $canfw != length($data)) {
	    # packet types need to be handled as a single piece
	    debug("partial $w for $type ignored");
	    next;
	}

	$DEBUG && debug("can $w %d bytes of %d", $canfw, length($data));
	my $fwd = substr($data,0,$canfw,'');
	my $lastoff = $self->{skipped}[$dir] && $buf->[0][0];
	$buf->[0][0] += length($fwd);
	$self->{$w}[$dir] = 0; # no more (pre)pass
	$self->out($dir,$fwd,$type);
	if ($w eq 'prepass') {
	    $self->{imp}->data($dir,$fwd,$lastoff,$type);
	    $self->{skipped}[$dir] = 0;
	} else {
	    $self->{skipped}[$dir] = 1;
	}
    }

    # data left which need to be forwarded to analyzer
    if ( ! $buf->[-1][2] ) {
	# replace empty (untyped) buffer with new data
	$buf->[-1][1] = $data;
	$buf->[-1][2] = $type;
    } elsif ( $type < 0 and $buf->[-1][2] == $type ) {
	# streaming data of same type can be added to current buffer
	$buf->[-1][1] .= $data;
    } else {
	# need new buffer
	push @$buf,[
	    $buf->[-1][0] + length($buf->[-1][1]),  # base = end of last
	    $data,
	    $type
	];
    }

    $DEBUG && debug("buffer and analyze %d bytes of data", length($data));
    my $lastoff = $self->{skipped}[$dir] && $buf->[0][0];
    $self->{imp}->data($dir,$data,$lastoff,$type);
    $self->{skipped}[$dir] = 0;
}

# callback from analyzer
sub _imp_cb {
    my $self = shift;
    $self->{dead} and return;

    my @fwd;
    for my $rv (@_) {
	my $rtype = shift(@$rv);
	$DEBUG && debug("$rtype ".join(" ",map { "'$_'" } @$rv));

	if ( $rtype == IMP_DENY ) {
	    my ($dir,$msg) = @$rv;
	    $self->deny($msg,$dir);
	    $self->{dead} = 1;
	    return;
	} elsif ( $rtype == IMP_FATAL ) {
	    my $reason = shift;
	    $self->fatal($reason);
	    $self->{dead} = 1;
	    return;

	} elsif ( $rtype == IMP_LOG ) {
	    my ($dir,$offset,$len,$level,$msg) = @$rv;
	    $self->log($level,$msg,$dir,$offset,$len);

	} elsif ( $rtype == IMP_ACCTFIELD ) {
	    my ($key,$value) = @$rv;
	    $self->acctfld($key,$value);

	} elsif ( $rtype ~~ [ IMP_PASS, IMP_PREPASS ] ) {
	    my ($dir,$offset) = @$rv;
	    $DEBUG && debug("got %s %d|%d", $rtype,$dir,$offset);

	    if ( $self->{pass}[$dir] == IMP_MAXOFFSET ) {
		next; # cannot get better than previous pass
	    } elsif ( $rtype == IMP_PASS ) {
		if ( $offset == IMP_MAXOFFSET ) {
		    $self->{pass}[$dir] = $offset;
		    $self->{prepass}[$dir] = 0;
		} elsif ( $offset > $self->{pass}[$dir] ) {
		    $self->{pass}[$dir] = $offset;
		    $self->{prepass}[$dir] = 0
			if $offset >= $self->{prepass}[$dir];
		} else {
		    next; # not better than previous pass
		}

	    # IMP_PREPASS
	    } elsif ( $offset == IMP_MAXOFFSET or (
		$offset > $self->{pass}[$dir] and
		$offset > $self->{prepass}[$dir] )) {
		# update for prepass
		$self->{prepass}[$dir] = $offset
	    } else {
		# next; # no better than previous prepass
	    }

	    my $buf = $self->{buf}[$dir];
	    my $end;

	    while ($buf->[0][2]) {
		my $buf0 = shift(@$buf);
		$end = $buf0->[0] + length($buf0->[1]);
		if ( $offset == IMP_MAXOFFSET
		    or $offset >= $end ) {
		    $DEBUG && debug("pass complete buf");
		    push @fwd, [ $dir, $buf0->[1] ];
		    # keep dummy in buf
		    if ( ! @$buf ) {
			unshift @$buf,[ $buf0->[0] + length($buf0->[1]),'',0 ];
			push @fwd,[$dir,''] if $self->{eof}[$dir]; # fwd eof
			last;
		    }
		} elsif ( $offset <  $buf0->[0] ) {
		    $DEBUG && debug("duplicate $rtype $offset ($buf0->[0])");
		    unshift @$buf,$buf0;
		    last;
		} elsif ( $offset ==  $buf0->[0] ) {
		    # at border, e.g. forward 0 bytes
		    unshift @$buf,$buf0;
		    last;
		} elsif ( $buf0->[2] < 0 ) {
		    # streaming type, can pass part of buf
		    $DEBUG && debug("pass part of buf");
		    push @fwd, [
			$dir,
			substr($buf0->[1],0,$offset - $end,'')
		    ];
		    # put back with adjusted offset
		    $buf0->[0] = $offset;
		    unshift @$buf, $buf0;
		    last;
		} else {
		    $DEBUG && debug(
			"ignore partial $rtype for $buf0->[2] (offset=$offset,pos=$buf0->[0])");
		    unshift @$buf, $buf0; # put back
		    last;
		}
	    }

	    if ( $offset != IMP_MAXOFFSET and $offset <= $end ) {
		# limit reached, reset (pre)pass
		$self->{ $rtype == IMP_PASS ? 'pass':'prepass' }[$dir] = 0;
	    }

	} elsif ( $rtype == IMP_REPLACE ) {
	    my ($dir,$offset,$newdata) = @$rv;
	    $DEBUG && debug("got %s %d|%d", $rtype,$dir,$offset);

	    if ( $self->{pass}[$dir] or $self->{prepass}[$dir] ) {
		# we are allowed to (pre)pass in future, so we cannot replace
		die "cannot replace already passed data";
	    }

	    my $buf = $self->{buf}[$dir];
	    my $buf0 = $buf->[0];
	    my $eob = $buf0->[0] + length($buf0->[1]);
	    if ( $eob < $offset ) {
		die "replacement cannot span different types or packets";
	    } elsif ( $eob == $offset ) {
		# full replace
		$DEBUG && debug("full replace");
		push @fwd,[ $dir,$newdata ];
		shift(@$buf);
		push @$buf, [ $eob,'',0 ] if ! @$buf;
	    } else {
		die "no partial replacement for packet types allowed"
		    if $buf0->[2]>0;
		$DEBUG && debug("partial replace");
		push @fwd,[ $dir,$newdata ];
		substr( $buf0->[1],0,$offset - $buf0->[0],'');
		$buf0->[0] = $offset;
	    }

	} elsif ( $rtype ~~ [ IMP_PAUSE, IMP_CONTINUE ] ) {
	    # ignore
	} else {
	    die "cannot handle Net::IMP rtype $rtype";
	}
    }
    $self->out(@$_) for (@fwd);
}


1;
__END__

=head1 NAME

Net::IMP::Filter - simple data filter using Net::IMP analyzers

=head1 SYNOPSIS

    package myFilter;
    use base 'Net::IMP::Filter';
    sub out {
	my ($self,$dir,$data) = @_;
	print "[$dir] $data\n";
    }

    package main;
    use Net::IMP::Pattern;
    my $factory = Net::IMP::Pattern->new_factory...;
    my $f = myFilter->new( $factory->new_analyzer );
    ..
    $f->in(0,$data0);
    $f->in(1,$data1);
    ..

=head1 DESCRIPTION

C<Net::IMP::Filter> is a class which can be used for simple filters (e.g. data
in, data out) using Net::IMP analyzers, thus hiding the complexity but also
useful features of the Net::IMP interface for simple use cases.
To create such a filter subclass from C<Net::IMP::Filter> and implement any of
the following methods (which by default do nothing):

=over 4

=item out($self,$dir,$data)

this gets called for output of data

=item deny($self,$msg,$dir)

this gets called on IMP_DENY

=item fatal($self,$msg)

this gets called on IMP_FATAL

=item log($self,$level,$msg,$dir,$offset,$len)

this gets called on IMP_LOG

=item acctfld($self,$key,$value)

this gets called on IMP_ACCTFIELD

=back

To use the module the following subroutines are defined

=over 4

=item new($class,$factory,%args) 

This function creates a new filter object. 
C<%args> will be put as keys into the objects hash and thus be available to the
methods described above.

=item in($self,$dir,$data,$type)

This method puts new data C<$data> for direction C<$dir> with type C<$type> into
the filter object, which will then send it to the analyzer and finally result in
calls to C<out>, C<deny>, C<log> etc.

=back

=head1 AUTHOR

Steffen Ullrich <sullr@cpan.org>

=head1 COPYRIGHT

Copyright by Steffen Ullrich.

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