#!/usr/bin/perl

use strict;
use warnings;
use Data::Dumper;
use File::Basename;
use Emdebian::Grip;
use POSIX qw(locale_h);
use Locale::gettext;
use Debian::Packages::Compare;

use vars qw/ $filter_name $grip_name $suite $base $locale_name
 $noskip @archlist @locroots @lines $line %pkg @filter $have
 %debianstable %gripstable %gripupdate %localestable $go $mirror
 $our_version %debupdate %griptesting %localeupdate %skip
 %prepare %remove %migrate %outdated %skip_locale %remove_locale
 %migrate_locale %errors %outdated_locale $list_skips $prep $purge
 $do_release $single @srcset @binset $debver $gripver $updver %binarch
 %srcs %binaries /;

my $prog = basename($0);
$our_version = &scripts_version();
setlocale(LC_MESSAGES, "");
textdomain("emdebian-grip");

$mirror='http://ftp.uk.debian.org/debian'; # default
$filter_name = 'filter';
$grip_name = 'grip';
$locale_name = "locale";
$suite = "stable";
$base = '/opt/reprepro/';
$go = 1;

while( @ARGV ) {
	$_= shift( @ARGV );
	last if m/^--$/;
	if (!/^-/) {
		unshift(@ARGV,$_);
		last;
	}
	elsif (/^(-\?|-h|--help|--version)$/) {
		&usageversion();
		exit (0);
	}
	elsif (/^(-M|--mirror)$/) {
		$mirror = shift;
	}
	elsif (/^(-b|--base-path)$/) {
		$base = shift;
	}
	elsif (/^(-s|--single-package)$/) {
		$single = shift;
	}
	elsif (/^(-l|--list-skipped)$/) {
		$list_skips++;
	}
	elsif (/^(-p|--prepare)$/) {
		$prep++;
	}
	elsif (/^(-P|--purge)$/) {
		$purge++;
	}
	elsif (/^(-n|--dry-run)$/) {
		undef $go;
	}
	elsif (/^(-m|--migrate$)$/) {
		$do_release++;
	}
	elsif (/^(--filter-name)$/) {
		$filter_name = shift;
	}
	elsif (/^(--grip-name)$/) {
		$grip_name = shift;
	}
	else {
		die "$prog: "._g("Unknown option")." $_.\n";
	}
}
$base .= '/' if ("$base" !~ m:/$:);
if (not -d $base) {
	printf (_g("ERR: Please specify an existing directory for the base-path: %s\n"), $base);
	exit 1;
}
&set_base($base);
&set_repo_names ($filter_name, $grip_name);
my $a = &get_archlist ($suite, $filter_name);
die (_g("unable to get architecture list\n")) if (not defined $a);
@archlist = @$a;
my $l = &get_locale_roots ($suite, 'locale');
die (_g("unable to get locale rootfs list\n")) if (not defined $l);
@locroots = @$l;

die(_g("no pkglist filter.\n"))
	if ( not -f "${base}${filter_name}/conf/pkglist" );

print _g("INF: Reading stable.\n");
my $debf  = &read_packages ('stable', $filter_name);
my $gripf = &read_packages ('stable', $grip_name);
print _g("INF: Reading testing.\n");
my $gript = &read_packages ('testing', $grip_name);
print _g("INF: Reading stable-proposed-updates.\n");
my $updatedeb = &read_packages ('stable-proposed-updates', $filter_name);
my $updategrip = &read_packages ('stable-proposed-updates', $grip_name);
print _g("INF: Reading locale repository.\n");
my $locf  = &read_locale ('stable', $locale_name);
my $locup = &read_locale ('stable-proposed-updates', $locale_name);
%debianstable = %$debf   if (defined $debf);
%gripstable   = %$gripf  if (defined $gripf);
%gripupdate   = %$updategrip  if (defined $updategrip);
%griptesting  = %$gript  if (defined $gript);
%debupdate    = %$updatedeb if (defined $updatedeb);
%localestable = %$locf if (defined $locf);
%localeupdate = %$locup if (defined $locup);

