use strict;
use warnings;

############################################################################
#
#   wrap Net::SIP::NATHelper::Base
#   read commands from socket and propagete them to NATHelper, send
#   replies back
#
############################################################################

package Net::SIP::NATHelper::Server;
use fields qw( helper callbacks cfd commands );

use Net::SIP qw(invoke_callback :debug);
use Net::SIP::NATHelper::Base;
use Storable qw(thaw nfreeze);
use Data::Dumper;

my %default_commands = (
    allocate => sub { shift->allocate_sockets(@_) },
    activate => sub { shift->activate_session(@_) },
    close    => sub { shift->close_session(@_) },
);


############################################################################
# new NAThelper
# Args: ($class,?$helper,@socket)
#  $helper: Net::SIP::NATHelper::Base object, will be created if not given
#  @socket: SOCK_STREAM sockets for communication SIP proxies
# Returns: $self
############################################################################
sub new {
    my $class = shift;
    my $helper;
    if ( @_ && UNIVERSAL::isa( $_[0],'Net::SIP::NATHelper::Base' )) {
	$helper = shift;
    } else {
	$helper = Net::SIP::NATHelper::Base->new;
    }
    my $self = fields::new( $class );
    %$self = (
	helper => $helper,
	callbacks => [],
	cfd => \@_,
	commands => { %default_commands },
    );
    return $self,
}

############################################################################
# read + execute command
# command is transported as [ $cmd,@args ] using Storable::nfreeze
# and reply is transported back using nfreeze too
# Args: $self
# Returns: NONE
############################################################################
sub do_command {
    my Net::SIP::NATHelper::Server $self = shift;
    my $cfd = shift;

    my $sock = $cfd->accept || do {
	DEBUG( 50,"accept failed: $!" );
	return;
    };
    $sock->autoflush;

    read( $sock,my $buf, 4 ) || do {
	DEBUG( 50, "read of 4 bytes len failed: $!" );
	return;
    };
    my $len = unpack( "N",$buf );
    DEBUG( 50, "len=$len" );
    if ( $len > 32768 ) {
	warn( "tooo much data to read, unbelievable len=$len" );
	return;
    }
    read( $sock,$buf, $len ) || do {
	DEBUG( 50,"read of $len bytes failed: $!" );
	return;
    };

    my ($cmd,@args) = eval { @{ thaw( $buf ) } } or do {
	DEBUG( 50,"thaw failed: $@" );
	return;
    };

    DEBUG( 100, "request=".Dumper([$cmd,@args]));
    my $cb = $self->{commands}{$cmd} or do {
	DEBUG( 10,"unknown command: $cmd" );
	return;
    };
    my $reply = invoke_callback($cb,$self,@args);
    unless ( defined( $reply )) {
	DEBUG( 10, "no reply for $cmd" );
    }

    DEBUG( 100, "reply=".Dumper($reply));

    # nfreeze needs reference!
    print $sock pack( "N/a*",nfreeze(\$reply));
    close($sock);
}


############################################################################
# loop:
# * if received new command execute it
# * if receive data on RTP sockets forward them
# Args: $self
# Returns: NEVER
############################################################################
sub loop {
    my Net::SIP::NATHelper::Server $self = shift;

    my $rin; # select mask
    my $last_expire = 0;
    my $helper = $self->{helper};

    while (1) {

	# @$callbacks get set to empty in _update_callbacks which get
	# called if something on the sockets changed. In this case
	# recompute the callbacks. This is not the fastest method, but
	# easy to understand :)

	my $callbacks = $self->{callbacks};
	my $timeout = 1;
	if ( !@$callbacks ) {
	    # recompute callbacks:
	    # - add callbacks from NATHelper
	    foreach ( $helper->callbacks ) {
		my ($fd,$cb) = @$_;
		$callbacks->[ fileno($fd) ] = $cb;
	    }

	    # if nothing to do on helper set timeout to infinite
	    if ( !@$callbacks && ! $helper->number_of_calls ) {
		$timeout = undef;
		DEBUG( 50,"no RTP socks: set timeout to infinite" );
	    }

	    # - and for command sockets
	    foreach my $cfd ( @{ $self->{cfd} } ) {
		$callbacks->[ fileno($cfd) ] = [ \&do_command, $self,$cfd ];
	    }

	    # recompute select mask
	    $rin = '';
	    for( my $i=0;$i<@$callbacks;$i++ ) {
		vec( $rin,$i,1 ) = 1 if $callbacks->[$i]
	    }

	}

	# select which sockets got readable or timeout
	$rin || die;
	defined( select( my $rout = $rin,undef,undef,$timeout ) ) || die $!;
	my $now = time();

	# handle callbacks on sockets
	if ( $rout ) {
	    for( my $i=0;$i<@$callbacks;$i++ ) {
		invoke_callback( $callbacks->[$i] ) if vec( $rout,$i,1 );
	    }
	}

	# handle expires
	if ( $now - $last_expire >= 1 ) {
	    $last_expire = $now;
	    $self->expire;
	    DEBUG( 100, $helper->dump );
	}
    }
}

############################################################################
# wrap methods in helper to call _update_callbacks when appropriate
############################################################################
sub expire {
    my Net::SIP::NATHelper::Server $self = shift;
    my @expired = $self->{helper}->expire(@_);
    @expired && $self->_update_callbacks;
    return int(@expired);
}

sub allocate_sockets {
    my Net::SIP::NATHelper::Server $self = shift;
    my $media = $self->{helper}->allocate_sockets(@_) || return;
    #$self->_update_callbacks;
    return $media;
}

sub activate_session {
    my Net::SIP::NATHelper::Server $self = shift;
    my ($info,$duplicate) = $self->{helper}->activate_session(@_)
	or return;
    $self->_update_callbacks;
    return $duplicate ? -1:1;
}

sub close_session {
    my Net::SIP::NATHelper::Server $self = shift;
    my @info = $self->{helper}->close_session(@_) or return;
    $self->_update_callbacks;
    return scalar(@info);
}


sub _update_callbacks {
    my Net::SIP::NATHelper::Server $self = shift;
    @{ $self->{callbacks} } = ();
}

1;