package Modem::VBox;

use strict 'subs';
use Carp;
use bytes;

require Exporter;
use POSIX ':termios_h';
use Fcntl;
use Event qw(unloop one_event time unloop_all);
use Event::Watcher qw(R W);
use Time::HiRes qw/time/; # this is required(!)

BEGIN { $^W=0 } # I'm fed up with bogus and unnecessary warnings nobody can turn off.

@ISA = qw(Exporter);

@_consts = qw(RING RUNG CONNECT BREAK EOTX);
@_funcs = qw();

@EXPORT = @_consts;
@EXPORT_OK = @_funcs;
%EXPORT_TAGS = (all => [@_consts,@_funcs], constants => \@_consts);
$VERSION = '0.051';

# if debug is used, STDIN will be used for events and $play will be used to play messages
$debug = 0;

# hardcoded constants
$HZ=8000;
$PFRAG=8192; # frag size for play_pause

$ETX="\003";
$DLE="\020";
$DC4="\024";

# bit flags for state var
sub VCON	(){ 1 }
sub VTX		(){ 2 }
sub VRX		(){ 4 }

# event types
sub RING	(){ -1 } # a single ring (+ count)
sub RUNG	(){ -2 } # ring timeout
sub CONNECT	(){ -3 } # a single ring (+ count)
sub BREAK	(){ -4 } # break sequence detected
sub EOTX	(){ -6 } # end of current transmissions

sub slog {
   my $self=shift;
   my $level=shift;
   print STDERR $self->{line},": ",@_,"\n" if $level <= $debug;
}

#  port => /dev/ttyI0
sub new {
   my $class = shift;
   my(%attr)=@_;

   croak "line must be specified" unless $attr{line};

   eval { $attr{speed}	||= &B115200 };
   eval { $attr{speed}	||= &B57600  };
   $attr{speed}		||= B38400;
   $attr{timeout}	||= 2;

   $attr{dropdtrtime}	||= 0.25; # dtr timeout
   $attr{modeminit}	||= "ATZ";
   $attr{ringto}	||= 6; # ring-timeout
   $attr{rings}		||= 3; # number of rings

   $attr{ring_cb}	||= sub { };

   my $self = bless \%attr,$class;

   $self->{ringtowatcher} = Event->timer(
      interval => $self->{ringto},
      desc => "RING timeout watcher",
      parked => 1,
      cb => sub {
         $self->rung;
         $self->slog(1, "ring timeout, aborted connection");
      }
   );

   $self->initialize;
   $self->reset;

   $self->{HZ}		||= $HZ;
   $self->{FRAG}	||= 1024;

   $self;
}

sub DESTROY {
   my $self=shift;
   $self->{tio}->setispeed(B0); $self->{tio}->setospeed(B0); $self->sane;
   close $self->{fh} or croak "error during modem-close: $!";
}

sub flush {
   my $self=shift;
   undef $self->{rawinput};
   tcflush $self->{fileno}, TCIOFLUSH;
   my $buf; 1 while (sysread ($self->{fh},$buf,1024) > 0);
}

sub sane {
   my $self=shift;
   $self->{tio}->setiflag(BRKINT|IGNPAR|IXON);
   $self->{tio}->setoflag(OPOST);
   $self->{tio}->setcflag($self->{tio}->getcflag
                  &~(CSIZE|CSTOPB|PARENB|PARODD|CLOCAL)
                  | (CS8|CREAD|HUPCL));
   $self->{tio}->setlflag(ECHOK|ECHOE|ECHO|ISIG|ICANON);
   $self->{tio}->setattr($self->{fileno});
   $self->{tio}->setcc(VMIN,1);
   $self->{tio}->setcc(VTIME,0);
}

sub raw {
   my $self=shift;
   $self->{tio}->setiflag($self->{tio}->getiflag & (IXON|IXOFF));
   $self->{tio}->setoflag(0);
   $self->{tio}->setcflag(0);
   $self->{tio}->setlflag(0);
   $self->{tio}->setcc(VMIN,1);
   $self->{tio}->setcc(VTIME,0);
   $self->{tio}->setattr($self->{fileno});
}

