package PPM::UI;

use strict;
use Data::Dumper;
use PPM::Repository;
use PPM::Installer;
use PPM::Config;
use PPM::Trace qw(trace);
use PPM::Result qw(Ok Warning Error List);

$PPM::UI::VERSION = '3.00';

my $CONF;
my $TARG;
my $REPS;

#=============================================================================
# Name indexes into arrays:
#=============================================================================
use constant PROP_PPD_OBJ => 0;
use constant PROP_INSTDATE => 1;
use constant PROP_REPOS => 2;

use constant QUERY_NAME => 0;
use constant QUERY_VERSION => 1;
use constant QUERY_ABSTRACT => 2;
use constant QUERY_AUTHOR => 3;

use constant CONF_INFO_KEY => 0;
use constant CONF_INFO_VAL => 1;

use constant CONF_KEYS_KEY => 0;
use constant CONF_KEYS_RW => 1;

use constant REP_INFO_NAME => 0;
use constant REP_INFO_LOC => 1;
use constant REP_INFO_TYPE => 2;

#=============================================================================
# Configuration Options
#=============================================================================

my %config_keys;
BEGIN {
   %config_keys = (tempdir => 1,
		   downloadbytes => 1,
		   tracefile => 1,
		   tracelvl => 1,
		   profile  => 1,
		   profile_server => 0,
		   profile_enable => 1,
		  );
}

sub config_keys {
    trace(3, "PPM::UI::config_keys()\n");
    List(map { [$_ => $config_keys{$_}] } keys %config_keys);
}

sub config_info {
    trace(3, "PPM::UI::config_info()\n");
    List(map { [$_ => $CONF->{DATA}{$_}] } keys %config_keys);
}

sub config_set {
    my $key = shift;
    my $val = shift;
    trace(3, "PPM::UI::config_set($key, $val)\n");
    return Error("no such config key '$key'")
      unless exists $config_keys{$key};
    return Error("read-only configuration key '$key'")
      unless $config_keys{$key};
    $CONF->{DATA}{$key} = $val;
    $CONF->save;
    return Ok();
}

sub config_get {
    my $key = shift;
    trace(3, "PPM::UI::config_get($key)\n");
    return Error("no such config key '$key'.")
      unless exists $config_keys{$key};
    return Ok($CONF->{DATA}{$key});
}

#=============================================================================
# Repositories
#=============================================================================

sub repository_list {
    trace(3, "PPM::UI::repository_list()\n");
    return List(sort keys %{$REPS->{DATA}});
}

sub repository_add {
    my $name = shift;
    my $location = shift;
    my $username = shift;
    my $password = shift;
    trace(3, "PPM::UI::repository_add($name, $location, ", $username,
	  ", ", $password, ")\n");

    # Validate it:
    my ($r, $er) = PPM::Repository->new($location, $name, $username, $password);
    return Error($er)
      unless $r;

    $REPS->{DATA}{$name}{url} = $location;
    $REPS->{DATA}{$name}{username} = $username if defined $username;
    $REPS->{DATA}{$name}{password} = $password if defined $password;
    $REPS->save;
    return Ok();
}

sub repository_del {
    my $name = shift;
    trace(3, "PPM::UI::repository_del($name)\n");
    return Error("Can't delete nonexistent repository '$name'.")
      unless exists $REPS->{DATA}{$name};
    delete $REPS->{DATA}{$name};
    $REPS->save;
    del_rep($name);
    return Ok();
}

sub repository_rename {
    my $oldname = shift;
    my $newname = shift;
    trace(3, "PPM::UI::repository_rename($oldname, $newname)\n");
    $REPS->{DATA}{$newname} = $REPS->{DATA}{$oldname}
      if exists $REPS->{DATA}{$oldname};
    repository_del($oldname);
}

sub repository_info {
    my $name = shift;
    trace(3, "PPM::UI::repository_info($name)\n");
    return Error("Can't describe nonexistent repository '$name'.")
      unless exists $REPS->{DATA}{$name};
    my $rep = get_rep($name);
    if ($rep->ok) {
	$rep = $rep->result;
	return List(
	    $name,
	    $rep->location,
	    $rep->type_printable,
	    $rep->username,
	    $rep->password,
	);
    }
    else {
	return Warning($rep->msg_raw,
		       -1,
		       [$name,
			$REPS->{DATA}{$name}{url},
			'unsupported']);
    }
}

#=============================================================================
# Operations on non-installed packages
#=============================================================================