if (defined $single) {
	printf (_g("INF: Processing a single package: '%s'\n"), $single);
	my $single_src = $debianstable{$single}{'Src'};
	if (not defined $single_src) {
		printf STDERR (_g("Error: Failed to find a source package for %s.\n"), $single);
		exit 1;
	}
	my $debval = (defined $debianstable{$single_src}{'source'}) ?
		$debianstable{$single_src}{'source'} : "";
	print "Debian stable: $debval\n";
	my $gripval = (defined $gripstable{$single_src}{'source'}) ?
		$gripstable{$single_src}{'source'} : "";
	print "Grip stable: $gripval\n";
	my $updval = (defined $gripupdate{$single_src}{'source'}) ?
		$gripupdate{$single_src}{'source'} : "";
	print "Grip updates: $updval\n";
	$prepare{$single}=1 if ($debval ne $updval);
	%binaries=();
	%binarch=();
	foreach my $arch (@archlist) {
		next if ($arch eq "source");
		$debver = $debianstable{$single}{$arch};
		$gripver = $gripstable{$single}{$arch};
		$updver = $gripupdate{$single}{$arch};
		next if (not defined $debver);
		if (defined $updver) {
			$updver =~ s/em1$//;
			# Translators: checking debian ver = update_ver for $architecture
			printf (_g("INF: checking %s = %s for %s\n"), $debver, $updver, $arch);
			next if ($debver eq $updver);
			my $retval = system ("dpkg --compare-versions $debver '=' $updver");
			$retval /= 256;
			next if ($retval == 0);
		}
		if (not defined $gripver) {
			# Translators: at this point, no idea if there are multiple binary packages
			printf (_g("INF: processing %s binary package(s) for %s.\n"), $single, $arch);
			push @{$binaries{$single}}, $single;
			push @{$binarch{$single}}, $arch;
			next;
		}
		$gripver =~ s/em1$//;
			print "INF: checking $debver = $gripver for $arch\n";
		next if ($debver eq $gripver);
		my $retval = system ("dpkg --compare-versions $debver '>>' $gripver");
		$retval /= 256;
		if ($retval == 0) {
			printf (_g("INF: processing %s binary package(s) for %s.\n"), $single, $arch);
			push @{$binaries{$single}}, $single;
			push @{$binarch{$single}}, $arch;
		}
	}
	&prepare;
	exit 0;
}

print _g("INF: Calculating, please wait ...\n");
@binset=();
@srcset=();
# map binaries back to source packages
foreach my $bin (sort keys %gripstable) {
	my $src = $gripstable{$bin}{'Src'};
	$srcs{$src}++ if (defined $src);
	foreach my $arch (@archlist) {
		next if ($arch eq "source");
		$debver = $debianstable{$bin}{$arch};
		$gripver = $gripstable{$bin}{$arch};
		$updver = $gripupdate{$bin}{$arch};
		next if (not defined $debver);
		if (defined $updver) {
			$updver =~ s/em1$//;
			my $retval = system ("dpkg --compare-versions $debver '=' $updver");
			$retval /= 256;
			# updates is already newest, skip.
			next if ($retval == 0);
		}
		if (not defined $gripver) {
			push @{$binaries{$src}}, $bin;
			push @{$binarch{$bin}}, $arch;
			next;
		}
		# ok, some version exists, let's see if it's older.
		$gripver =~ s/em1$//;
		my $retval = system ("dpkg --compare-versions $debver '>>' $gripver");
		$retval /= 256;
		if ($retval == 0) {
			push @{$binaries{$src}}, $bin;
			push @{$binarch{$bin}}, $arch;
		}
	}
}
my %u=();
foreach my $b (values %binaries) {
	foreach my $a (@$b) {
		$u{$a}++;
	}
}
foreach my $bin (sort keys %gripupdate) {
	my $src = $gripupdate{$bin}{'Src'};
	if (defined $src) {
		$srcs{$src}++;
		push @{$binaries{$src}}, $bin if ($src ne $bin);
	}
}
push @srcset, sort keys %srcs;
push @binset, sort keys %binaries;
my $c = 0;
foreach my $b (values %binaries) {
	my @a = @$b;
	$c += scalar @a;
}
printf(ngettext("INF: Found %s unique source package to be checked . . .\n",
	"INF: Found %s unique source packages to be checked . . .\n",
	scalar @srcset), scalar @srcset);
