package PPM::Installer;

use strict;
use Socket;
use Data::Dumper;
use PPM::Result qw(Error Warning Ok List);

my %connections;
sub new {
    my ($class, $name, $targ) = @_;

    my @ret;
    my $pkg  = "PPM::Installer::$targ->{type}";

    # Let the type decide which subclass to use:
    eval "require $pkg";
    if (not $@) {
	no strict 'refs';
	$connections{$name} = &{"${pkg}::new"}($pkg, $targ, $name);
	$connections{$name}->init;
	@ret = $connections{$name};
    }
    else {
	@ret = (undef, "Unsupported installer type '$targ->{type}'");
    }
    @ret;
}

sub DESTROY {
    my $o = shift;
    $o->fini;
}

#=============================================================================
# API methods -- simple fetch routines.
#=============================================================================
sub name {
    my $inst = shift;
    $inst->{name};
}

sub port {
    my $inst = shift;
    $inst->{port};
}

sub type {
    my $inst = shift;
    $inst->{type};
}

sub ckeys { qw(port type) }
sub cvals {
    my $o = shift;
    map { $o->$_ } $o->ckeys;
}

#=============================================================================
# Methods which determine which version of the API the backend is running.
#=============================================================================
# We remember here what methods were supported under protocol version 1.
my %HAS = map { $_ => 1 } qw(
    query
    properties
    remove
    precious
    bundled
    dependents
    config_info
    config_keys
    config_get
    config_set
    error_str
    install
    upgrade
    pkginit
    pkgfini
    transmit
    stop
);

sub has {
    my $inst = shift;
    my $method = shift;
    return $inst->_has($method) if $inst->protocol >= 2;
    return $HAS{$method} ? 1 : 0;
}

sub _has {
    my $inst = shift;
    my $method = shift;
    my $send = $inst->encode_record("HAS", $method);
    $inst->sendmsg($send);
    my $msg = $inst->recvmsg;
    return 0 if $msg and $msg eq 'NOK';
    return 1;
}

#=============================================================================
# API methods which probably should be overridden in subclasses.
#=============================================================================
sub init { }
sub fini { }

# Transmit files to the backend installer agent. This is used for remote
# installers, which must copy the files to the remote machine in order to
# install them. Local installers can just use the local copy.
sub pkginit  { Ok() }	# creates any temporary directory needed
sub transmit { Ok() }	# transmits files (into the temp dir)
sub pkgfini  { Ok() }	# removes the temporary directory

#=============================================================================
# API methods which _may_ be overridden in subclasses.
#=============================================================================
sub connect_to {
    my $inst = shift;
    my $addr = shift;
    my $port = shift;
    my ($iaddr, $paddr, $proto);
    $iaddr = inet_aton($addr)
      or return Error("no host: $addr");
    $paddr = sockaddr_in($port, $iaddr);
    $proto = getprotobyname('tcp');
    socket($inst->{SOCK}, PF_INET, SOCK_STREAM, $proto)
      or return Error("socket: $!");
    connect($inst->{SOCK}, $paddr)
      or return Error("connect: $!");
    select((select($inst->{SOCK}), $| = 1)[0]);
    Ok();
}

sub query {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("QUERY", @_));
    my $msg = $inst->recvmsg;
    return Error($inst->error_str) if $msg eq 'NOK';
    my@l = map { PPM::PPD->new($_) } 
	   map { chomp; $inst->decode_record($_) }	# flattens fields
	   split(/^/, $msg);
    List(@l);
}

sub properties {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("PROPERTIES", @_));
    my $recv = $inst->recvmsg;
    if ($recv eq 'NOK') {
	return Error($inst->error_str);
    }
    my ($ppd, @other) = $inst->decode_record($recv);
    return List(PPM::PPD->new($ppd), @other);
}

sub precious {
    my $inst = shift;
    $inst->sendmsg("PRECIOUS");
    List($inst->decode_record($inst->recvmsg));
}

sub bundled {
    my $inst = shift;
    $inst->sendmsg("BUNDLED");
    List($inst->decode_record($inst->recvmsg));
}