sub search {
    my $rlist = shift;
    @$rlist = map { $_->result } grep { $_->ok } map { get_rep($_) } @$rlist;
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $query = shift;
    my $case = shift;
    my @results;
    for my $r (@$rlist) {
	my $l = $r->search($target, $query, $case);
	next unless $l->ok;
	push @results, map { get_pkg($_, $r) } $l->result_l;
    }
    return List(@results);
}

sub describe {
    my $rlist = shift;
    @$rlist = map { $_->result } grep { $_->ok } map { get_rep($_) } @$rlist;
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $pkg = get_pkg(shift, $rlist);
    my $package = $pkg->name;
    my $desc = $pkg->describe($target);
    return $desc unless $desc->ok;
    return Ok(get_pkg($desc->result, $rlist));
}

sub install {
    my $rlist = shift;
    @$rlist = map { $_->result } grep { $_->ok } map { get_rep($_) } @$rlist;
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $pkg = get_pkg(shift, $rlist);
    my $opts = shift;
    my $status_cb = shift;

    # Find the correct target for this package. This means matching the
    # LANGUAGE tag in the PPD. Basically we find out what LANGUAGE the PPD
    # represents, and we search through the targets looking for a subset which
    # implement that language. If more than one target implements the language
    # and version, we pick the first. If none work, we fail. If the LANGUAGE
    # tag is missing, or the LANGUAGE matches the given target, we use the
    # given target.
    $target = $pkg->choose_target($target, target_list()->result_l);
    return $target unless $target->ok;
    $target = $target->result;
    install_or_upgrade($rlist, $target, $pkg, $opts, $status_cb, 'install');
}

#=============================================================================
# Targets
#=============================================================================

sub target_list {
    trace(3, "PPM::UI::target_list()\n");
    return List(sort keys %{$TARG->{DATA}});
}

sub target_info {
    my $target = shift;
    trace(3, "PPM::UI::target_info($target)\n");
    my $t = get_targ($target);
    return $t unless $t->ok;
    $t = $t->result;
    my @keys = map { "\u$_" } $t->ckeys;
    my @vals = $t->cvals;
    my %hash;
    @hash{@keys} = @vals;
    return Ok(\%hash);
}
sub target_raw_info {
    my $target = shift;
    return Ok($TARG->{DATA}{$target});
}

sub target_config_info {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    trace(3, "PPM::UI::target_config_info(", $target->name, ")\n");
    return $target->config_info;
}

sub target_config_keys {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    trace(3, "PPM::UI::target_config-keys(", $target->name, ")\n");
    return $target->config_keys;
}

sub target_config_get {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $key = shift;
    trace(3, "PPM::UI::target_config_get(", $target->name, ", $key)\n");
    return $target->config_get($key);
}

sub target_config_set {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $key = shift;
    my $value = shift;
    trace(3, "PPM::UI::target_config_get(", $target->name, ", $key, $value)\n");
    return $target->config_set($key, $value);
}

sub target_rename {
    my $oldname = shift;
    my $newname = shift;

    # Make sure the target even exists:
    my @targets = target_list()->result_l;
    return Error("Can't rename nonexistent target '$oldname'.")
      unless grep { $_ eq $oldname } @targets;

    # Load the targets file read/write:
    {
	my $t = PPM::Config::load_config_file('targets', 'rw');
	$t->{DATA}{$newname} = $t->{DATA}{$oldname};
	delete $t->{DATA}{$oldname};
    }
    $TARG = PPM::Config::load_config_file('targets', 'ro');

    # Return success if profile tracking is disabled.
    return Ok() unless config_get('profile_enable')->result;

    # We must rename the target in all profiles:
    my $res = profile_list();
    unless ($res->is_success) {
	return Error(
	    "failed to rename target in profiles: " . $res->msg_raw
	) unless $res->ok;
    }
    my @profiles = $res->result_l;

    my $repos = get_rep(config_get('profile_server')->result);
    return $repos unless $repos->ok;
    $repos = $repos->result;
    for my $profile (@profiles) {
	my $r = $repos->profile_target_rename($profile, $oldname, $newname);
	return Error(
	    "failed to rename target in profiles: " . $r->msg_raw
	) unless $r->ok;
    }

    Ok();
}

sub target_fix_paths {
    my $from = shift;
    my $to   = shift;
    my $i    = $^O eq 'MSWin32' ? '(?i)' : '';
    {
	my $t    = PPM::Config::load_config_file('targets', 'rw');
	for my $targ (target_list()->result_l) {
	    for my $key (keys %{$t->{DATA}{$targ}}) {
		$t->{DATA}{$targ}{$key} =~ s{$i\Q$from\E}{$to};
	    }
	}
    }
    $TARG = PPM::Config::load_config_file('targets', 'ro');
}

