#!/usr/bin/env perl
#***************************************************************************
#*   Copyright (C) 2008-2009 by Eugene V. Lyubimkin                        *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU General Public License                  *
#*   (version 3 or above) as published by the Free Software Foundation.    *
#*                                                                         *
#*   This program is distributed in the hope that it will be useful,       *
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#*   GNU General Public License for more details.                          *
#*                                                                         *
#*   You should have received a copy of the GNU GPL                        *
#*   along with this program; if not, write to the                         *
#*   Free Software Foundation, Inc.,                                       *
#*   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA               *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the Artistic License, which comes with Perl     *
#***************************************************************************

BEGIN {
	unshift @INC, ".";
}

package main;

use 5.10.0;
use warnings;
use strict;

use List::Util qw(sum);
use Getopt::Long;
Getopt::Long::Configure('pass_through', 'no_auto_abbrev', 'no_ignore_case');
use File::Temp qw(tempfile);
use File::stat;

use Cupt::Core;
use Cupt::Config;
use Cupt::Cache;
use Cupt::Cache::Relation qw(stringify_relation_expressions stringify_relation_expression
		parse_relation_line);

my $config;
my $cache;
my $download_progress_class_name = 'Cupt::Download::Progresses::Console';

# build config at start
eval {
	$config = new Cupt::Config;
};
if (mycatch()) {
	myerr("error while loading config");
	exit 1;
}

process_options();

if ($config->var('quiet')) {
	close(STDOUT) or mydie("unable to close standard output");
	open(STDOUT, '>>', '/dev/null') or mydie("unable to turn standard output to /dev/null");
	$download_progress_class_name = 'Cupt::Download::Progress';
}

my $command;
foreach my $idx (0..$#ARGV) {
	if ($ARGV[$idx] !~ m/^-/) {
		# this is not a option, then it's a command
		$command = $ARGV[$idx];
		splice @ARGV, $idx, 1;
		last;
	}
}

myerr("no command specified") and exit 2 if (!defined($command));

my %command_handlers = (
	'config-dump' => \&config_dump,
	'show' => \&show_package_versions,
	'search' => \&search,
	'depends' => sub { show_package_relations('normal') },
	'rdepends' => sub { show_package_relations('reverse') },
	'policy' => \&policy,
	'pkgnames' => \&pkgnames,
	'changelog' => sub { show_changelog_or_copyright('changelog') },
	'copyright' => sub { show_changelog_or_copyright('copyright') },
	'screenshots' => \&screenshots,
	'update' => \&update_release_data,
	'install' => sub { manage_packages('install') },
	'remove' => sub { manage_packages('remove') },
	'purge' => sub { manage_packages('purge') },
	'satisfy' => sub { manage_packages('satisfy') },
	'safe-upgrade' => sub { manage_packages('safe-upgrade') },
	'full-upgrade' => sub { manage_packages('full-upgrade') },
	'clean' => sub { clean_archives(0) },
	'autoclean' => sub { clean_archives(1) },
	'markauto' => sub { mark_autoinstalled('markauto') },
	'unmarkauto' => sub { mark_autoinstalled('unmarkauto') },
);