sub reset {
   my $self=shift;

   $self->initialize;
   $self->sane;
   $self->{inwatcher}->stop;

   my $i=$self->{tio}->getispeed; my $o=$self->{tio}->getospeed;
   $self->{tio}->setispeed(B0); $self->{tio}->setospeed(B0);

   $self->{tio}->setattr($self->{fileno});
   my $w = Event->timer(after => $self->{dropdtrtime},
                        cb => sub { $_[0]->w->cancel; unloop },
                        desc => 'Modem DTR drop timeout');

   $self->{tio}->setispeed($i); $self->{tio}->setospeed($o);

   $self->slog(3,"waiting for reset");
   $self->loop;
   $self->slog(3,"line reset");

   $self->{tio}->setattr($self->{fileno});

   $self->raw;
   $self->flush;
   $self->{inwatcher}->start;

   $self->command("AT")=~/^OK/ or croak "modem returned $self->{resp} to AT";
   $self->command($self->{modeminit})=~/^OK/ or croak "modem returned $self->{resp} to modem init string";
   $self->command("AT+VLS=2")=~/^OK/ or croak "modem returned $self->{resp} to AT+VLS=2";
   $self->command("AT+VSM=6")=~/^OK/ or croak "modem returned $self->{resp} to AT+VSM=6";
}

# read a line
sub modemline {
   my $self=shift;
   my $timeout;
   Event->timer (
      after => $self->{timeout},
      desc => "modem response timeout",
      cb => sub { $timeout = 1;
                  $_[0]->w->cancel }
   );
   one_event while !@{$self->{modemresponse}} && !$timeout;
   shift(@{$self->{modemresponse}});
}

sub modemwrite {
   my $self = shift;
   my $cmd = shift;
   fcntl $self->{fh},F_SETFL,0;
   syswrite $self->{fh}, $cmd, length $cmd;
   fcntl $self->{fh},F_SETFL,O_NONBLOCK;
}

sub command {
   my $self = shift;
   my $cmd = shift;
   $self->modemwrite("$cmd\r");
   $self->{resp} = $self->modemline;
   $self->{resp} = $self->modemline if $self->{resp} eq $cmd;
   $self->slog(2,"COMMAND($cmd) => ",$self->{resp});
   $self->{resp};
}