sub target_add {
    my $name = shift;
    my %opts = @_;

    # Handle loading a file:
    if (not defined $name and -f $opts{From}) {
	my $t = PPM::Config->new;
	$t->loadfile($opts{From});
	# There's only ever 1 target in that config file:
	($name) = keys %{$t->{DATA}};
	%opts = %{$t->{DATA}{$name}};
    }
    return Error("can't add existing target '$name'")
	if exists $TARG->{DATA}{$name};

    # Find an unused Port:
    require PPM::Compat;
    $opts{port} = PPM::Compat::PPM_PORT_PERL();
    ++$opts{port} while (
	grep { $opts{port} eq $TARG->{DATA}{$_}{port} }
	keys %{$TARG->{DATA}}
    );

    # Save the file:
    {
	my $t = PPM::Config::load_config_file('targets', 'rw');
	$t->{DATA}{$name} = \%opts;
    }
    $TARG = PPM::Config::load_config_file('targets', 'ro');
    return Ok();
}

sub target_del {
    my $name = shift;
    return Error("can't delete nonexistent target '$name'")
	unless exists $TARG->{DATA}{$name};
    {
	my $t = PPM::Config::load_config_file('targets', 'rw');
	delete $t->{DATA}{$name};
    }
    $TARG = PPM::Config::load_config_file('targets', 'ro');
    return Ok();
}

#=============================================================================
# Operations on installed packages
#=============================================================================

sub query {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $query = shift;
    my $case = shift;
    trace(3, "PPM::UI::query(", $target->name, ", '$query', $case)\n");
    my @results = map { get_pkg($_) }
		  $target->query($query, $case)->result_l;
    List(@results);
}

sub properties {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $pkg = get_pkg(shift, undef); # don't care about repository.
    trace(3, "PPM::UI::properties(", $target->name, ", ", $pkg->name, ")\n");
    my $res = $target->properties($pkg->name);
    return $res unless $res->ok;
    my @res = $res->result_l;
    $res[0] = get_pkg($res[0]);
    return List(@res);
}

sub remove {
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $pkg = get_pkg(shift, undef); # don't care about repository.
    my $package = $pkg->name;
    my $force = shift;  # normally, if removing a package would break a
			# dependency of another installed package, we refuse.
			# But if the user really wants to...
    my $cb_remove = shift;
    my $verbose = shift;

    trace(3, "PPM::UI::remove(", $target->name, ", $package)\n");
    if (grep { $pkg->name eq $_ } $target->precious->result_l) {
	return Error("package '$package' is required by the target.");
    }
    my $prop = $target->properties($package);
    return $prop unless $prop->ok;

    my $ok = $target->dependents($package);
    return $ok unless $ok->ok;

    my @deps = $ok->result_l;
    if (@deps and not $force) {
	my $msg = "removing '$package' would break these dependencies:\n";
	$msg .= "\t$package is needed by $_.\n" for @deps;
	return Error($msg);
    }

    my $version = ($prop->result_l)[PROP_PPD_OBJ]->version;
    $cb_remove->($package, $version, $target->name, "PRE-REMOVE");
    my $ret = $target->remove($package, $verbose);
    return $ret unless $ret->ok;
    $cb_remove->($package, $version, $target->name, "COMPLETE");
    my $track = config_get('profile_enable')->result;
    if ($track and $ret->ok) {
	my $repos = get_rep(config_get('profile_server')->result);
	return $repos unless $repos->ok;
	$repos = $repos->result;
	my $rep = ($prop->result_l)[PROP_REPOS];
	my $ver = ($prop->result_l)[PROP_PPD_OBJ]->version_osd;
	my $entry = [$rep,
		     $target->config_get('TARGET_TYPE')->result,
		     $target->name,
		     $package,
		     $ver
		    ];
	my $profile = config_get('profile')->result;
	$repos->removed($profile, $entry);
    }
    $ret;
}