if (defined($command_handlers{$command})) {
	eval {
		exit ($command_handlers{$command}->() // 0);
	};
	if (mycatch()) {
		myerr("error performing command '%s'", $command);
		exit 3;
	}
} else {
	myerr("unrecognized command '%s'", $command) and exit 2;
}

sub build_cache {
	eval {
		# propagate any parameters passed to Cupt::Cache::&new
		$cache = new Cupt::Cache($config, @_);
	};
	if (mycatch()) {
		myerr("error while creating package cache");
		exit 1;
	}
}

sub check_package_name ($) {
	if (! ($_[0] =~ m/^$package_name_regex$/)) {
		mydie("bad package name '%s'", $_[0]);
	}
}

sub check_version_string ($) {
	if (! ($_[0] =~ m/^$version_string_regex$/)) {
		mydie("bad version string '%s'", $_[0]);
	}
}

sub check_no_extra_args () {
	if (scalar @ARGV > 0) {
		mywarn("extra arguments '%s' are not processed", "@ARGV");
	}
}

sub check_no_more_options () {
	foreach (@ARGV) {
		m/^-/ and mydie("unknown option '%s'", $_);
	}
}

sub get_binary_package ($) {
	my ($package_name) = @_;
	my $package = $cache->get_binary_package($package_name);
	if (defined $package) {
		return $package;
	} else {
		mydie("cannot find binary package '%s'", $package_name);
	}
}

sub select_binary_package_version ($) {
	my ($package_expression) = @_;

	if ($package_expression =~ m/^(.*?)=(.*)/) {
		# selecting by strict version string
		# example: "nlkt=0.3.2.1-1"
		my $package_name = $1;
		check_package_name($package_name);
		my $version_string = $2;
		check_version_string($version_string);
		my $package = get_binary_package($package_name);
		my $version = $package->get_specific_version($version_string);
		# not found
		if (defined $version) {
			return $version;
		} else {
			mydie("cannot find version '%s' for package '%s'", $version_string, $package_name);
		}
	} elsif ($package_expression =~ m/^(.*?)\/(.*)/) {
		# selecting by release distibution
		my $package_name = $1;
		check_package_name($package_name);
		my $distribution_expression = $2;
		if (! ($distribution_expression =~ m/^[a-z-]+$/)) {
			mydie("bad distribution '%s' requested, use archive or codename", $distribution_expression);
		}
		my $package = get_binary_package($package_name);

		# example: "nlkt/sid" or "nlkt/unstable"
		foreach my $version (@{$package->get_versions()}) {
			my @avail_as = @{$version->{avail_as}};

			foreach (@avail_as) {
				if ($_->{release}->{archive} eq $distribution_expression ||
					$_->{release}->{codename} eq $distribution_expression)
				{
					# found such a version
					return $version;
				}
			}
		}
		# not found
		mydie("cannot find distribution '%s' for package '%s'", $distribution_expression, $package_name);
	} else {
		my $result_version = $cache->get_policy_version(get_binary_package($package_expression));
		return $result_version;
	}
}

sub config_dump () {
	check_no_more_options();

	my $particular_key = shift @ARGV;

	check_no_extra_args();

	my $print_key_value = sub {
		my $key = shift;
		my $value = shift;
		defined $value or return;
		print qq/$key "$value"\n/;
	};

	if (defined $particular_key) {
		my $value;
		$value = $config->var($particular_key);
		$value //= "";
		say $value;
	} else {
		my @regular_keys = sort keys %{$config->{regular_vars}};
		foreach my $key (@regular_keys) {
			$print_key_value->($key, $config->{regular_vars}->{$key});
		}

		my @list_keys = sort keys %{$config->{list_vars}};
		foreach my $key (@list_keys) {
			my @values = @{$config->{list_vars}->{$key}};
			foreach (@values) {
				$print_key_value->("${key}::", $_);
			}
		}
	}
}

sub show_package_versions () {
	my $o_with_release_info = 0;
	my $o_installed_only = 0;
	GetOptions(
		'with-release-info' => \$o_with_release_info,
		'installed-only' => \$o_installed_only,
	);

	check_no_more_options();

	my $sub_get_reverse_provides = sub {
		my ($package_name) = @_;

		my $virtual_relation = new Cupt::Cache::Relation("$package_name");
		my $ref_satisfying_versions = $cache->get_satisfying_versions($virtual_relation);
		my @relations;
		foreach (@$ref_satisfying_versions) {
			# we don't need versions of the same package
			my $new_package_name = $_->{package_name};
			$new_package_name ne $package_name or next;

			my $new_relation_version_string = $_->{version_string};
			my $new_relation = new Cupt::Cache::Relation("$new_package_name (= $new_relation_version_string)");
			push @relations, $new_relation;
		}
		return \@relations;
	};

	my @package_expressions = @ARGV;
	scalar @package_expressions or
			mydie("no package expressions specified");

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	foreach my $package_expression (@package_expressions) {
		my $package_name;
		my $ref_versions;
		my $p = sub { print shift, ': ', shift, "\n" };

		if ($config->var('apt::cache::allversions')) {
			$package_name = $package_expression;
			$ref_versions = get_binary_package($package_name)->get_versions();
		} else {
			if (!defined $cache->get_binary_package($package_expression)
				&& $package_expression =~ /^$package_name_regex$/) {
				# there is no such binary binary, maybe it's virtual?
				my $ref_reverse_provides = $sub_get_reverse_provides->($package_expression);
				if (scalar @$ref_reverse_provides) {
					$p->(__('Virtual package, provided by'), stringify_relation_expressions($ref_reverse_provides));
					$ref_versions = [];
				}
			}
			if (!defined $ref_versions) {
				my $version = select_binary_package_version($package_expression);
				$package_name = $version->{package_name};
				$ref_versions = [ $version ];
			}
		}

		foreach my $version (@$ref_versions) {
			$p->(__('Package'), $package_name);
			$p->(__('Version'), $version->{version_string});
			my $installed_status = $cache->get_system_state()->get_status_for_version($version);
			$p->(__('Status'), defined($installed_status) ? $installed_status->{'status'} : "not installed");
			if ($version->is_installed()) {
				my $is_auto_installed = $cache->is_automatically_installed($package_name);
				$p->(__('Automatically installed'), $is_auto_installed ? __('yes') : __('no'));
			}
			$p->(__('Source'), $version->{source_package_name});
			if ($version->{source_version_string} ne $version->{version_string}) {
				$p->(__('Source version'), $version->{source_version_string});
			}
			$p->(__('Essential'), $version->{essential}) if defined($version->{essential});
			$p->(__('Priority'), $version->{priority});
			$p->(__('Section'), $version->{section});
			$p->(__('Size'), human_readable_size_string($version->{size})) if defined($version->{size});
			$p->(__('Uncompressed size'), human_readable_size_string($version->{installed_size}*1024));
			$p->(__('Maintainer'), $version->{maintainer});
			$p->(__('Architecture'), $version->{architecture});
			if ($o_with_release_info) {
				foreach (@{$version->{avail_as}}) {
					next if !defined($_->{release}->{description}); # no release description available
					$p->(__('Release'), $_->{release}->{description});
				}
			}
			$p->(__('Pre-Depends'), stringify_relation_expressions($version->{pre_depends})) if @{$version->{pre_depends}};
			$p->(__('Depends'), stringify_relation_expressions($version->{depends})) if @{$version->{depends}};
			$p->(__('Recommends'), stringify_relation_expressions($version->{recommends})) if @{$version->{recommends}};
			$p->(__('Suggests'), stringify_relation_expressions($version->{suggests})) if @{$version->{suggests}};
			$p->(__('Conflicts'), stringify_relation_expressions($version->{conflicts})) if @{$version->{conflicts}};
			$p->(__('Breaks'), stringify_relation_expressions($version->{breaks})) if @{$version->{breaks}};
			$p->(__('Replaces'), stringify_relation_expressions($version->{replaces})) if @{$version->{replaces}};
			$p->(__('Provides'), join(", ", @{$version->{provides}})) if @{$version->{provides}};
			$p->(__('Enhances'), stringify_relation_expressions($version->{enhances})) if @{$version->{enhances}};
			my $ref_reverse_provides = $sub_get_reverse_provides->($package_name);
			if (scalar @$ref_reverse_provides) {
				$p->(__('Provided by'), stringify_relation_expressions($ref_reverse_provides));
			}
			foreach (map { $_->{'download_uri'} } $version->uris()) {
				$p->(__('URI'), $_);
			}
			$p->(__('MD5'), $version->{md5sum}) if defined($version->{md5sum});
			$p->(__('SHA1'), $version->{sha1sum}) if defined($version->{sha1sum});
			$p->(__('SHA256'), $version->{sha256sum}) if defined($version->{sha1sum});
			$p->(__('Description'), $version->{short_description});
			print $version->{long_description} if defined($version->{long_description});
			$p->(__('Homepage'), $version->{homepage}) if defined($version->{homepage});
			$p->(__('Task'), $version->{task}) if defined($version->{task});
			$p->(__('Tags'), $version->{tags}) if defined($version->{tags});
			print "\n";
		}
	}
}

sub search () {
	# turn off relations parsing, we don't need them
	$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;

	my $o_names_only = 0;
	my $o_installed_only = 0;
	my $o_case_sensitive = 0;
	GetOptions(
		'names-only|n' => sub { $config->set_regular_var('apt::cache::namesonly', 1) },
		'case-sensitive' => \$o_case_sensitive,
		'installed-only' => \$o_installed_only,
	);

	check_no_more_options();
	my @patterns = @ARGV;
	scalar @patterns or
			mydie("no search patterns specified");

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 0);

	my @regexes;
	foreach my $pattern (@patterns) {
		eval {
			push @regexes, ($o_case_sensitive ? qr/$pattern/ : qr/$pattern/i);
		};
		if ($@) {
			# something was bad with regex compilation
			mydie("regular expression '%s' is not valid", $pattern);
		}
	}

	if ($config->var('apt::cache::namesonly')) {
		# search only in package names
		foreach my $package_name (keys %{$cache->get_binary_packages()}) {
			my $matched = 1;
			foreach my $regex (@regexes) {
				if ($package_name !~ m/$regex/) {
					$matched = 0;
					last;
				}
			}

			say $package_name if $matched;
		}
	} else {
		while (my ($package_name, $package) = each %{$cache->get_binary_packages()}) {
			my $matched = 1;
			my $version;
			REGEX:
			foreach my $regex (@regexes) {
				if ($package_name =~ m/$regex/) {
					next REGEX;
				} else {
					foreach (@{$package->get_versions()}) {
						if ($_->{short_description} =~ m/$regex/ or
							defined($_->{long_description}) && $_->{long_description} =~ m/$regex/)
						{
							$version = $_;
							next REGEX;
						}
					}
				}
				$matched = 0;
				last REGEX;
			}
			next if !$matched;

			if (defined $version) {
				say $package_name . " - " . $version->{short_description} if $matched;
			} else {
				say $package_name;
			}
		}
	}
}

