package PPM::InstallerClient;

use strict;
use Socket;
use Cwd qw(cwd);
use File::Basename qw(dirname basename);
use File::Path qw(mkpath rmtree);

use Data::Dumper;
use vars qw($VERSION %handlers);

use constant PROTOCOL => 2;
$VERSION = '3.0';

#=============================================================================
# API:
#=============================================================================
sub new {
    my ($pkg, $port) = @_;
    my $o = bless {}, ref($pkg) || $pkg;
    $o->init($port);
}

sub init {
    my ($inst, $ppm_port) = @_;
    my ($paddr, $proto, $msg);

    # Set up a temporary socket server and waits for the frontend to connect
    # to it.
    # TODO: put this in a big while(1) loop, and keep a list of connected
    # frontends. That way, we can service multiple front-ends at once, which
    # prevents multiple instances of the same target from clobbering each
    # other's changes.
    $proto = getprotobyname('tcp');
    socket(SERVER, PF_INET, SOCK_STREAM, $proto)	|| die "socket: $!";
    setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,
	       pack('l', 1))				|| die "setsockopt: $!";
    bind(SERVER, sockaddr_in($ppm_port, INADDR_ANY))	|| die "bind: $!";
    listen(SERVER, SOMAXCONN);
    $paddr = accept(CLIENT, SERVER);
    my ($port, $iaddr) = sockaddr_in($paddr);
    my $name = gethostbyaddr($iaddr, AF_INET);
    select((select(CLIENT), $| = 1)[0]);
    $inst->{fd} = \*CLIENT;
    close(SERVER);

    $inst->{cwd} = cwd;

    # Read commands from the socket:
    while ($msg = $inst->recvmsg) {
	my ($cmd, @args) = decode_record($msg);
	my $h = $handlers{lc($cmd)};
	if (defined $h) {
	    last unless $inst->$h($cmd, @args);
	}
	else {
	    die "Unrecognized command: $cmd";
	}
    }
}