sub verify {
    my $rlist = shift;
    @$rlist = map { $_->result } grep { $_->ok } map { get_rep($_) } @$rlist;
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $pkg = get_pkg(shift, $rlist);
    my $package = $pkg->name;
    trace(3, "PPM::UI::verify(", $target->name, ", $package)\n");

    # To do: 
    # 1. Check if the package is installed; return false otherwise.
    my $prop = $target->properties($package);
    return $prop unless $prop->ok;
    my @prop = $prop->result_l;

    my $bundled  = grep { $prop[PROP_PPD_OBJ]->name eq $_ }
    			$target->bundled->result_l;
    my $precious = grep { $prop[PROP_PPD_OBJ]->name eq $_ }
    			$target->precious->result_l;

    # 2. Get the installed version of the package.
    my $ver = $prop[PROP_PPD_OBJ]->version_osd;
    my $ver_p = $prop[PROP_PPD_OBJ]->version;

    # 3. Send the installed version to the Repository for checking.
    # XXX
    # I used to only upgrade from the place it came from. Now it will come
    # from the current repository.
    my $res = $pkg->uptodate($target);
    unless ($res->ok) {
	return Error("bundled package - no upgrade available")
	  if $bundled;
	return $res;
    }
    my ($uptodate, $newversion) = $res->result_l;

    # 4. Return uptodate(t/f), newversion, oldversion, bundled(t/f):
    return List($uptodate, $bundled, $precious, $newversion, $ver_p);
}

sub upgrade {
    my $rlist = shift;
    @$rlist = map { $_->result } grep { $_->ok } map { get_rep($_) } @$rlist;
    my $target = get_targ(shift);
    return $target unless $target->ok;
    $target = $target->result;
    my $pkg = get_pkg(shift, $rlist);
    my $package = $pkg->name;
    my $opts = shift;
    my $status_cb = shift;

    trace(3, "PPM::UI::upgrade(", $target->name,
	  ", $package, $opts->{force}, $opts->{follow}, $opts->{dryrun})\n");

    install_or_upgrade($rlist, $target, $pkg, $opts, $status_cb, 'upgrade');
}

#=============================================================================
# Operations which require you to have logged in
#=============================================================================

sub profile_set {
    my $profile = shift;
    trace(3, "PPM::UI::profile_set($profile)\n");
    config_set('profile', $profile);
    Ok();
}

sub profile_get {
    trace(3, "PPM::UI::profile_get()\n");
    config_get('profile');
}

sub profile_list {
    trace(3, "PPM::UI::profile_list()\n");
    my $p_rep = config_get('profile_server')->result;
    my $rep = get_rep($p_rep);
    return $rep unless $rep->ok;
    $rep = $rep->result;
    $rep->profile_list;
}

sub profile_add {
    my $profile = shift;
    trace(3, "PPM::UI::profile_add($profile)\n");
    my $p_rep = config_get('profile_server')->result;
    my $rep = get_rep($p_rep);
    return $rep unless $rep->ok;
    $rep = $rep->result;
    $rep->profile_add($profile);
}

sub profile_del {
    my $profile = shift;
    trace(3, "PPM::UI::profile_del($profile)\n");
    my $p_rep = config_get('profile_server')->result;
    my $rep = get_rep($p_rep);
    return $rep unless $rep->ok;
    $rep = $rep->result;
    $rep->profile_del($profile);
}

sub profile_restore {
    my $profile = shift;
    my $status_cb = shift;
    my $remove_cb = shift;
    my $force = shift;
    my $follow = shift;
    my $dry = shift;
    my $clean_pkgs = shift;

    trace(3, "PPM::UI::profile_restore($profile, CODEREF, ",
	  "$force, $follow, $dry, $clean_pkgs)\n");

    my $p_rep = config_get('profile_server')->result;
    my $rep = get_rep($p_rep);
    return $rep unless $rep->ok;
    $rep = $rep->result;

    # 1. Download the profile_info() from the repository
    my $res = $rep->profile_info($profile);
    return $res unless $res->ok;

    my %packages;

    # 2. For each package in profile_info(), upgrade (or install)
    for my $entry ($res->result_l) {
	my ($repos, $targ_type, $targ_name, $package, $version) = @$entry;
	my $rep = get_rep($repos);
	return $rep unless $rep->ok;
	$rep = $rep->result;
	my $targ = get_targ($targ_name)->result;
	next unless $targ;	# skip unknown targs

	$packages{$targ->name}{$package} = $version;

	my $prop = properties($targ_name, $package);
	next if ($prop->ok
		 and ($prop->result_l)[PROP_PPD_OBJ]->version_osd eq $version);

	if ($dry) {
	    my $version = PPM::PPD::printify($version);
	    $status_cb->($package, $version, $targ->name,
			 'PRE-INSTALL', 0, 0, 0);
	}
	else {
	    remove($targ_name, $package, 1, $remove_cb)
	      if $prop->ok;
	    install($rep, $targ_name, $package, $force, $follow, $dry, $status_cb);
	}
    }

    return Ok() unless $clean_pkgs;

    # 3. Now query each target and make sure it only contains the packages we
    # just installed (if clean_pkgs is set):
    for my $target (keys %packages) {
	my @precious = get_targ($target)->result->precious->result_l;
	my $q = query($target, '*', 0);
	next unless $q->ok;
	for my $pkg ($q->result_l) {
	    next if exists $packages{$target}{$pkg->name};
	    next if grep { $pkg->name eq $_ } @precious;
	    if ($dry) {
		$remove_cb->($pkg->name, $pkg->version, $target);
	    }
	    else {
		remove($target, $pkg->name, 1, $remove_cb);
	    }
	}
    }

    return Ok();
}