printf(ngettext("INF: Found %s binary package to be checked . . .\n",
	"INF: Found %s binary packages to be checked . . .\n",
	scalar keys (%u)), scalar keys (%u));

foreach my $pkg (@srcset) {
	my @output=();
	push @output, "Package: $pkg";
	if (defined $debianstable{$pkg}{'source'}) {
		push @output, "DebianVersion: $debianstable{$pkg}{'source'}";
	} else {
		$errors{$pkg} .= _g("Error: source package is not in Debian stable.");
		next;
	}
	if (defined $gripstable{$pkg}{'source'}) {
		push @output, "GripVersion: $gripstable{$pkg}{'source'}";
		if (&is_same ($gripstable{$pkg}{'source'}, $debianstable{$pkg}{'source'}) == 0) {
			push @output, "Result: nothing to do";
			$skip{$pkg}++;
		} elsif (&is_newer ($debianstable{$pkg}{'source'}, $gripstable{$pkg}{'source'}) == 0) {
			$errors{$pkg} .= _g("Error: grip stable is newer than debian stable. ");
			$errors{$pkg} .= sprintf(_g("Grip has %s "),$gripstable{$pkg}{'source'});
			$errors{$pkg} .= sprintf(_g("Debian has %s"),$debianstable{$pkg}{'source'});
		} elsif (&is_newer ($gripstable{$pkg}{'source'}, $debianstable{$pkg}{'source'}) == 0) {
			if (&is_same ($debianstable{$pkg}{'source'}, $gripupdate{$pkg}{'source'}) == 0) {
				$migrate{$pkg}++;
			} else {
				$prepare{$pkg}++;
			}
		}
	} else {
		push @output, "GripVersion: missing.";
		$outdated{$pkg}++;
	}
	if (defined $gripupdate{$pkg}{'source'}) {
		push @output, "UpdatesVersion: $gripupdate{$pkg}{'source'}";
		if (defined $outdated{$pkg}) {
			if (&is_same ($gripupdate{$pkg}{'source'}, $debianstable{$pkg}{'source'}) == 0) {
				foreach my $arch (@archlist) {
					next if ($arch eq "arm");
					if (defined $debianstable{$pkg}{$arch}) {
						my $cmp = $gripupdate{$pkg}{$arch};
						if (defined $cmp) {
							$cmp =~ s/em1$//;
							if ($cmp ne $debianstable{$pkg}{$arch}) {
								$prepare{$pkg}++;
								$errors{$pkg} .= sprintf(_g("mismatch for %s, "), $arch);
							}
						} else {
							$prepare{$pkg}++;
							$errors{$pkg} .= sprintf (_g("missing on %s, "), $arch);
						}
					}
				}
				if (not defined $prepare{$pkg}) {
					push @output, "Result: migrate from updates";
					$migrate{$pkg}++;
				} else {
					push @output, _g("Error: ").join(", ", $errors{$pkg});
				}
			} else {
				push @output, "Result: updates has the wrong version.";
				$prepare{$pkg}++;
				# prepare means rebuilding, so locales will be updated at
				# the same time.
				next;
			}
		} elsif (defined $skip{$pkg}) {
			push @output, "Duplicate: need to purge $pkg from updates.";
			$remove{$pkg}++;
		}
	} elsif (defined $outdated{$pkg}) {
		push @output, "Prepare: $pkg source not in updates.";
		$prepare{$pkg}++;
	}
	if (defined $localestable{$pkg}{'source'}) {
		my $lsrc = $localestable{$pkg}{'source'};
		$lsrc =~ s/em1tdeb$//;
		if (&is_same ($lsrc, $debianstable{$pkg}{'source'}) == 0) {
			$skip_locale{$pkg}++;
		} else {
			push @output, "LocaleStableVersion: $lsrc";
		}
	}
	if (defined $localeupdate{$pkg}{'source'}) {
		my $lsrc = $localeupdate{$pkg}{'source'};
		if (&is_same ($lsrc, $debianstable{$pkg}{'source'}) == 0) {
			push @output, "LocaleResult: migrate from updates";
			$migrate_locale{$pkg}++;
		}
	}
	print join ("\n", @output)."\n\n" unless (defined $skip{$pkg});
}
printf(ngettext("Err: Error in %s package\n",
	"Err: Errors in %s packages\n",scalar keys (%errors)), scalar keys (%errors));