sub show_package_relations ($) {
	my ($mode) = @_;

	# turn off info parsing, we don't need it, only relations :)
	$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;

	my $o_with_suggests = 0;
	my $o_installed_only = 0;
	GetOptions(
		'with-suggests' => \$o_with_suggests,
		'installed-only' => \$o_installed_only,
	);

	check_no_more_options();

	scalar @ARGV or
			mydie("no package expressions specified");

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my @versions = map { select_binary_package_version($_) } @ARGV;

	my @relation_groups = (
		[ 'pre_depends', __('Pre-Depends') ],
		[ 'depends', __('Depends') ],
	);
	if (!$config->var('apt::cache::important')) {
		push @relation_groups, 
				[ 'recommends', __('Recommends') ];

		if ($o_with_suggests) {
			push @relation_groups,
					[ 'suggests', __('Suggests') ];
		}
	}

	# don't output the same version more than one time
	my %processed_entries;

	while (scalar @versions) {
		my $version = shift @versions;

		my $package_name = $version->{package_name};
		my $version_string = $version->{version_string};

		next if exists $processed_entries{$package_name,$version_string};
		$processed_entries{$package_name,$version_string} = 1;

		say "$package_name $version_string:";
		foreach my $item (@relation_groups) {
			my $relation_group_name = $item->[0];
			my $caption = $item->[1];

			# [ relation_expression, package_name ]
			my @relation_expressions_and_packages;
			# fill relations with actual relations
			if ($mode eq 'normal') {
				# just add normal dependencies
				@relation_expressions_and_packages = map { [ $_, "" ] } @{$version->{$relation_group_name}};
			} else {
				# we have to check all reverse dependencies for this version
				foreach (values %{$cache->get_binary_packages()}) {
					my $reverse_version_candidate = $cache->get_policy_version($_);
					my $needed = 0;
					foreach (@{$reverse_version_candidate->{$relation_group_name}}) {
						# check each relation
						if (ref $_ ne 'ARRAY' && $_->package_name eq $package_name) {
							# don't worry about any virtual packages, just check the relation
							if (!defined $_->relation_string) {
								# there is no versioned info in relation, all's ok
								$needed = 1;
							} elsif ($_->satisfied_by($version_string)) {
								# versioned info is good
								$needed = 1;
							} else {
								# bad, this is inappropriate relation, stop at this
								last;
							}
						} else {
							# ok, check as common
							my $ref_relation_versions = $cache->get_satisfying_versions($_);
							foreach (@$ref_relation_versions) {
								if ($_->{package_name} eq $package_name && $_->{version_string} eq $version_string) {
									# ok, good
									$needed = 1;
									last;
								}
							}
						}
						if ($needed == 1) {
							# positive result
							push @relation_expressions_and_packages, [ $_, $reverse_version_candidate->{package_name} ];
							last;
						}
					}
				}
			}

			my $starter = "  " . ($mode eq 'reverse' ? __('Reverse-') : '') . $caption . ": ";
			foreach (@relation_expressions_and_packages) {
				say $starter, ($_->[1] ? $_->[1] . ": " : ""), stringify_relation_expression($_->[0]);
			}

			if ($config->var('apt::cache::recursedepends')) {
				# insert recursive depends into queue
				foreach (@relation_expressions_and_packages) {
					if ($mode eq 'normal') {
						# normal depends

						my $ref_satisfying_versions = $cache->get_satisfying_versions($_->[0]);
						# if some versions exist
						if (scalar @$ref_satisfying_versions) {
							# choose only one version
							push @versions, $ref_satisfying_versions->[0];
						}
					} else {
						# reverse depends
						push @versions, $cache->get_policy_version($cache->get_binary_package($_->[1]));
					}
				}
			}
		}
	}
}