sub profile_save {
    my $name = shift;
    trace(3, "PPM::UI::profile_save($name)\n");

    my $p_rep = config_get('profile_server')->result;
    my $rep = get_rep($p_rep);
    return $rep unless $rep->ok;
    $rep = $rep->result;

    # 1. Get the "query *" information from all current targets.
    my @entries;

    # First, get the targets:
    my @targets = map { get_targ($_)->result } target_list()->result_l;
    for my $targ (@targets) {

	# Now get information about that target:
	my $targ_t = $targ->config_get("TARGET_TYPE")->result;
	my $targ_name = $targ->name;

	# Now get the packages:
	my @pkgs = query($targ, '*', 0)->result_l;
	for my $pkg (@pkgs) {
	    my $obj = $pkg->getppd_obj;
	    next unless $obj->ok;
	    my $prop = properties($targ, $pkg->name);
	    my $repos = ($prop->result_l)[PROP_REPOS];
	    my $entry = [$repos,
			 $targ_t,
			 $targ_name,
			 $pkg->name,
			 $obj->result->version_osd,
			];
	    push @entries, $entry;
	}
    }

    # 2. Upload the information to the Repository.
    $rep->profile_save($name, @entries);
}

sub profile_info {
    my $name = shift;
    trace(3, "PPM::UI::profile_info($name)\n");
    my $p_rep = config_get('profile_server')->result;
    my $rep = get_rep($p_rep);
    return $rep unless $rep->ok;
    $rep = $rep->result;
    my $res = $rep->profile_info($name);
    return $res unless $res->ok;
    my @lst = $res->result_l;
    my @ret;
    for (@lst) {
	my $ent = [@$_[qw(3 4 2)]];
	$ent->[1] = PPM::PPD::printify($ent->[1]);
	push @ret, $ent;
    }
    List(@ret);
}

sub profile_rename {
    my $oldname = shift;
    my $newname = shift;

    # Make sure the profile actually exists:
    my @profiles = profile_list()->result_l;
    return Error("Can't rename nonexistent repository '$oldname'.")
      unless grep { $oldname eq $_ } @profiles;

    # Tell the server to rename the profile:
    my $repos = get_rep(config_get('profile_server')->result);
    return $repos unless $repos->ok;
    $repos = $repos->result;
    $repos->profile_rename($oldname, $newname);
}