sub dependents {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("DEPENDENTS", @_));
    my $recv = $inst->recvmsg;
    return Error($inst->error_str) if $recv eq 'NOK';
    return List() if not defined (($inst->decode_record($recv))[0]);
    return List($inst->decode_record($recv));
}

sub remove {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("REMOVE", @_));
    return Ok() if $inst->recvmsg eq 'OK';
    return Error($inst->error_str);
}

sub install {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("INSTALL", @_));
    return Ok() if $inst->recvmsg eq 'OK';
    return Error($inst->error_str);
}

sub upgrade {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("UPGRADE", @_));
    return Ok() if $inst->recvmsg eq 'OK';
    return Error($inst->error_str);
}

sub config_info {
    my $inst = shift;
    $inst->sendmsg("CONFIG_INFO");
    my %info = map { chomp; $inst->decode_record } split /^/, $inst->recvmsg;
    return List(map { [$_, $info{$_}] } sort keys %info);
}

sub config_keys {
    my $inst = shift;
    $inst->sendmsg("CONFIG_KEYS");
    my %keys = map { chomp; $inst->decode_record } split /^/, $inst->recvmsg;
    return List(map { [$_, $keys{$_}] } sort keys %keys);
}

sub config_get {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("CONFIG_GET", @_));
    my $value = $inst->recvmsg;
    return Error($inst->error_str)
      if $value eq 'NOK';
    return Ok($value);
}

sub config_set {
    my $inst = shift;
    $inst->sendmsg($inst->encode_record("CONFIG_SET", @_));
    return Ok() if $inst->recvmsg eq 'OK';
    return Error($inst->error_str);
}

sub protocol {
    my $inst = shift;
    my $v = $inst->config_get("PROTOCOL");
    return $v->result if $v->ok;
    return 1; # version 1 didn't expose the PROTOCOL variable.
}

# This method was not supported in the first version of the PPM3 backend. To
# compensate, we'll do a lousy emulation: if the languages are the same, and
# the required version is lexically less than the PERLCORE exposed by the
# backend, we'll allow the installation to proceed.
sub can_install {
    my $inst = shift;
    unless ($inst->has('can_install')) {
	my ($lang, $version, $compat) = @_;
	return Ok(1) if (
	    lc $lang eq lc $inst->config_get("TARGET_TYPE")->result and
	    lc $version lt lc $inst->config_get("PERLCORE")->result
	);
	return Ok(0);
    }
    $inst->sendmsg($inst->encode_record("CAN_INSTALL", @_));
    my $result = $inst->recvmsg;
    return Error($inst->error_str) if $result eq 'NOK';
    return Ok($result);
}

sub error_str {
    my $inst = shift;
    $inst->sendmsg("ERROR_STR");
    $inst->recvmsg;
}

#=============================================================================
# Non-API methods. Touch not, lest ye be smacked.
#=============================================================================
use constant FIELD_SEP => "\001";
use constant FIELD_UNDEF => "\002";
my $EOL = "\015\012";
sub sendmsg {
    my $inst = shift;
    my $msg = shift;
    my $fd = $inst->{SOCK};
    {
	local $\ = "$EOL.$EOL";
	print $fd $msg;
    }
}

sub recvmsg {
    my $inst = shift;
    local $/ = "$EOL.$EOL";
    my $fd = $inst->{SOCK};
    chomp(my $msg = <$fd>);
    return $msg;
}

sub qmeta {
    local $_ = shift || $_;
    s{([^A-Za-z0-9])}{sprintf('\x%.2X',ord($1))}eg;
    $_;
}

sub uqmeta {
    local $_ = shift || $_;
    $_ = eval qq{qq{$_}};
    warn $@ if $@;
    $_;
}

sub encode_record {
    my $o = shift;
    my @fields = map { my $a = defined $_ ? $_ : FIELD_UNDEF; qmeta($a) } @_;
    join FIELD_SEP, @fields;
}

sub decode_record {
    my $o = shift;
    my $t = shift || $_;
    return map { $_ = &uqmeta; $_ = undef if $_ eq FIELD_UNDEF; $_ }
	   split(FIELD_SEP, $t, -1);
}

1;