print Dumper (\%errors);
print "\n\n";
printf(ngettext("INF: To prepare: %s package: %s\n\n", "INF: To prepare: %s packages: %s\n\n",
	scalar keys (%prepare)), scalar keys (%prepare), join (" ", sort keys %prepare));
printf(ngettext("INF: To migrate: %s package: %s\n\n", "INF: To migrate: %s packages: %s\n\n",
	scalar keys (%migrate)), scalar keys (%migrate), join (" ", sort keys %migrate));
printf(ngettext("INF: To remove : %s package from updates: %s\n\n",
	"INF: To remove : %s packages from updates: %s\n\n",
	scalar keys (%remove)), scalar keys (%remove), join (" ", sort keys %remove));
&skipping if (defined $list_skips);
&prepare if (defined $prep);
&migration if (defined $do_release);
&removal if (defined $purge);
print _g("INF: Done.\n");
exit (0);

sub skipping {
	printf(ngettext("INF: Skipping: %s package: %s\n\n",
	"INF: Skipping: %s packages: %s\n\n", scalar keys (%skip)),
	scalar keys (%skip), join (" ", sort keys %skip));
}

sub prepare {
	&set_dry_run if (not defined $go);
	foreach my $src (sort keys %prepare) {
		my $ver = $debianstable{$src}{'source'};
		my $src_name = $src;
		if (not defined $ver) {
			$src_name = $debianstable{$src}{'Src'};
			# bug: default fallback of armel will break sometimes.
			$ver = $debianstable{$src}{'armel'};
			if (not defined $ver) {
				die ("debug: no version in filter stable for armel.\n");
			}
			$ver =~ s/\+b[0-9]$//;
		}
		if (defined $go) {
			&grip_source ($src_name, $ver, 'stable-proposed-updates', 'source');
		} else {
			print "$src, $ver, stable-proposed-updates, source\n";
		}
	}
	foreach my $bin (sort keys %binarch) {
		foreach my $arch (sort @{$binarch{$bin}}) {
			next if ($arch eq 'source');
			my $bver = (defined $debupdate{$bin}{$arch}) ?
				$debupdate{$bin}{$arch} : $debianstable{$bin}{$arch};
			if (defined $go) {
				&grip_binary ($bin, $bver, 'stable-proposed-updates', $arch);
			} else {
				print "binary: $bin, $bver, stable-proposed-updates, $arch\n";
			}
		}
	}
}

sub migration {
	foreach my $query (sort keys %migrate) {
		my $v = $debianstable{$query}{'source'};
		my $from = 'stable-proposed-updates';
		my $to = 'stable';
		printf(_g("INF: Migrating %s (%s) into %s %s.\n"), $query, $v, $grip_name, $to);
		print "reprepro -v -b ${base}${grip_name} copysrc $to $from $query\n"
			if (not defined $go);
		system ("reprepro -v -b ${base}${grip_name} copysrc $to $from $query")
			if (defined $go);
		if (defined $migrate_locale{$query}) {
			printf(_g("INF: Migrating TDebs for %s (%s) into %s %s.\n"),
				$query, $v, $locale_name, $to);
			print "reprepro -v -b ${base}${locale_name} copysrc $to $from $query\n"
				if (not defined $go);
			system ("reprepro -v -b ${base}${locale_name} copysrc $to $from $query")
				if (defined $go);
		}
	}
}

sub removal {
	foreach my $dupe (sort keys %remove) {
	print "reprepro -v -b $base${grip_name} -v removesrc stable-proposed-updates $dupe\n"
		if (not defined $go);
	system "reprepro -v -b $base${grip_name} -v removesrc stable-proposed-updates $dupe"
		if (defined $go);
	print "reprepro -v -b $base${locale_name} -v removesrc stable-proposed-updates $dupe\n"
		if (not defined $go);
	system "reprepro -v -b $base${locale_name} -v removesrc stable-proposed-updates $dupe"
		if (defined $go);
	}
}

exit 0;