#=============================================================================
# Utilities
#=============================================================================
sub install_or_upgrade {
    my $rlist = shift;      # A list of repositories to search in order
    my $target = shift;
    my $package = shift;
    my $opts = shift;
    my %opts = %$opts;
    my $status_cb = shift;
    my $event_name = shift;

    my $do_install = sub {
	my $pkg = shift;

	# Download the PPD and package tarball:
	my $pkg_obj = $pkg->getppd_obj($target)->result;
	if ($opts{dryrun}) {
	    $status_cb->($pkg->name, $pkg_obj->version,
			 $target->name, 'PRE-INSTALL', 0, 0, 0);
	    return Ok(); # do nothing, successfully
	}
	my $location = $pkg->getppm($target,
				    config_get("tempdir")->result,
				    $status_cb,
				    config_get("downloadbytes")->result,
				   );

	# update ERR appropriately, and fail.
	return $location unless $location->ok;

	# Send the install (or update) event to the backend:
	my $err = $target->$event_name($pkg_obj->name,
				       $location->result,
				       $pkg_obj->ppd,
				       $pkg->rep->location,
				       $opts{verbose},
				      );
	return $err unless $err->ok;

	$status_cb->($pkg->name, $pkg_obj->version, $target->name, "COMPLETE");

	# Track the profile:
	my $track = config_get('profile_enable')->result;
	if ($track) {
	    my $p_rep = get_rep(config_get('profile_server')->result);
	    my $ok = $p_rep;
	    my $profile = config_get('profile')->result;
	    if ($p_rep->ok) {
		$p_rep = $p_rep->result;
		my $entry = [$pkg->rep->location,
			     $target->config_get('TARGET_TYPE')->result,
			     $target->name,
			     $pkg_obj->name,
			     $pkg_obj->version_osd,
			    ];
		if ($event_name eq 'install') {
		    $ok = $p_rep->installed($profile, $entry);
		}
		else {
		    $ok = $p_rep->upgraded($profile, $entry);
		}
	    }
	    unless ($ok->is_success) {
		my $warning = Warning("Profile '$profile' may not be in sync. "
			       . $ok->msg_raw);
		return $warning;
	    }
	}
	return Ok();
    };

    # We can shortcut the prerequisite check if we're ignoring that:
    return $do_install->($package)
      if ($opts{force} and not $opts{follow});

    my $warning = Ok();
    my @pkgs = ($package);
    my %done;

  PACKAGE:
    while (@pkgs) {
	my $pkg = shift @pkgs;

	# If the package is up to date (and $force isn't set), return.
	my $prop = properties($target, $pkg);
	if ($prop->ok) {
	    my $u2d = $pkg->uptodate($target);

	    # If the server doesn't have that package available, we'll
	    # _assume_ it's up to date, issuing a warning to that effect
	    my $uptodate = 1;
	    if ($u2d->ok) {
		($uptodate) = $u2d->result_l;
	    }
	    else {
#		print "NOTE: package " . $p->name . " not on server...\n";
#		print Dumper $p, $u2d;
#		print Dumper \@pkgs;
		next PACKAGE;
	    }
	    next PACKAGE if ($uptodate and not $opts{force});
	}

	# Try to get a list of prerequisites for the package:
	my @missing;
	my $ppd_ref = $pkg->getppd_obj($target);
	return $ppd_ref unless $ppd_ref->ok;
	my $impl = $ppd_ref->result->find_impl($target);
	return $impl unless $impl->ok;
	
	# Get a list of prerequisites from the implementation:
	my @prereqs = grep { not $done{$_->name} } $impl->result->prereqs;

	# We can shortcut the cross-checking of prereqs if we're forcing the
	# install of any prereqs:
	if ($opts{force} and $opts{follow} and @prereqs) {
	    unshift @pkgs, (map { get_pkg($_->name, $rlist) } @prereqs), $pkg;
	    next PACKAGE;
	}
	
	# Check each prerequisite to see if it's installed.
	else {
	    for my $pre (@prereqs) {
		my $prop = $target->properties($pre->name);
		push @missing, $pre->name and next
		  unless $prop->ok;
		my $ver = ($prop->result_l)[PROP_PPD_OBJ]->version_osd;
		my $ok = $pkg->uptodate($target, $pre->name, $ver);
		push @missing, $pre->name
		  if ($ok->ok and not (($ok->result_l)[0]));
	    }
	    if (@missing and not $opts{force} and not $opts{follow}) {
		return Error(
		    "can't install package '", $pkg->name,
		    "': missing prereqs @missing."
		);
	    }
	    elsif (@missing) {
		unshift @pkgs, (map { get_pkg($_, $rlist) } @missing), $pkg;
		next PACKAGE;
	    }
	}

	# Install the durned package
	my $res = $do_install->($pkg);
	return $res unless $res->ok;
	$warning = $res unless $res->is_success;
	$done{$pkg->name}++;
    }
    return $warning;
}

#=============================================================================
# These utilities make it easier for clients of this class to find out
# information about packages. Any subroutine which takes the name of a package
# can now take either a URL or a filename. That means clients may want to know
# this!
#=============================================================================
sub get_pkg {
    my $pkg = shift;
    my $rep = shift;
    return $pkg if eval { $pkg->isa('PPM::Package') };
    PPM::Package->new($pkg, $rep);
}

sub pkg_type {
    my $pkg = get_pkg(shift, undef); # not going to use the repository
    return $pkg->type;
}

sub is_pkg {
    my $pkg = shift;
    return 1 if eval { $pkg->isa('PPM::Package') };
    my $p = PPM::Package->new($pkg);
    return 0 if $p->type eq 'UNKNOWN';
    1;
}

#=============================================================================
# Cache of "active" repositories and targets:
#=============================================================================
my %open_repositories;
sub get_rep {
    my $rep = shift;
    trace(3, "PPM::UI::get_rep($rep)\n");
    return Ok($rep) if eval { $rep->isa("PPM::Repository") };
    return Ok($open_repositories{$rep})
      if exists $open_repositories{$rep};
    my ($url,$name,$pass) = exists $REPS->{DATA}{$rep}
			  ? @{$REPS->{DATA}{$rep}}{qw(url username password)}
			  : ($rep, undef, undef);

    my @ok = PPM::Repository->new($url, $rep, $name, $pass);
    return Error($ok[1]) unless $ok[0];
    $open_repositories{$rep} = $ok[0];
    Ok($ok[0]);
}
sub del_rep {
    my $rep = shift;
    delete $open_repositories{$rep};
}