sub initialize {
   my $self=shift;

   $self->{inwatcher}->cancel  if $self->{inwatcher};
   $self->{outwatcher}->cancel if $self->{outwatcher};

   delete @{$self}{qw(play_queue state context break callerid
                      rawinput rawoutput modemresponse record
                      inwatcher outwatcher tio fh)};

   $self->slog(3,"opening line");

   $self->{fh}=local *FH;
   sysopen $self->{fh},$self->{line},O_RDWR|O_NONBLOCK
      or croak "unable to open device $self->{line} for r/w";
   $self->{fileno}=fileno $self->{fh};

   $self->{tio} = new POSIX::Termios;
   $self->{tio}->getattr($self->{fileno});

   $self->{inwatcher}=Event->io(
      poll => R,
      fd => $self->{fileno},
      desc => "Modem input for $self->{line}",
      parked => 1,
      cb => sub {
         my $ri = \($self->{rawinput});
         if (sysread($self->{fh}, $$ri, 8192, length $$ri) == 0) {
            $self->slog(1, "short read, probably remote hangup");
            if ($self->connected) {
               #$self->{state} &= ~(VCON|VRX|VTX);
               $self->hangup;
            } else {
               $self->slog(0, "WOAW, short read while in command mode, reinitialize");
               $self->initialize;
            }
         } else {
            if ($self->{state} & VRX) {
               my $changed;
               # must use a two-step process
               $$ri =~ s/^((?:[^$DLE]+|$DLE[^$ETX$DC4])*)//o;
               my $data = $1;
               $data =~ s{$DLE(.)}{
                  if ($1 eq $DLE) {
                     $DLE;
                  } else {
                     $self->{break} .= $1;
                     $changed=1;
                     "";
                  }
               }ego;
               $self->{record}->($data) if $self->{record};
               if ($$ri =~ s/^$DLE$ETX//o) {
                  $self->slog(3, "=> ETX, EO VTX|VRX");
                  $self->{state} &= ~VRX;
                  if ($self->{state} & VTX) {
                     $self->{state} &= ~VTX;
                     delete $self->{play_queue};
                     delete $self->{rawoutput};
                     $self->modemwrite("$DLE$ETX");
                  }
                  $$ri =~ s/^[\r\n]*(?:VCON)?[\r\n]+//;
               }
               $self->check_break if $changed;
            }
            unless ($self->{state} & VRX) {
               while ($$ri =~ s/^([^\r\n]*)[\r\n]+//) {
                  local $_ = $1;
                  if (/^CALLER NUMBER:\s+(\d+)$/) {
                     $self->{_callerid}=$1;
                     $self->slog(3,"incoming call has callerid $1");
                  } elsif (/^RING\b/) {
                     my $cid = delete $self->{_callerid} || "0";
                     my $oci = $self->{callerid};
                     $self->{callerid}=$cid;
                     if (defined $oci) {
                        if ($oci ne $cid) {
                           $self->rung;
                        }
                     } else {
                        $self->{ring}=0;
                     }
                     $self->{ringtowatcher}->stop;
                     $self->{ringtowatcher}->again;
                     $self->{ring}++;
                     $self->{ring_cb}->($self->{ring}, $self->{callerid});
                     $self->slog(1, "the telephone rings (#".($self->{ring})."), hurry! (callerid $self->{callerid})");
                     $self->accept if $self->{ring} >= $self->{rings};
                  } elsif (/^RUNG\b/) {
                     $self->rung;
                  } elsif (/\S/) {
                     push @{$self->{modemresponse}}, $_;
                  }
               }
            }
         }
      }
   );
   $self->{outwatcher} = Event->timer(
      parked => 1,
      desc => "Modem sound output for $self->{line}",
      cb => sub {
         my $w = $_[0]->w;
         my $l;
         unless (length $self->{rawoutput}) {
            my $q = $self->{play_queue};
            if (@$q) {
               #$self->slog(7, "(out $q->[0])");
               if (ref \($q->[0]) eq "GLOB") {
                  my $n;
                  $l = sysread $q->[0], $self->{rawoutput}, $self->{FRAG};
                  #$self->slog(7, "reading from file ($l bytes)\n");#d#
                  $self->{rawoutput} =~ s/$DLE/$DLE$DLE/go;
                  if ($l <= 0) {
                    #$self->slog(7, "EOTX\n");#d#
                    $self->event(EOTX, scalar@$q);
                    shift @$q;
                  }
               } else {
                  $self->{rawoutput} = ${shift(@$q)};
               }
            } else {
               $w->stop;
               $self->event(EOTX, 0);
               return;
            }
         }
         if (length $self->{rawoutput}) {
            #$self->slog(7, "(send ".(length $self->{rawoutput})." bytes)");
            $l = syswrite $self->{fh}, $self->{rawoutput}, length $self->{rawoutput};
            #$self->slog(7, "(sent $l bytes)");
            substr($self->{rawoutput}, 0, $l) = "" if $l > 0;
            if (defined $l) {
               $l /= $self->{HZ}; #/
            } else {
               $l = 0.1;
            }
            $self->{vtx_end} += $l;
         }
         $w->at($self->{vtx_end} - 0.01);
         $w->start;
      }
   );

   $self->{tio}->setispeed($self->{speed});
   $self->{tio}->setospeed($self->{speed});

   $self->{ring}=0;
}

sub abort {
   my $self=shift;
   $self->initialize;
   $self->reset;
   $self->slog(1,"modem is now in listening state");
}

sub rung {
   my $self=shift;
   $self->{ringtowatcher}->stop;
   $self->{ring}=0;
   $self->event(RUNG);
   $self->slog(1,"caller ($self->{callerid}) hung up before answering");
   delete $self->{callerid};
}

sub loop {
   local $Event::DIED = sub {
      print STDERR $_[1];
      unloop_all;
   };
   Event::loop;
}

sub accept {
   my $self=shift;
   # DLE etc. handling
   $self->{ringtowatcher}->stop;
   if ($self->command("ATA") =~ /^VCON/) {
      $self->slog(2, "call accepted (callerid $self->{callerid})");
      if ($self->command("AT+VTX+VRX") =~ /^CONNECT/) {
         $self->{state} |= VCON|VTX|VRX;
         delete $self->{event};
         $self->event(CONNECT);
         $self->{connect_cb}->($self);
         delete $self->{event};
      } else {
         $self->rung;
         $self->abort;
         $self->slog(1, "modem did not respond with CONNECT to AT+VTX+VRX command");
      }
   } else {
      $self->slog(1, "modem did not respond with VCON to my ATA");
      $self->rung;
   }
}

sub check_break {
   my $self=shift;
   while(my($k,$v) = each %{$self->{context}}) {
      if ($self->{break} =~ /$k/) {
         ref $v eq "CODE" ? $v->($self, $self->{break})
                          : $self->event(BREAK, $v);
      }
   }
}

