# $Id: Connection.pm,v 1.15 2004/02/16 03:04:14 swick Exp $

package Connection;
use Carp qw(cluck);

use ActiveConferenceViewSession;
use ConferenceControlSession;
use Schedule;
use strict;

use IO::Socket;

my $STX = v2;
my $ETX = v3;

my %Sessions;

my $dotrace = 0;

sub new($$)
{
    my ($proto, $remote, $trace, $MATaddr) = @_;
    my $class = ref($proto) || $proto;

    die "need a remote host name" unless defined $remote;

    my $self  = {
		  '_connhash'		=> {},
		  '_error'		=> IO::Select->new(),
		  '_MATmsgHandlers'	=> undef,
		  '_MATsocket'		=> undef,
		  '_timeoutHandlers'	=> undef,
		  '_read'		=> IO::Select->new(),
		  '_socket'		=> undef,
		  '_timeout'		=> undef,
		  '_write'		=> IO::Select->new(),
	        };

    bless ($self, $class);

    $self->settrace( $trace ) if defined $trace;

    $self->open($remote, $MATaddr);

    sleep 1; # CAS on NT4/sp6 misses the first packet without this delay

    $Sessions{0} = LinkSession->new($self, 0);

    return $self;
}

sub register
# Register a new session
# Params: serviceKey, sessionID
{
    my ($self, $svckey, $id) = @_;

    cluck "Error: Session $id is already registered"
	if defined $Sessions{$id};

    if ($svckey eq "BV") {
	$Sessions{$id} = BridgeViewSession->new($self, $id);
    }
    elsif ($svckey eq "ACV") {
	$Sessions{$id} = ActiveConferenceViewSession->new($self, $id);
    }
    elsif ($svckey eq "ACC") {
	$Sessions{$id} = ConferenceControlSession->new($self, $id);
    }
    return $Sessions{$id};
}

sub unregister
# Remove a session registration
# Params: sessionID
{
    my ($class, $id) = @_;

    cluck "Error: Session $id is not registered"
	if not defined $Sessions{$id};

    delete $Sessions{$id};
}

sub open($$;$)
{
    my ($self, $remote, $MATaddr) = @_;
    my $socket = IO::Socket::INET->new(PeerAddr => $remote,
				       Proto => "tcp",
				      )
	or die "cannot connect to $remote";

    $self->{_socket} = $socket;
    $self->{_read}->add($socket);
    $self->{_connhash}->{$socket} = [ \&receive, $self ];

    return unless $MATaddr;

    $socket = IO::Socket::INET->new(PeerAddr => $MATaddr,
				    Proto => "tcp",
				   )
	or do {warn "cannot connect to $MATaddr; ignoring MAT connection";
	       return};

    $self->{_MATsocket} = $socket;
    $self->{_read}->add($socket);
    $self->{_connhash}->{$socket} = [ \&MATreceive, $self ];

#    $self->addMATmsgHandler('t',sub{my($self,$msg)=@_;print "tick: $msg\n";});

    $self->addMATmsgHandler('P', \&Party::PortInfoHandler);
    $self->addMATmsgHandler('A', \&Party::PortAttributeHandler);
    $self->addMATmsgHandler('B', \&Party::PortStatusHandler);
    $self->addMATmsgHandler('z3', \&Conference::MATActivateHandler);
    $self->addMATmsgHandler('z2', \&Conference::MATDeactivateHandler);
    $self->{_schedule} = new Schedule($self, $self->settrace);

    $self->MATsend('B~0');	# start port status download
}

sub close
{
    my ($self) = @_;
    trace( "closing socket\n" );
    $self->{_socket}->close;
    $self->{_socket} = undef;
    if ($self->{_MATsocket}) {
	$self->{_MATsocket}->close;
	$self->{_MATsocket} = undef;
    }
}

sub seterrorhandler
{
    my ($self, $handler) = @_;
    $self->{_errorhandler} = $handler;
}

sub sockets
# Returns the sockets used by the connection and the handler routine for
# each socket. This takes the form of an array of pairs with the socket
# being the first element of each pair and the handler the second element.
{
    my $self = shift;
    if ($self->{_MATsocket}) {
	return ($self->{_socket}, \&receive, $self->{_MATsocket}, \&MATreceive);
    }
    else {
	return ($self->{_socket}, \&receive);
    }
}

sub send($$)
{
    my ($self, $packet) = @_;

    $self->trace( "CAS TX: $packet" );
    my ($xorsum, $bytesum);
    for (my $p = 0; $p < length($packet); $p++) {
	my $c = ord(substr($packet, $p, 1));
	$xorsum ^= $c;
	$bytesum += $c;
    }

    $xorsum &= 0x7f;
    $bytesum &= 0x7f;

    $xorsum = sprintf("%2x", $xorsum);
    $bytesum = sprintf("%2x", $bytesum);

    my $socket = $self->{_socket};
    print $socket $STX.$packet.$xorsum.$bytesum.$ETX;
}

sub settrace
{
    my $self = shift;
    my $was = $dotrace;
    if (@_) { $dotrace = shift }
    return $was;
}

sub trace
{
    my ($class, @rest) = @_;
    if ($dotrace) {
	if (ref($class)) { print @rest, "\n" }
	else		 { print @_, "\n" }
    }
}

sub setTimeout
{
    my ($self, $timeout) = @_;
    my $was = $self->{_timeout};
    $self->{_timeout} = $timeout;
    return $was;
}

sub addTimeoutHandler
# Registers a function to invoke from listen() whenver the timeout expires
{
    my ($self, $proc, $obj) = @_;
    $self->{_timeoutHandlers}->{$proc} = [ $proc, $obj ];
}