my %open_installers;
sub get_targ {
    my $targ = shift;
    $targ = '' unless defined $targ;
    trace(3, "PPM::UI::get_targ($targ)\n");
    return Ok($targ) if eval { $targ->isa("PPM::Installer") };
    return Ok($open_installers{$targ})
      if exists $open_installers{$targ};
    return Error("Target '$targ' not found")
      unless exists $TARG->{DATA}{$targ};
    my $t = $TARG->{DATA}{$targ};
    my @r = PPM::Installer->new($targ, $t);
    return Error($r[1]) unless $r[0];
    $open_installers{$targ} = $r[0];
    Ok($r[0]);
}

#=============================================================================
# Settings persistence
#=============================================================================
BEGIN {
    $CONF = PPM::Config::load_config_file('clientlib');
    $REPS = PPM::Config::load_config_file('repositories');
    $TARG = PPM::Config::load_config_file('targets', 'ro');

    # Start up the trace if it's needed:
    my $tracelvl = config_get('tracelvl');
    if ($tracelvl->result && $tracelvl->result > 0) {
	PPM::Trace::trace_init(config_get('tracefile')->result,
			       config_get('tracelvl')->result);
    }
}

package PPM::Package;
use strict;
use PPM::Result qw(Ok Warning Error List);
use URI;
use Data::Dumper;