sub hangup {;
   my $self=shift;
   $self->event(undef) if $self->connected;
   $self->abort;
}

sub connected {
   $_[0]->{state} & VCON;
}

# return the number of pending events
sub pending {
   @{$_[0]->{event}};
}

sub wait_event {
   my $self = shift;
   one_event while !$self->pending;
}

sub event {
   my $self=shift;
   #$self->slog(3, "EVENT ".(scalar@_)." :@_:");
   if (@_) {
      push @{$self->{event}},
         defined $_[0] ? bless [@_], "Modem::VBox::Event" 
                       : undef;
   } else {
      $self->wait_event;
      defined $self->{event}->[0] ? shift @{$self->{event}}
                                  : undef;
   }
}

sub play_file($$) {
   my $self = shift;
   my $path = shift;
   my $fh = do { local *FH };
   $self->slog(5, "play_file $path");
   open $fh,"<$path" or croak "unable to open ulaw file '$path' for playing";
   $self->play_object($fh);
}

sub play_data($$) {
   my $self=shift;
   my $data=shift;
   $data=~s/$DLE/$DLE$DLE/go;
   $self->play_object(\$data);
}

sub play_object($$) {
   my $self=shift;
   my $obj=shift;
   $self->{state} & VCON or return;
   unless ($self->{outwatcher}->is_active) {
      $self->{outwatcher}->at($self->{vtx_end} = time);
      $self->{outwatcher}->start;
   }
   push @{$self->{play_queue}}, $obj;
}

sub play_pause($$) {
   my $self=shift;
   $self->slog(5, "play_pause $_[0]");
   my $len = int($self->{HZ}*$_[0]+0.999);
   my $k8  = "\xFE" x $PFRAG;
   while ($len>length($k8)) {
      $self->play_object(\$k8);
      $len-=length($k8);
   }
   $self->play_object(\("\xFE" x $len));
}

sub play_count($) {
   scalar @{$_[0]->{play_queue}};
}

sub play_flush($) {
   my $self=shift;
   #tcflush $self->{fileno}, TCOFLUSH;
   @{$self->{play_queue}} = ();
   delete $self->{rawoutput};
   one_event;
}

sub play_drain($) {
   my $self=shift;
   my $waiting = 1;
   one_event while $self->play_count;
   Event->timer(at => $self->{vtx_end},
                desc => "play_drain timer",
                cb => sub { $waiting = 0;
                            $_[0]->w->cancel }
               );
   one_event while $waiting;
}

sub record($$) {
   my $self = shift;
   $self->{record} = shift;
}

sub record_file($$) {
   my $self = shift;
   my $fh = shift;
   $self->record (sub { print $fh $_[0] });
}

sub callerid($) { $_[0]->{callerid} }

sub context($) {
   my $self=shift;
   bless [$self, {%{$self->{context}}}], "Modem::VBox::context";
}

package Modem::VBox::Event;

sub type($$)	{ $_[0]->[0] == $_[1] }
sub isbreak($)	{ $_[0]->[0] == Modem::VBox::BREAK }
sub iseotx($;$)	{ $_[0]->[0] == Modem::VBox::EOTX && ( @_ < 2 || $_[1] >= $_[0]->[1] ) }
sub data($)	{ $_[0]->[1] }

package Modem::VBox::context;

sub set {
   my $self=shift;
   %{$self->[0]{context}} = @_;
   $self;
}

*clr = \&set;

sub add {
   my $self=shift;
   while(@_) {
      $self->[0]{context}{$_[0]} = $_[1];
      shift; shift;
   }
   $self;
}

sub del {
   my $self=shift;
   for(@_) {
      delete $self->[0]{context}{$_};
   }
   $self;
}

sub DESTROY {
   my $self=shift;
   my($vbox,$ctx)=@$self;
   $vbox->{context}=$ctx;
}

1;
__END__

=head1 NAME

Modem::VBox - Perl module for creation of voiceboxes.

=head1 SYNOPSIS

  use Modem::VBox;

=head1 DESCRIPTION

Oh well ;) Not written yet! An example script (C<vbox>) is included in the distro, though.

=head1 AUTHOR

Marc Lehmann <schmorp@schmorp.de>.

=head1 SEE ALSO

perl(1), L<Modem::Vgetty> a similar but uglier interface.

=cut