%handlers = (
    has		=> sub {
	my ($inst, undef, @args) = @_;
	my $cmd = $args[0];
	if (exists $handlers{lc($cmd)}) {
	    $inst->sendmsg("OK");
	}
	else {
	    $inst->sendmsg("NOK");
	}
    },
    query	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @ppds = $inst->query(@args);
	if (not $ppds[0]) {
	    $inst->sendmsg("NOK");
	}
	else {
	    my @records = map { encode_record($_) } @ppds[1..$#ppds];
	    local $" = "\n";
	    $inst->sendmsg("@records");
	}
	1;
    },
    properties	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @fields = $inst->properties(@args);
	if (@fields) {
	    $inst->sendmsg(encode_record(@fields));
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    remove	=> sub {
	my ($inst, $cmd, @args) = @_;
	my $ret = $inst->remove(@args);
	if ($ret) {
	    $inst->sendmsg("OK");
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    precious	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @ret = $inst->precious();
	$inst->sendmsg(encode_record(@ret));
	1;
    },
    bundled	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @ret = $inst->bundled();
	$inst->sendmsg(encode_record(@ret));
    },
    dependents	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @ret = $inst->dependents(@args);
	if (@ret == 1 and not defined $ret[0]) {
	    $inst->sendmsg('NOK');
	}
	elsif (@ret == 0) {
	    $inst->sendmsg(encode_record(undef));
	}
	else {
	    $inst->sendmsg(encode_record(@ret));
	}
	1;
    },
    config_info	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @ret = $inst->config_info;
	if (@ret) {
	    my @records = map { encode_record(@$_) } @ret;
	    local $" = "\n";
	    $inst->sendmsg("@records");
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    config_keys	=> sub {
	my ($inst, $cmd, @args) = @_;
	my @ret = $inst->config_keys;
	if (@ret) {
	    my @records = map { encode_record(@$_) } @ret;
	    local $" = "\n";
	    $inst->sendmsg("@records");
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    config_get	=> sub {
	my ($inst, $cmd, @args) = @_;
	# Because I don't want the actual installer reporting this key, I
	# will override it here, in the client library. The C version will do
	# this.
	if ($args[0] eq 'PROTOCOL') {
	    $inst->sendmsg(PROTOCOL);
	    return 1;
	}
	my $ret = $inst->config_get(@args);
	if ($ret) {
	    $inst->sendmsg($ret);
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    config_set	=> sub {
	my ($inst, $cmd, @args) = @_;
	if ($inst->config_set(@args)) {
	    $inst->sendmsg("OK");
	} 
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    error_str	=> sub {
	my ($inst, $cmd, @args) = @_;
	$inst->sendmsg($inst->error_str);
	1;
    },
    can_install	=> sub {
	my ($inst, $cmd, @args) = @_;
	# The following line is for reference:
	# my ($lang, $version, $compat_type) = @args;
	my $ret = $inst->can_install(@args);
	if (defined $ret) {
	    $inst->sendmsg($ret);
	}
	else {
	    sendmsg('NOK');
	}
	1;
    },
    install	=> sub {
	my ($inst, $cmd, @args) = @_;
	# The following line is for reference:
	# my ($pkg, $ppmpath, $ppd, $repos, $ppmpath) = @args;
	$args[1] = $inst->{tmpdirs}{$args[0]}
	  if exists $inst->{tmpdirs}{$args[0]};

	my $ret = $inst->install(@args);
	if ($ret) {
	    $inst->sendmsg("OK");
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    upgrade	=> sub {
	my ($inst, $cmd, @args) = @_;
	# The following line is for reference:
	# my ($pkg, $ppmpath, $ppd, $repos, $ppmpath) = @args;
	$args[1] = $inst->{tmpdirs}{$args[0]}
	  if exists $inst->{tmpdirs}{$args[0]};

	my $ret = $inst->upgrade(@args);
	if ($ret) {
	    $inst->sendmsg("OK");
	}
	else {
	    $inst->sendmsg("NOK");
	}
	1;
    },
    pkginit	=> sub {
	my ($inst, $cmd, @args) = @_;
	my $pkg = shift @args;
	my $tmpdir = $inst->config_get("tempdir");
	unless ($tmpdir and -w $tmpdir) {
	    $inst->sendmsg(encode_record(
		'NOK', "Backend tempdir '$tmpdir' not writeable"
	    ));
	    return 1;
	}
	$tmpdir .= "/$pkg-$$";
	mkpath($tmpdir);
	$inst->{tmpdirs}{$pkg} = $tmpdir;
	$inst->sendmsg('OK');
	1;
    },
    pkgfini	=> sub {
	my ($inst, $cmd, @args) = @_;
	my $pkg = shift @args;
	my $path = $inst->{tmpdirs}{$pkg} or do {
	    $inst->sendmsg(
		encode_record('NOK', 'pkgfini() without pkginit()')
	    );
	    return 1;
	};
	rmtree($path);
	delete $inst->{tmpdirs}{$pkg};
	$inst->sendmsg('OK');
	1;
    },
    transmit	=> sub {
	my ($inst, $cmd, @args) = @_;
	my $pkg  = shift @args;
	my $tmpdir = $inst->{tmpdirs}{$pkg};
	my $file = shift @args;
	my $dir  = dirname($file);

	chdir($tmpdir);
	mkpath($dir);

	eval {
	    open(FILE, "> $file")	|| die "can't write $file: $!";
	    binmode(FILE)		|| die "can't binmode $file: $!";
	};
	if ($@) {
	    $inst->sendmsg(encode_record('NOK', "$@"));
	    return 1;
	}
	$inst->sendmsg('OK');
	my $msg;
	while ($msg = $inst->recvmsg) {
	    my ($flag, $data) = decode_record($msg);
	    last if $flag eq 'EOT';
	    print FILE $data;
	}
	eval {
	    close(FILE)			|| die "can't close $file: $!";
	};
	if ($@) {
	    $inst->sendmsg(encode_record('NOK', "$@"));
	    return 1;
	}
	$inst->sendmsg('OK');
	chdir($inst->{cwd});
	1;
    },
    stop	=> sub {
	my ($inst, $cmd, @args) = @_;
	close(CLIENT);
	0;
    },
);

#=============================================================================
# Private functions!
#=============================================================================

use constant FIELD_SEP => "\001";
use constant FIELD_UNDEF => "\002";
my $EOL = "\015\012";

sub sendmsg {
    my $o = shift;
    my $fd = $o->{fd};
    my $msg = shift;
    local $\ = "$EOL.$EOL";
    print $fd $msg;
}

sub recvmsg {
    my $o = shift;
    my $fd = $o->{fd};
    local $/ = "$EOL.$EOL";
    my $msg = <$fd>;
    chomp $msg if $msg;
    return $msg;
}

sub qmeta {
    local $_ = shift || $_;
    s{([^A-Za-z0-9])}{sprintf('\x%.2X',ord($1))}eg;
    $_;
}

sub uqmeta {
    local $_ = shift || $_;
    eval qq{qq{$_}};
}

sub encode_record {
    my @fields = map { my $a = defined $_ ? $_ : FIELD_UNDEF; qmeta($a) } @_;
    join FIELD_SEP, @fields;
}

sub decode_record {
    my $t = shift || $_;
    return map { $_ = &uqmeta; $_ = undef if $_ eq FIELD_UNDEF; $_ }
	   split(FIELD_SEP, $t, -1);
}