sub listen
{
    my ($self) = @_;
    my ($ev, $sock, $timeout);

    while (1) {
    # thanks to NET:IRC for the following
    my $hadWork = 0;
    foreach $ev (IO::Select->select($self->{_read},
				    $self->{_write},
				    $self->{_error},
				    $self->{_timeout})) {
	$hadWork = 1;
	foreach $sock (@{$ev}) {
	    my $conn = $self->{_connhash}->{$sock};
	    
	    # $conn->[0] is a code reference to a handler sub.
	    # $conn->[1] is optionally an object which the
	    #    handler sub may be a method of.
	    
	    $conn->[0]->($conn->[1] ? ($conn->[1], $sock) : $sock);
	}
    }
    $self->callTimeouts() unless $hadWork;
    }
}

sub callTimeouts
{
    my ($self) = shift;
    if ($self->{_timeoutHandlers}) {
	for my $f (values %{$self->{_timeoutHandlers}}) {
	    $f->[0]->($f->[1] ? ($f->[1], $self)
			      : ($self));
	}

    }
}

sub receive
{
    my ($self) = @_;
    my ($line, $buff);

    my $rval = IO::Socket::recv($self->{_socket}, $buff, 2048,0);

    if (!defined $rval) {
	if (defined $self->{_errorhandler}) {
	    return $self->{_errorhandler}();
	}
	else {
	    die "Error in recv()";
	}
    }

    my @msg = split(/$ETX/o, $buff);
    for (my $l = 0; defined $msg[$l]; $l++) {
	my $line = $msg[$l];

	# strip off STX and checksum digits
	$line = substr($line, 1, length($line)-5);

	next if ($line eq "");	    

	$self->trace( 'CAS RX: ', $line );

	my ($sessionID) = split('~', $line);

	$Sessions{$sessionID} = Session->new($self, $sessionID)
	    unless defined $Sessions{$sessionID};

	$Sessions{$sessionID}->handle($line);
    }

    return $rval;
}

sub request
{
    my ($self, $msg) = @_;
    
    if ($msg =~ m/^LS./o) {
	$Sessions{0}->request($msg);
    }
    else {
	cluck "Error: connections only handle LS messages";
    }
}

sub requestWithReply
{
    my ($self, $msg, @rest) = @_;
    
    if ($msg =~ m/^LS./o) {
	$Sessions{0}->requestWithReply($msg, @rest);
    }
    else {
	cluck "Error: connections only handle LS messages";
    }
}

#sub linksession
#{
#    my ($self) = @_;
#
#    return $Sessions{0};
#}

sub addMATmsgHandler($$$)
{
    my ($self, $msgType, $handler) = @_;

    unless ($handler) {
	warn "MAT msg handler not specified";
	return;
    }

    if ($self->{_MATmsgHandlers}->{$msgType}) {
	push @{$self->{_MATmsgHandlers}->{$msgType}}, $handler;
    }
    else {
	my @handlers = ($handler);
	$self->{_MATmsgHandlers}->{$msgType} = \@handlers;
    }
}

sub removeMATmsgHandler($$$)
{
    my ($self, $msgType, $handler) = @_;

    unless ($handler) {
	warn "MAT msg handler not specified";
	return;
    }

    if ($self->{_MATmsgHandlers}->{$msgType}) {
	my @handlers = @{$self->{_MATmsgHandlers}->{$msgType}};

	for (my $index = 0; $index <= $#handlers; $index++) {
	    if ($handlers[$index] == $handler) {
		splice (@{$self->{_MATmsgHandlers}->{$msgType}}, $index, 1);
		return;
	    }
	}
    }

    warn "MAT msg handler for $msgType not found";
}

# sends a request to the MAT
sub MATsend($$)
{
    my ($self, $packet) = @_;

    return undef unless $self->{_MATsocket};

    $self->trace( "MAT TX: $packet" );
    my ($xorsum, $bytesum) = (2^3, 2+3); # includes STX and ETX
    for (my $p = 0; $p < length($packet); $p++) {
	my $c = ord(substr($packet, $p, 1));
	$xorsum ^= $c;
	$bytesum += $c;
	$xorsum &= 0xff;
	$bytesum &= 0xff;
    }

    $xorsum = sprintf("%02X", $xorsum);
    $bytesum = sprintf("%02X", $bytesum);

    return $self->{_MATsocket}->send($STX.$packet.$ETX.$xorsum.$bytesum, 0);
}

sub MATreceive
{
    my ($self) = @_;
    my ($line, $buff);

    my $rval = IO::Socket::recv($self->{_MATsocket}, $buff, 2048,0);

    if (!defined $rval) {
	if (defined $self->{_errorhandler}) {
	    return $self->{_errorhandler}();
	}
	else {
	    die "Error in recv() from MAT";
	}
    }

    unless ($buff) {		# no message; likely dropped connection
	print "no msg from MAT; likely lost connection\n";
	$self->{_MATsocket}->close;
	$self->{_MATsocket} = undef;
	return $rval;
    }

    my @receiveQueue = split(/$STX/o, $buff);

    while (@receiveQueue) {
	my ($line,$chk) = split(/$ETX/o, shift(@receiveQueue));

	next unless $line;

	$self->trace( 'MAT RX: ', $line )
	    unless $line =~ m/^t~/o; # don't trace clock ticks

	my ($msgType) = split('~', $line);

	if ($self->{_MATmsgHandlers}->{$msgType}) {
	    my @handlers = @{$self->{_MATmsgHandlers}->{$msgType}};
	    # the handler list is copied in case a handler modifies the list
	    foreach my $handler (@handlers) {
		$handler->($self, $line);
	    }
	}
    }

    return $rval;
}

1;