sub new {
    my $class = shift;
    my $name  = shift;
    my $rep   = shift;
    my $o = bless {}, ref($class) || $class;
    $rep = [] if not defined $rep;

    # A PPM::PPD object
    if (eval { $name->isa('PPM::PPD') }) {
	$o->{type}    = 'PPM::PPD';
	$o->{name}    = $o->{rawname} = $name->name;
	$o->{id}      = $name->id;
	$o->{current_rep} =
	    defined $name->repository ? $name->repository :
	    ref $rep eq 'ARRAY'       ? $rep->[0]         : $rep;
	$o->{reps}    = [$o->{current_rep}];
	$o->{obj}     = Ok($name);
    }
    # A URL:
    elsif ($name =~ m{(^[^:]{2,}://.+)/([^/]+)\.ppd}i) {
	$o->{type}    = 'WWW';
	$o->{rawname} = $o->{id} = $2;
	my $rep       = PPM::UI::get_rep($1);
	$o->{uri}     = URI->new($name);
	die "Can't parse PPD location $name: " . $rep->msg
	  unless $rep->is_success;
	$o->{reps}    = [$rep->result];
	my $ppd_obj   = $o->getppd_obj(undef); # undef'd target...
	$o->{name}    = $o->{rawname} unless $ppd_obj->ok;
	$o->{name}    = $ppd_obj->result->name;
    }
    # A filename:
    elsif ($name =~ m{((?:^[A-Z]:[\\/]|[\\/]{2}[^\\/]+)?[^:]*[\\/])?([^/]+)\.ppd}i) {
	$o->{type}    = 'FILE';
	$o->{rawname} = $o->{id} = $2;
	my $dir = $1;
	$dir =~ s{[\\/]+$}{} if $dir and $dir !~ m{^[\\/]+$};
	my $rep = PPM::UI::get_rep($dir || '.');
	die "Can't parse PPD location $name: " . $rep->msg
	  unless $rep->is_success;
	$o->{reps}  = [$rep->result];
	my $ppd_obj = $o->getppd_obj(undef); # undef'd target...
	if ($ppd_obj->ok) {
	    $o->{name} = $ppd_obj->result->name;
	}
	else {
	    $o->{name} = $o->{rawname} unless $ppd_obj->ok;
	}
    }
    # A plain package name:
    elsif ($name =~ m{^[-_A-Za-z0-9]+$}) {
	$o->{type} = 'PKG';
	$o->{name} = $o->{rawname} = $name;
	$o->{reps} = ref($rep) eq 'ARRAY' ? $rep : [$rep];
    }
    # Something else:
    else {
	#print STDERR "WARNING: could not parse package name '$name'.\n";
	$o->{type} = 'UNKNOWN';
	$o->{name} = $o->{rawname} = $name;
	$o->{reps} = ref($rep) eq 'ARRAY' ? $rep : [$rep];
    }
    $o->{id} = $o->{rawname} unless defined $o->{id};

    return $o;
}

sub name {
    my $o = shift;
    $o->{name};
}

sub reps {
    my $o = shift;
    @{$o->{reps}};
}

sub rep {
    my $o = shift;
    $o->{current_rep};
}

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

sub uri {
    my $o = shift;
    $o->{uri};
}

# Forces a refresh of the {obj} or {desc} fields if they are not marked as
# complete by the PPM::Repository client.
sub make_complete {
    my $o = shift;
    my $targ = PPM::UI::get_targ(shift)->result;
    my $obj = $o->getppd_obj($targ);
    return if $obj->ok && $obj->result->is_complete;
    delete @$o{qw(obj desc)};
    $o->{obj} = $o->{desc} = $o->getppd_obj($targ);
}

# Find the first repository containing the package, and report whether the
# package is up-to-date w.r.t that repository.
sub uptodate {
    my $o = shift;
    my $target = PPM::UI::get_targ(shift)->result;
    my $desc = $o->describe($target);
    if ($desc and $desc->ok) {
	my $u2d = $o->{current_rep}->uptodate(
	    $target,
	    $o->{id},
	    $desc->result->version_osd,
	);
	return Ok(0) unless $u2d->ok;
	return Ok($u2d->result);
    }
    Error("package $o->{rawname} not found in repositories");
}

sub describe {
    my $o = shift;
    my $target = PPM::UI::get_targ(shift)->result;
    return $o->{obj}  if $o->{type} eq 'PPM::PPD';
    return $o->{desc} if $o->{desc};
    unless ($o->{desc}) {
	for my $rep (@{$o->{reps}}) {
	    $o->{current_rep} = $rep;
	    $o->{desc} = $rep->describe($target, $o->{id});
	    last if $o->{desc}->ok;
	}
    }
    $o->{desc};
}

sub getppd_obj {
    my $o = shift;
    my $target = PPM::UI::get_targ(shift)->result;
    unless ($o->{obj}) {
	for my $rep (@{$o->{reps}}) {
	    $o->{current_rep} = $rep;
	    $o->{obj} = $rep->getppd_obj($target, $o->{id});
	    last if $o->{obj}->ok;
	}
    }
    $o->{obj}
}

sub getppd {
    my $o = shift;
    my $target = PPM::UI::get_targ(shift)->result;

    # If the current object already has a complete PPD, use it
    return Ok($o->{obj}->result->ppd) if (
	$o->{obj} and
	$o->{obj}->ok and
	$o->{obj}->result->is_complete and
	$o->{obj}->result->ppd
    );

    # Otherwise download a fresh one
    my $ppd;
    for my $rep (@{$o->{reps}}) {
	$o->{current_rep} = $rep;
	$ppd = $rep->getppd($target, $o->{id});
	last if $ppd->ok;
    }
    $ppd;
}

sub getppm {
    my $o = shift;
    my $target = PPM::UI::get_targ(shift)->result;
    my $ppm;
    for my $rep (@{$o->{reps}}) {
	$o->{current_rep} = $rep;
	$ppm = $rep->getppm($target, $o->{id}, @_);
	last if $ppm->ok;
    }
    $ppm;
}

# Find the correct target for this package. This means matching the
# LANGUAGE tag in the PPD. Basically we find out what LANGUAGE the PPD
# represents, and we search through the targets looking for a subset which
# implement that language. If more than one target implements the language
# and version, we pick the first. If none work, we fail. If the LANGUAGE
# tag is missing, or the LANGUAGE matches the given target, we use the
# given target.
# NOTE: because LANGUAGE is a child-node of IMPLEMENTATION, we _first_ have to
# search for an implementation that matches the target, _then_ we have to
# verify that the target supports the LANGUAGE tag. If it does, we return it,
# otherwise we go on to the next target.
sub choose_target {
    my $o = shift;
    for (@_) {
	# Load the target:
	my $target = PPM::UI::get_targ($_);
	next unless $target->ok;
	$target = $target->result;

	# Load the PPD and find a suitable implementation for this target:
	my $ppd = $o->getppd_obj($target);
	return $ppd unless $ppd->ok;	# the package doesn't exist.
	my $impl = $ppd->result->find_impl($target);
	next unless $impl->ok;
	my $lang = $impl->result->language;

	# Older PPDs didn't have a LANGUAGE tag, so we must assume a Perl
	# implementation. For old-times' sake, we'll assume version 5.005 is
	# required.
	unless (defined $lang) {
	    $lang = PPM::PPD::Language->new({
		NAME	=> 'Perl',
		VERSION	=> '5.6.0',
	    });
	}

	# Check if this implementation's language is understood by the target:
	my $match = $lang->matches_target($target);
	return $match unless $match->ok;
	return Ok($target) if $match->result;
    }
    return Error(
	"no suitable installation target found for package $o->{name}."
    );
}

1;