sub policy () {
	# turn off info and relations parsing, we don't need it
	$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
	$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;

	check_no_more_options();

	my @packages = @ARGV;

	if (scalar @packages) {
		# print release info for supplied package names

		build_cache(-source => 0, -binary => 1, -installed => 1);

		for my $package_name (@packages) {
			my $package = get_binary_package($package_name);
			my $policy_version = $cache->get_policy_version($package);

			my $installed_version_string = $cache->get_system_state()->get_installed_version_string($package_name);
			say "$package_name:";
			say "  " . __('Installed') . ": " .
					($installed_version_string // ('(' . __('none') . ')'));

			say "  " . __('Candidate') . ": " . $policy_version->{version_string};
			say "  " . __('Version table') . ":";

			my $ref_versions = $cache->get_sorted_pinned_versions($package);

			foreach my $entry (@$ref_versions) {
				my $version = $entry->{'version'};
				my $pin = $entry->{'pin'};

				if (defined($installed_version_string) && 
					$version->{version_string} eq $installed_version_string)
				{
					print " *** ";
				} else {
					print "     ";
				}

				say $version->{version_string} . ' ' . $pin;

				my @avail_as = @{$version->{avail_as}};
				foreach (@avail_as) {
					print (' ' x 8);
					my $origin = $_->{release}->{base_uri} || $config->var('dir::state::status');
					print $origin . ' ';
					print $_->{release}->{archive} . '/'. $_->{release}->{component} . ' ';
					say '(' . ($_->{release}->{signed} ? __('signed') : __('unsigned')) . ')';
				}
			}
		}
	} else {
		# print overall release data
		build_cache(-source => 1, -binary => 1, -installed => 1);

		my $sub_say_release_info = sub {
			my ($ref_release_info) = @_;
			my $origin = $ref_release_info->{base_uri} || $config->var('dir::state::status');
			my $archive = $ref_release_info->{archive};
			my $component = $ref_release_info->{component};
			print "  $origin $archive/$component: ";
			print "o=", $ref_release_info->{vendor};
			print ",a=", $archive;
			print ",l=", $ref_release_info->{label};
			print ",c=", $component;
			print ",v=", $ref_release_info->{version};
			print ",n=", $ref_release_info->{codename};
			say "";
		};

		say "Package files:";
		my $ref_binary_release_data = $cache->get_binary_release_data();
		foreach my $ref_release_info (@$ref_binary_release_data) {
			$sub_say_release_info->($ref_release_info);
		}

		say "Source files:";
		my $ref_source_release_data = $cache->get_source_release_data();
		foreach my $ref_release_info (@$ref_source_release_data) {
			$sub_say_release_info->($ref_release_info);
		}
	}
}

sub mark_autoinstalled ($) {
	require Cupt::System::Worker;
	my ($mark_action) = @_;

	check_no_more_options();

	build_cache(-source => 0, -binary => 0, -installed => 0);

	my $worker = new Cupt::System::Worker($config, $cache);
	my $markauto = $mark_action eq 'markauto' ? 1 : 0;
	$worker->mark_as_automatically_installed($markauto, @ARGV);
}

sub pkgnames () {
	my $o_installed_only = 0;
	GetOptions(
		'installed-only' => \$o_installed_only,
	);

	check_no_more_options();

	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my $prefix = "";
	if (scalar @ARGV) {
		$prefix = quotemeta(shift @ARGV);
	}

	check_no_extra_args();

	while ((my $package_name) = each %{$cache->get_binary_packages()}) {
		# check package name for pattern and output it
		$package_name =~ m/^$prefix/o and say $package_name;
	}
}

sub generate_management_prompt ($$$) {
	my ($worker, $show_versions, $show_size_changes) = @_;

	my $show_reasons = $config->var('cupt::resolver::track-reasons');

	my $sub_prompt = sub {
		my $ref_desired_packages = $_[0];
		$worker->set_desired_state($ref_desired_packages);

		my $ref_actions_preview = $worker->get_actions_preview();
		my $ref_unpacked_sizes_preview = $worker->get_unpacked_sizes_preview($ref_actions_preview);
		# print planned actions
		do {
			my %action_names = (
				'install' => __('INSTALLED'),
				'remove' => __('REMOVED'),
				'upgrade' => __('UPGRADED'),
				'purge' => __('PURGED'),
				'downgrade' => __('DOWNGRADED'),
				'configure' => __('CONFIGURED'),
				'deconfigure' => __('DECONFIGURED'),
			);
			say "";
			foreach my $action (qw(install upgrade remove purge downgrade configure deconfigure)) {
				my $ref_package_entries = $ref_actions_preview->{$action};

				# sort by package name
				@$ref_package_entries = sort { $a->{'package_name'} cmp $b->{'package_name'} } @$ref_package_entries;

				scalar @$ref_package_entries or next; # don't print empty lists
				my $action_name = $action_names{$action};
				say sprintf __("The following %u packages will be %s:"), scalar @$ref_package_entries, $action_name;
				say "";
				foreach my $ref_package_entry (@$ref_package_entries) {
					my $package_name = $ref_package_entry->{'package_name'};
					print "$package_name";
					if ($show_versions) {
						my $old_version_string =
								$cache->get_system_state()->get_installed_version_string($package_name);
						my $new_version = $ref_package_entry->{'version'};
						my $new_version_string = defined $new_version ?
								$new_version->{version_string} : undef;
						if (!defined $old_version_string) {
							print " [$new_version_string]";
						} elsif (!defined $new_version_string) {
							print " [$old_version_string]";
						} else {
							print " [$old_version_string -> $new_version_string]";
						}
					}
					if ($show_size_changes) {
						my $size_change = $ref_unpacked_sizes_preview->{$package_name};
						if ($size_change != 0) {
							my $size_change_string = ($size_change >= 0) ?
									'+' . human_readable_size_string($size_change) :
									'-' . human_readable_size_string(-$size_change);
							print " <$size_change_string>";
						}
					}

					if ($show_versions || $show_size_changes || $show_reasons) {
						# put newline
						say "";
					} else {
						# put a space between package names
						print ' ';
					}

					if ($show_reasons) {
						my $sub_say_reason = sub {
							say "  " . __("reason: ") . $_[0];
						};
						foreach my $reason (@{$ref_package_entry->{'reasons'}}) {
							my @reason_parts = @$reason;
							if (ref $reason_parts[0]) {
								# first element contains a reference, this is
								# indicator that reason is some dependency
								# relation
								my %reason_dependency_name_translations = (
									'pre-depends' => __('pre-depends on'),
									'depends' => __('depends on'),
									'recommends' => __('recommends'),
									'suggests' => __('suggests'),
									'conflicts' => __('conflicts with '),
									'breaks' => __('breaks'),
								);
								my $reason_version = shift @reason_parts;
								my $reason_dependency_name = shift @reason_parts;
								my $reason_relation_expression = shift @reason_parts;

								my $reason_package_name = $reason_version->{package_name};
								my $reason_version_string = $reason_version->{version_string};

								$sub_say_reason->(sprintf("%s %s %s '%s'",
										$reason_package_name, $reason_version_string,
										$reason_dependency_name_translations{$reason_dependency_name},
										stringify_relation_expression($reason_relation_expression)
										));
							} elsif ($reason_parts[0] eq 'user') {
								$sub_say_reason->(__('user request'));
							} elsif ($reason_parts[0] eq 'auto-remove') {
								$sub_say_reason->(__('auto-removal'));
							} else {
								myinternaldie("bad first part '%s' of the reason", $reason_parts[0]);
							}
						}
						say "";
					}
				}
				say "" if !$show_versions && !$show_size_changes && !$show_reasons;
				say "";
			}
		};
		# prepare list of soft dependencies that are not satisfied
		do {
			my @unsatisfied_soft_dependencies;
			my @versions_to_remove;
			foreach my $action (qw(remove purge)) {
				my $ref_package_entries = $ref_actions_preview->{$action};
				foreach my $ref_package_entry (@$ref_package_entries) {
					my $package = $cache->get_binary_package($ref_package_entry->{'package_name'});
					my $version = $package->get_installed_version();
					push @versions_to_remove, $version;
				}
			}
			foreach my $package_name (keys %$ref_desired_packages) {
				my $ref_package_entry = $ref_desired_packages->{$package_name};
				next if !defined $ref_package_entry->{version};

				foreach my $relation_expression (@{$ref_package_entry->{version}->{recommends}}) {
					my $ref_satisfying_versions = $cache->get_satisfying_versions($relation_expression);
					
					my $soft_breakage_possible = 0;
					RELATION_ENTRY:
					foreach my $satisfying_version (@$ref_satisfying_versions) {
						foreach my $version_to_remove (@versions_to_remove) {
							next if $satisfying_version->{package_name} ne $version_to_remove->{package_name};
							next if $satisfying_version->{version_string} ne $version_to_remove->{version_string};
							$soft_breakage_possible = 1;
							last RELATION_ENTRY;
						}
					}

					if ($soft_breakage_possible) {
						my $is_broken = 1;
						foreach my $satisfying_version (@$ref_satisfying_versions) {
							my $satisfying_package_name = $satisfying_version->{package_name};
							next if !defined $ref_desired_packages->{$satisfying_package_name};
							my $desired_version = $ref_desired_packages->{$satisfying_package_name}->{version};
							next if !defined $desired_version;
							next if $desired_version->{version_string} ne $satisfying_version->{version_string};
							$is_broken = 0;
							last;
						}
						if ($is_broken) {
							my $version_string = $ref_package_entry->{version}->{version_string};
							push @unsatisfied_soft_dependencies, 
									sprintf __("%s %s recommends %s"), $package_name, $version_string,
									stringify_relation_expression($relation_expression);
									
						}
					}
				}
			}
			if (scalar @unsatisfied_soft_dependencies) {
				say sprintf __("Leave the following dependencies unresolved:");
				say "";
				say $_ for @unsatisfied_soft_dependencies;
				say "";
			}
		};

		if (!$config->var('apt::get::allowunauthenticated')) {
			my @untrusted_package_names;
			# generate loud warning for unsigned versions
			foreach my $action (qw(install upgrade downgrade)) {
				my $ref_package_entries = $ref_actions_preview->{$action};
				foreach my $version (map { $_->{version} } @$ref_package_entries) {
					push @untrusted_package_names, $version->{package_name} if !$version->is_signed();
				}
			}
			if (scalar @untrusted_package_names) {
				say sprintf __("WARNING! The untrusted versions of the following packages will be INSTALLED:");
				say "";
				say $_ for @untrusted_package_names;
				say "";
			}
		}

		my $is_dangerous_action = 0;
		do { # generate loud warnings for removing essential packages
			my @essential_package_names;
			# generate loud warning for unsigned versions
			foreach my $action (qw(remove purge)) {
				my $ref_package_entries = $ref_actions_preview->{$action};
				foreach my $package_name (map { $_->{'package_name'} } @$ref_package_entries) {
					my $version = $cache->get_binary_package($package_name)->get_installed_version();
					push @essential_package_names, $version->{package_name} if $version->{essential};
				}
			}
			if (scalar @essential_package_names) {
				$is_dangerous_action = 1;
				say sprintf __("WARNING! The following essential packages will be REMOVED:");
				say "";
				say $_ for @essential_package_names;
				say "";
			}
		};

		do { # print size estimations
			my ($total_bytes, $need_bytes) = $worker->get_download_sizes_preview($ref_actions_preview);
			print sprintf __("Need to get %s/%s of archives. "),
					human_readable_size_string($need_bytes),
					human_readable_size_string($total_bytes);

			my $total_unpacked_size_change =
					sum(values %$ref_unpacked_sizes_preview) // 0;
			if ($total_unpacked_size_change >= 0) {
				say sprintf __("After unpacking %s will be used."),
						human_readable_size_string($total_unpacked_size_change);
			} else {
				say sprintf __("After unpacking %s will be freed."),
						human_readable_size_string(-$total_unpacked_size_change);
			}
		};

		my $confirmation_for_dangerous_action = __("Yes, do as I say!");
		my $question = $is_dangerous_action ?
				sprintf(__("Dangerous actions selected. Type '%s' if you want to continue, 'q' to exit, anything else to discard this solution:\n"), $confirmation_for_dangerous_action) :
				__("Do you want to continue? [y/N/q] ");
		my $positive_answer = $is_dangerous_action ? $confirmation_for_dangerous_action : 'y';

		print $question;
		my $answer = <STDIN>;
		chomp $answer;
		$answer = lc($answer) if !$is_dangerous_action;
		if ($answer eq $positive_answer) {
			# solution has been accepted
			return 1;
		} elsif ($answer eq 'q') {
			# user is willing to abandon all further tryings
			return undef;
		} else {
			# user haven't chosen this solution, try next one
			print __("Resolving further... ");
			return 0;
		}
	};

	return $sub_prompt;
}

sub manage_packages ($) {
	require Cupt::System::Resolver;
	require Cupt::System::Worker;

	my $o_show_versions = 0;
	my $o_show_size_changes = 0;
	my $o_simulate;
	my $o_no_install_unpacked;
	GetOptions(
		'resolver=s' => sub { $config->set_regular_var('cupt::resolver::type', $_[1]) },
		'show-deps|show-reasons|D' => sub { $config->set_regular_var('cupt::resolver::track-reasons', 1) },
		'max-solution-count=s' => sub { $config->set_regular_var('cupt::resolver::max-solution-count', $_[1]) },
		'no-install-recommends|R' => sub { $config->set_regular_var('apt::install-recommends' => 0) },
		'no-remove' => sub { $config->set_regular_var('cupt::resolver::no-remove' => 1) },
		'no-auto-remove' => sub { $config->set_regular_var('cupt::resolver::auto-remove', 0) },
		'download-only|d' => sub { $config->set_regular_var('cupt::worker::download-only', 1) },
		'show-versions|V' => \$o_show_versions,
		'show-size-changes|Z' => \$o_show_size_changes,
		'no-install-unpacked' => \$o_no_install_unpacked,
	);
	# action can be 'install', 'remove', 'purge', 'safe-upgrade', 'full-upgrade'
	my ($action) = @_;

	# turn off info parsing, we don't need it
	$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;

	check_no_more_options();

	local $| = 1;
	print __('Building the package cache... ');
	build_cache(-source => 0, -binary => 1, -installed => 1);
	say __('[done]');

	print __('Initializing package resolver and worker... ');
	my $resolver;
	do {
		my $resolver_module_name = defined $config->var('cupt::resolver::external-command') ?
				'Cupt::System::Resolvers::External' : 'Cupt::System::Resolvers::Native';
		eval "require $resolver_module_name;"; die $@ if $@;
		$resolver = $resolver_module_name->new($config, $cache);
	};
	$resolver->import_installed_versions($cache->get_system_state()->export_installed_versions());
	my $worker = new Cupt::System::Worker($config, $cache);
	say __('[done]');

	print __('Scheduling requested actions... ');
	if ($action eq 'safe-upgrade' || $action eq 'full-upgrade') {
		if ($action eq 'safe-upgrade') {
			$config->set_regular_var('cupt::resolver::no-remove' => 1);
		}
		$resolver->upgrade();

		# despite the main action is {safe,full}-upgrade, allow package
		# modifiers in the command line just as with the install command
		$action = 'install';
	}

	my $o_purge = 0;
	if ($action eq 'purge') {
		$action = 'remove';
		$config->set_regular_var('cupt::worker::purge', 1)
	}

	if (not defined $o_no_install_unpacked) {
		# find all unpacked packages and install them if the appropriate
		# version exist

		my $system_state = $cache->get_system_state();
		foreach my $package_name (keys %{$cache->get_binary_packages()}) {
			my $ref_install_info = $system_state->get_installed_info($package_name);
			next unless defined $ref_install_info;
			next if $ref_install_info->{'status'} ne 'unpacked';
			my $version_string = $ref_install_info->{'version_string'};
			my $version = $cache->get_binary_package($package_name)->get_specific_version($version_string);
			next unless defined $version;
			$resolver->install_version($version);
		}
	}

	my @package_expressions = @ARGV;
	foreach my $package_expression (@package_expressions) {
		if ($action eq 'satisfy') {
			my $ref_relation_expressions = parse_relation_line($package_expression);
			$resolver->satisfy_relation_expression($_) for @$ref_relation_expressions;
		} else {
			my $action = $action;
			if (!defined $cache->get_binary_package($package_expression)) {
				# we have a funny situation with package names like 'g++',
				# where one don't know is there simple package name or '+'/'-'
				# modifier at the end of package name, so we enter here only if
				# it seems that there is no such binary package

				# "localizing" action to make it modifiable by package modifiers
				my $last_letter = substr($package_expression, -1, 1);
				if ($last_letter eq '+') {
					$action = 'install';
					$package_expression = substr($package_expression, 0, length($package_expression)-1);
				} elsif ($last_letter eq '-') {
					$action = 'remove';
					$package_expression = substr($package_expression, 0, length($package_expression)-1);
				}
			}

			if ($action eq 'install') {
				my $version = select_binary_package_version($package_expression);
				$resolver->install_version($version);
			} else { # 'remove'
				check_package_name($package_expression);
				defined($cache->get_binary_package($package_expression)) or
						mydie("unable to find binary package '%s'", $package_expression);

				$resolver->remove_package($package_expression);
			}
		}
	}

	say __('[done]');

	print __('Resolving possible unmet dependencies... ');
	my $resolve_result = $resolver->resolve(
			generate_management_prompt($worker, $o_show_versions, $o_show_size_changes)
	);
	say "";

	if (!defined $resolve_result) {
		# tryings were abandoned
	} elsif ($resolve_result) {
		# if some solution was found and user has accepted it
		eval "require $download_progress_class_name"; die $@ if $@;
		my $download_progress = $download_progress_class_name->new();
		say __("Performing requested actions:");
		my $overall_result = $worker->change_system($download_progress);
		mydie("unable to do requested actions") unless $overall_result;
	} else {
		say __("no more solutions.");
	}
}

sub update_release_data () {
	require Cupt::System::Worker;

	check_no_extra_args();
	check_no_more_options();

	build_cache(-source => 0, -binary => 0, -installed => 0);

	eval "require $download_progress_class_name"; die $@ if $@;
	my $download_progress = $download_progress_class_name->new();
	my $worker = new Cupt::System::Worker($config, $cache);

	return $worker->update_release_and_index_data($download_progress);
}

sub show_changelog_or_copyright ($) {
	my ($target) = @_;
	# turn off info and relations parsing, we don't need it
	$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
	$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;

	my $o_installed_only = 0;
	GetOptions(
		'installed-only' => \$o_installed_only,
	);

	check_no_more_options();

	scalar @ARGV or
			mydie("no package expressions specified");

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my @versions = map { select_binary_package_version($_) } @ARGV;

	foreach my $version (@versions) {
		my $local_target_path;
		if ($target eq 'changelog') {
			$local_target_path = Cupt::Cache::get_path_of_debian_changelog($version);
		} else {
			$local_target_path = Cupt::Cache::get_path_of_debian_copyright($version);
		}

		if (defined $local_target_path) {
			# there is a local changelog, display it
			my $prepare_command = $target eq 'changelog' ? 'zcat' : 'cat';
			my $result = system("$prepare_command $local_target_path | sensible-pager");
			# return non-zero code in case of some error
			return $result if $result;
		} else {
			my %base_uris_by_vendor = (
				'Debian' => "http://packages.debian.org/changelogs/pool",
				'Ubuntu' => "http://changelogs.ubuntu.com/changelogs/pool",
				# yes, 'changelogs' even for copyrights :)
			);

			my @matched_avail_entries;
			foreach my $ref_avail_entry (@{$version->{avail_as}}) {
				if ($ref_avail_entry->{release}->{vendor} eq 'Debian') {
					# this is probably a package from Debian archive
					push @matched_avail_entries, $ref_avail_entry;
					last;
				}
				if ($ref_avail_entry->{release}->{vendor} eq 'Ubuntu') {
					# this is probably a package from Debian archive
					push @matched_avail_entries, $ref_avail_entry;
					last;
				}
			}

			my @target_uris;
			foreach my $ref_matched_avail_entry (@matched_avail_entries) {
				# all of above are only good guessings
				my $version_string = $version->{source_version_string};
				my $source_package_name = $version->{source_package_name};
				my $short_prefix = ($source_package_name =~ m/^lib/) ?
						substr($source_package_name, 0, 4) : substr($source_package_name, 0, 1);

				my $component = $ref_matched_avail_entry->{release}->{component};
				my $remote_target_base_uri = $base_uris_by_vendor{$ref_matched_avail_entry->{release}->{vendor}};
				my $uri_appendage = "$component/$short_prefix/$source_package_name/${source_package_name}_$version_string/$target";
				push @target_uris, "$remote_target_base_uri/$uri_appendage";
			}

			do { # temporary (I hope) hack to work around dropping epoch on Debian/Ubuntu web links
				my @fallback_uris;
				foreach my $target_uri (@target_uris) {
					(my $fallback_uri = $target_uri) =~ s/\d+://;
					if ($fallback_uri ne $target_uri) {
						push @fallback_uris, $fallback_uri;
					}
				}
				push @target_uris, @fallback_uris;
			};

			if (scalar @target_uris) {
				require Cupt::Download::Manager;

				my (undef, $temp_target_path) = tempfile();

				eval "require $download_progress_class_name"; die $@ if $@;
				my $download_progress = $download_progress_class_name->new();

				my $download_result;
				do {
					my $download_manager = new Cupt::Download::Manager($config, $download_progress);
					$download_result = $download_manager->download(
							{ 'uris' => \@target_uris, 'filename' => $temp_target_path });
				}; # make sure that download manager is already destroyed at this point

				# fail and exit if it was something bad with downloading
				mydie($download_result) if $download_result;

				my $result = system("sensible-pager $temp_target_path");
				# remove the file
				unlink $temp_target_path;
				# return non-zero code in case of some error
				return $result if $result;
			} else {
				mydie("no info where to acquire %s for version '%s' of package '%s'",
						$target, $version->{version_string}, $version->{package_name});
			}
		}
	}
}

sub screenshots ($) {
	# turn off info and relations parsing, we don't need it
	$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
	$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;

	check_no_more_options();

	my @package_names = @ARGV;
	scalar @package_names or
			mydie("no package expressions specified");

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => 1, -installed => 1);

	foreach my $package_name (@package_names) {
		# check for existence
		get_binary_package($package_name);

		if (my $result = system("sensible-browser http://screenshots.debian.net/package/$package_name")) {
			return $result;
		}
	}
}

