package FTP;

use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

require Exporter;
@ISA = qw(Exporter);

@EXPORT = qw(
);
@EXPORT_OK = qw(
);
$VERSION = '0.10';

require FTP::rfc959;

use Carp;
use FileHandle;
use Socket;
use Sys::Hostname;

use vars qw( $Verbose $Debug );


sub open_ctl {
    my ($class, $host) = @_;
    my ($port, $proto, $query, $iaddr);

    $proto = getprotobyname('tcp');
    $port = getservbyname('ftp', 'tcp')
	|| do { die "Invalid port: 'tcp'"; return undef  };
    $iaddr = inet_aton($host)
	|| do { carp "Invalid host: '$host'"; return undef  };
    print STDERR "Connecting to @{[inet_ntoa($iaddr)]}.$port...\n"
	if $FTP::Debug;

    my ($fh, $sin) = new FileHandle;
    socket($fh, PF_INET, SOCK_STREAM, $proto);
    $sin = sockaddr_in($port, $iaddr);
    connect($fh, $sin);

    $fh->autoflush(1);
    return $fh;
}

sub open_data {
    my ($class, $host) = @_;
    my ($port, $proto, $query, $iaddr);

    $proto = getprotobyname('tcp');
    $port = getservbyname('ftp', 'tcp')
	|| do { die "Invalid port: 'tcp'"; return undef  };
    $iaddr = inet_aton($host)
	|| do { carp "Invalid host: '$host'"; return undef  };
    print STDERR "Connecting to @{[inet_ntoa($iaddr)]}.$port...\n"
	if $FTP::Debug;

    my ($fh, $sin) = new FileHandle;
    socket($fh, PF_INET, SOCK_STREAM, $proto);
    $sin = sockaddr_in($port, $iaddr);
    connect($fh, $sin);

    $fh->autoflush(1);
    return $fh;
}

sub get_response {
    my ($self) = @_;
    my ($fh) = $self->{ctl};
    my (@cont);
    my ($resp) = scalar(<$fh>);
    $resp =~ s/[\r\n]+$//;

    carp "Reading before sending (deadlock expected)" if $self->{done};

    (my(@r) = ($resp =~ /^((\d)(\d)(\d))([ -])/))
	|| do { carp "Invalid response: '$resp'"; return undef  };

    print "- $r[0]-", FTP::rfc959::meaning(@r), "\n" if ($FTP::Verbose > 1);
    print "> $resp\n" if $FTP::Verbose;

    # deal with continuations
    if ($r[4] ne ' ') {
	while ($resp !~ /^$r[1]$r[2]$r[3] /) {
	    $resp = scalar(<$fh>);
	    croak "Server connection closed!" unless $resp;
	    $resp =~ s/[\r\n]+$//;
	    print "> $resp\n" if $FTP::Debug;
	    push (@cont, $resp);
	}
	pop (@cont);
	@r = ($resp =~ /^((\d)(\d)(\d))([ -])/);
    }

    # remember the last response recieved
    $self->{state} = $r[0];
    if (@cont) { $self->{data}  = [ @cont ] } else { delete $self->{data} };

    # deal with printing the output
    my($print) = ref($self)->find_state($self->{ptable}, $self->{state});
    &$print if ref($print);

    $self->{done} = 1;
    @cont;
}

sub send {
    my ($self, $cmd) = @_;
    my ($fh) = $self->{ctl};

    carp "Sending before read completed" unless $self->{done};

    print $fh $cmd,"\r\n";
    $self->{done} = 0;
}

sub find_state {
    my ($class, $table, $state) = @_;
    return undef unless ($state && $table);
    my (@s) = split(//, $state);

    print "Looking for $state among @{[keys %$table]}.\n";

    $table->{$state}
    || $table->{"$s[0]$s[1]x"}
    || $table->{"$s[0]xx"}
    || $table->{"x$s[1]x"}
    || $table->{"xxx"}
    || $class->find_state($table->{ISA}, $state);
}

sub dispatch {
    my ($self) = @_;
    my ($next);

    while(1) {
	$next = ref($self)->find_state($self->{stable}, $self->{state});
	ref($next) || return(1);
	&$next;
	$self->get_response();
    }
}

sub command {
    my ($self, $cmd) = @_;
    $self->send($cmd);
    $self->get_response();
    $self->dispatch();
}

sub print_data {
    my ($self) = @_;
    my ($i);
    for $i (@{$self->{data}}) {
	print "# $i\n";
    }
    delete $self->{data};
}

sub init_tables {
    my ($self) = @_;

    $self->{stable} = {220 => sub { $self->send("USER " . $self->{user}) },
		       331 => sub { $self->send("PASS " . $self->{pass}) },
		       230 => undef,
		   };

    $self->{ptable} = {'21x' => sub { $self->print_data() },
		   };
}

### constructors / destructors

sub new {
    my ($class, $host, $user, $pass) = @_;
    print "! creating connection for host $host, class $class\n"
	if ($FTP::Verbose > 8);

    my($ctl) = $class->open_ctl($host)
	|| do { croak "Can't open $class connection to $host"; return undef };

    my ($self) = {};
    $self->{host}   = $host;
    $self->{ctl}    = $ctl;
    $self->{done}   = 0;

    $self->{user}   = $user || 'anonymous';
    $self->{pass}   = $pass || ($ENV{USER} . "@" .
				(gethostbyname(hostname))[0]);
    bless $self, $class;
    $self->init_tables();
    $self->get_response();
    return $self;
}

sub DESTROY {
    my ($self) = @_;
    print "! destroying connection for host $self->{host}\n"
	if ($FTP::Verbose > 8);
}

1;
__END__

=head1 NAME

FTP - use File Transfer Protocol

=head1 SYNOPSIS

  use FTP;
  $connection = new FTP 'that.host.com';

=head1 DESCRIPTION

=head1 AUTHOR

Albert Dvornik, bert@mit.edu

=head1 SEE ALSO

=cut