=head1 Copyright and Licence

 Copyright (C) 2009-2010  Neil Williams <codehelp@debian.org>

 This package is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 3 of the License, or
 (at your option) any later version.

 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 General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

=head1 Errors and bugs

point-release.pl doesn't cope particularly well if stable-proposed-updates
needs to have a version of a package which already exists in testing or
unstable. If reprepro refuses to accept a prepared package because it
already exists, try copying that package into stable-proposed-updates.
Note that C<copysrc> only copies components and architectures which
are present in the source distribution. Use C<includedeb> to bring in
the binary packages. e.g.

 reprepro -C dev includedeb stable-proposed-updates pool/dev/c/cvs/*

This can occur if B<all> of the binary packages from a particular source
package are located in components other than B<main>.

Other problems can occur when a binary package has changed source
package. This is particularly problematic when Grip needs only a few of
the packages provided by a particular source package.

Using the C<single> option does cut out some of the calculations, so
if a package already exists in another suite, the package will need to
be migrated using the full call to C<point-release.pl>.

=cut

=head1 Plan

 Package must exist in Debian stable (filter)
 If same version is already in Grip stable, add to %skip
 If same version is also in Grip updates, add to %remove
 If older (or no) version is in Grip stable, add to %outdated
 If correct version is already in %outdated, add to %migrate

 If same version is already in locale stable, add to %skip_locale
 If same version is also in locale updates, add to %remove_locale
 If older version is in Grip stable, add to %outdated_locale
 If correct version is already in %outdated_locale, add to %migrate_locale

 If exists $skip{$pkg} and $skip_locale{$pkg}, next.

 In %migrate:
 If some architectures are missing, add to %prepare

=cut

sub usageversion {
	printf(STDERR (_g("
%s - handle updates to stable
version %s

Syntax: %s -b PATH [OPTIONS]
        %s -?|-h|--help|--version

Commands:
-b|--base-path PATH:           path to the top level grip directory [required]
-?|-h|--help|--version:        print this help message and exit

Options:
-n|--dry-run:                  check which packages would be processed
-l|--list-skipped:             list packages which do not need to be changed.
-p|--prepare:                  build any missing packages first
-P|--purge:                    remove duplicates afterwards
-m|--migrate:                  migrate all suitable packages
-M|--mirror MIRROR:            use a different Debian mirror for updates
                                [default: http://ftp.uk.debian.org/debian]
   --filter-name STRING:       alternative name for the filter repository
   --grip-name STRING:         alternative name for the grip repository

The default is to summarise the status of all the packages currently in
Debian stable versus Grip stable and Grip stable-proposed-updates,
for all architectures.

%s will only handle stable and stable-proposed-updates.

%s also updates the locale repository, shared by Emdebian
Grip and Emdebian Crush.

"), $prog, $our_version, $prog, $prog, $prog, $prog))
	or die ("$0: "._g("failed to write usage").": $!\n");
}

# a little jiggery-pokery to get the version
# from the hash in a collated manner.
sub collate_version {
	my $e = shift;
	my @list = values %$e;
	my %h=();
	foreach my $l (@list) {
		next if (not defined $l);
		next if ($l !~ /^[0-9]/);
		$h{$l}++;
	}
	@list=();
	@list = sort keys %h;
	return \@list;
}

sub is_newer {
	my $ourVer = shift;
	if (not defined $ourVer) {
		print _g("Undefined comparison version.\n");
		return 0;
	}
	$ourVer =~ s/em1$//;
	my $debianVer = shift;
	if (not defined $debianVer) {
		print _g("Undefined original version.\n");
		return 0;
	}
	my $retval = system ("dpkg --compare-versions $debianVer '>>' $ourVer");
	$retval /= 256;
	return $retval;
}

sub is_same {
	my $ourVer = shift;
	if (not defined $ourVer) {
		print _g("Undefined comparison version.\n");
		return 0;
	}
	$ourVer =~ s/em1$//;
	my $debianVer = shift;
	if (not defined $debianVer) {
		print _g("Undefined original version.\n");
		return 0;
	}
	my $retval = system ("dpkg --compare-versions $debianVer '=' $ourVer");
	$retval /= 256;
	return $retval;
}