sub clean_archives ($) {
	require Cupt::System::Worker;
	my ($leave_available) = @_;

	check_no_more_options();
	check_no_extra_args();

	build_cache(-source => 0, -binary => 1, -installed => 0);

	my $worker = new Cupt::System::Worker($config, $cache);
	my $size_deleted = 0;

	my $sub_callback = sub {
		my $path = $_[0];
		my $size = lstat($path)->size;
		$size_deleted += $size;
		say sprintf __("Deleting '%s' <%s>."), $path, human_readable_size_string($size);
	};
	$worker->clean_archives($sub_callback, $leave_available);
	say sprintf __("Freed %s of disk space."), human_readable_size_string($size_deleted);
}

sub process_options {
	# strip options out (for now)
	my @direct_options;
	eval {
		GetOptions(
			'option|o=s@' => \@direct_options,
			'important|i' => sub { $config->set_regular_var('apt::cache::important', 1) },
			'all-versions|a' => sub { $config->set_regular_var('apt::cache::allversions', 1) },
			'no-all-versions' => sub { $config->set_regular_var('apt::cache::allversions', 0) },
			'target-release|default-release|t=s' => sub { $config->set_regular_var('apt::default-release', $_[1]) },
			'recurse' => sub { $config->set_regular_var('apt::cache::recursedepends', 1) },
			'simulate|s' => sub { $config->set_regular_var('cupt::worker::simulate' => 1) },
			'purge' => sub { $config->set_regular_var('cupt::worker::purge' => 1) },
			'quiet|q' => sub { $config->set_regular_var('quiet' => 1) },
		);
		foreach (@direct_options) {
			if (m/(.*?)=(.*)/) {
				my ($key, $value) = ($1, $2);
				if ($key =~ m/^(.*?)::$/) {
					# this is list option
					$config->set_list_var($1, $value);
				} else {
					# regular option
					$config->set_regular_var($key, $value);
				}
			} else {
				mydie("incorrect option syntax (right is '<option>=<value>')");
			}
		}
	};
	if (mycatch()) {
		myerr("error while processing command-line options");
		exit 2;
	}
}

