#!/usr/bin/perl
#
# Author: Petter Reinholdtsen <pere@hungry.com>
# Date:   2001-08-23
#
# # $Id: cdd-gen-control 330 2004-10-18 21:18:17Z tille $
#
# Generate the control file used by the Debian Edu task package.

use warnings;
use strict;

use Getopt::Std;
use File::Path;

use vars qw(%opts %available %excluded %included @wanted %missing
	    @tasks $debug);
my @arch = qw(alpha arm i386 ia64 m68k mips mipsel powerpc s390 sparc hppa);

my $debug = 0;
my $nodepends = 0;

my %taskinfo = ();

my $aptsources = "/etc/cdd/sources.list";
my $tmpaptsources = "" ;

my %commondepends ;
$commondepends{"adduser"}    = "0" ;
$commondepends{"debconf"}    = "1.2" ;
$commondepends{"menu"}       = "2.1.14" ;
# the VERSION variable is replaced in the dist target of debian/rules
$commondepends{"cdd-common"} = "0.3.10" ;

my $CommonPackage = "" ;
my $prefix        = "test-" ;
my $cddname       = "" ;
my $cddshortname  = "" ;

getopts("cdaemis:tD", \%opts);

$tmpaptsources = $opts{'s'} if ($opts{'s'});

if ( -s $tmpaptsources ) {
    $aptsources = $tmpaptsources;
} else {
    if ( -s $aptsources.".".$tmpaptsources ) {
        $aptsources .= ".".$tmpaptsources ;
    }
}

$debug = 1 if ($opts{'d'});
$nodepends = 1 if ($opts{'D'});

load_available_packages();

load_tasks();

if ($opts{'c'}) {
    gen_control();
} else {
    if ($opts{'e'}) {
	print_excluded_packages();
    } elsif ($opts{'a'}) {
	print_all_packages();
    } else {
	print_available_packages();
    }
}
print_missing_packages() if ($opts{'m'});
print_task_desc()        if ($opts{'t'});

sub apt {
    my $op = shift;

    my $aptdir  = "/tmp/cdd-apt";
    my @aptopts = ("Dir::Etc::sourcelist=$aptsources",
		   "Dir::State=$aptdir/state",
		   "Dir::Cache=$aptdir/cache",
		   "Dir::State::Status=/dev/null",
		   "Debug::NoLocking=true");

    # Stupid apt-get and apt-cache do not understand the same arguments!
    # I have to map them to different formats to get both working.

    if ("update" eq $op) {
	mkpath "$aptdir/state/lists/partial";
	mkpath "$aptdir/cache/archives/partial";

	my $aptget   = "apt-get --assume-yes -o " . join(" -o ", @aptopts);

	print STDERR "aptget: $aptget\n" if $debug;
	system("$aptget update 1>&2");
    } elsif ("apt-cache" eq "$op") {
	my $aptcache = "apt-cache -o=" . join(" -o=", @aptopts);
	print STDERR "aptcache: $aptcache\n" if $debug;
	return $aptcache;
    }
}

sub gen_control {
    my $task;

    for $task (sort keys %taskinfo) {
	print "Package: $task\n";
	my $header;
# Leave out XBCS-Task for the moment ...
#	for $header (qw(Section Architecture XBCS-Task Priority)) {
	for $header (qw(Section Architecture Priority)) {
	    print "$header: $taskinfo{$task}{$header}\n"
		if (defined $taskinfo{$task}{$header});
	}

	if ($nodepends) {
		# degrade dependencies to recommends and recommends to
		# suggests
		print "Depends: education-tasks\n";
	    	print "Recommends: ", join(", ", sort @{$taskinfo{$task}{Depends}}),"\n"
			if defined $taskinfo{$task}{Depends};
		my @suggests;
		push @suggests, @{$taskinfo{$task}{Recommends}} if defined $taskinfo{$task}{Recommends};
		push @suggests, @{$taskinfo{$task}{Suggests}} if defined $taskinfo{$task}{Suggests};
	    	print "Suggests: ", join(", ", sort @suggests),"\n" if @suggests;
	}
	else {
	    for $header (qw(Pre-Depends Depends Suggests Recommends)) {
		if (defined $taskinfo{$task}{$header}) {
		    print "$header: ";
		    # Print -common page with versioned dependency if necessary
		    if ( $header =~ /Depends/ ) {
			unless ( $CommonPackage =~ /^$/ ) { print $prefix."common (= $CommonPackage), "; }
		    }
		    print join(", ", sort @{$taskinfo{$task}{$header}}),"\n" ;
		}
	    }
	}


	# Description Description-long
	print "Description: $taskinfo{$task}{Description}\n";
	print "$taskinfo{$task}{'Description-long'}"; # Already contain newline

	print "\n";
    }
}

sub print_task_desc {
	if (! -d "tasksel") {
		mkdir("tasksel") || die "mkdir tasksel: $!";
	}
	
	my $descout = $cddname . "-task.desc" ;
	unless ( open(TASKDESC, ">$descout" ) ) { die "Unable to open $descout\n" ; }
	select TASKDESC;
	foreach my $task (sort keys %taskinfo) {
		next if (exists $taskinfo{$task}{'Leaf'} &&
			$taskinfo{$task}{'Leaf'} eq 'false');
		
		print "Task: $task\n";
		print "Section: $cddname\n";
		print "Description: $taskinfo{$task}{Description}\n";
		print "$taskinfo{$task}{'Description-long'}"; # Already contain newline
		print "Packages: ".$cddshortname."-task-files\n";
		print "Relevance: 10\n";
		print "Key: \n";
		print " $task\n";
		
		print "\n";

		open (OUT, ">tasksel/$task") || die "tasksel/$task: $!";
		print OUT "$task\n";
		foreach my $package (task_packages($task)) {
			print OUT "$package\n";
		}
		close OUT;
	}
	close (TASKDESC) ;
}

sub task_packages {
	my $task=shift;
	my @packages=@_;
	foreach my $package (@{$taskinfo{$task}{Depends}}) {
		if ($package=~/\|/) {
			# Tasksel doesn't allow boolean oring of
			# dependencies. Just take the first one that is
			# available.
			my $ok=0;
			foreach my $alternative (split(' | ', $package)) {
	    			if (! exists $taskinfo{$alternative} &&
				    ! exists $available{$alternative}) {
				    	if (! exists $missing{$alternative}) {
				    		$missing{$alternative} = 1;
				    	}
				}
				else {
					print STDERR "task_packages: choosing $alternative from $package\n" if $debug;
					$package=$alternative;
					$ok=1;
					last;
				}
			}
			if (! $ok) {
				next;
			}
		}
		if (exists $taskinfo{$package}) {
			# Add packages from task recursively, since
			# tasksel does not support dependent tasks of
			# the type used by $cddname
			push @packages, $package, task_packages($package);
		}
		else {
			push @packages, $package;
		}
	}
	return @packages;
}

#
# Check the APT cache, and find the packages currently available.
#
sub load_available_packages
{
    apt("update");
    my $aptcache = apt("apt-cache");
    open(APT, "$aptcache dump |") || die "Unable to start apt-cache";
    my $pkg;
    while (<APT>) {
	chomp;
	if (/^Package: (.+)$/) {
	    $pkg = $1;
	    print STDERR "Found pkg '$pkg'\n" if $debug;
	}
	if (/^\s+Version:\s+(.+)/) {
	    print STDERR " pkg $pkg = ver $1\n" if $debug;
#	    print "C: $pkg $available{$pkg} lt $1\n" if ( exists $available{$pkg});
	    $available{$pkg} = $1 if ( ! exists $available{$pkg} ||
				       $available{$pkg} lt $1 );
	}
    }
}

#
# Load all tasks
#
sub load_tasks {
    my $taskfile;

    unless  ( -d "debian" ) {
	mkdir("debian") || die "mkdir debian: $!";
    }

    # make sure that old control file is removed first
    my $controlfile = "debian/control" ;
    if ( -s $controlfile ) { unlink($controlfile) || die "rm $controlfile: $!" ; }

    unless ( open(CTRL, ">$controlfile" ) ) { die "Unable to open $controlfile\n" ; }
    select CTRL;
    unless ( open(STUB, "debian/control.stub" ) ) {
        print STDERR "No template debian/control.stub.  Use test prefix.\n" ;
    } else {
        while ( <STUB> ) {
            if ( /^Package: (.+)$/) {
                $prefix  = $1."-";
		$cddshortname = $1 ;
		$cddname = "debian-".$cddshortname ;
		last ;
            } else {
                print ;
            }
        }
	close(STUB) ;
    }
print STDOUT "DEBUG: CDDNAME = $cddname\n" ;
    unless ( $cddname ) { die "Unable to parse CDD name\n" ; }
    
    # if there is a file common/control append this to control file
    if ( -s "common/control" && open(COMMON, "common/control") ) {
	my $dependsflag = 0;
	my @finaldeps ;
	my $cdep ;
        while ( <COMMON> ) {
	    if ( m/Depends:\s*(.*)/ ) {
		my $dep ;
		my @deps = split(/\s*,\s*/, $1) ;
		@finaldeps = () ;
		foreach $dep ( @deps ) {
		    my $version = "0" ;

		    if ( $dep =~ /([^\s]+)\s*\(\s*>*=+\s*([\d\.]*)\s*\)/ ) {
			$version = $2;
			$dep     = $1;
		    }
		    foreach $cdep ( keys %commondepends ) {
			if ( $dep =~ $cdep ) {
			    if ( ! system("dpkg --compare-versions $version lt $commondepends{$cdep}") ) {
				$version = $commondepends{$cdep} ;
			    }
			    last ;
			}
		    }
		    unless ( $version =~ /^0$/ ) { @finaldeps = (@finaldeps, "$dep (>= $version)") ; }
		    else                         { @finaldeps = (@finaldeps, "$dep") ; }
		}
		print "Depends: " . join(", ", @finaldeps) . "\n";
		$dependsflag = 1;
		next ;
	    }
	    if ( m/Description:/ ) {
		if ( $dependsflag == 0 ) {
		    @finaldeps = () ;
		    for $cdep (keys %commondepends) {
			unless ( $commondepends{$cdep} =~ /^0$/ ) { @finaldeps = (@finaldeps, "$cdep (>= $commondepends{$cdep})") ; }
			else                                    { @finaldeps = (@finaldeps, "$cdep") ; }
		    }
		    print "Depends: " . join(", ", @finaldeps) . "\n";
		    $dependsflag = 1;
		}
	    }
	    print ;
	}
	# make sure that there is at least one newline after description ...
	print "\n";
	close(COMMON);

	# calculate meta packages version number to make all meta packages depend from exactly the same common package
	open PARSED, "dpkg-parsechangelog |"
	    or die "Cannot execute dpkg-parsechangelog: $!";

	while (<PARSED>) {
	    chomp;
	    if ( /^Version:\s([.\d]+)$/) { $CommonPackage = $1; last ;}
	}
	if ( $CommonPackage =~ /^$/ ) { die "dpkg-parsechangelog does not contain package version number."; }
    }
    
    # First document their existence, so they can depend on each other.
    for $taskfile (<tasks/*>) {
	next if ("tasks/CVS" eq $taskfile);
	if ("tasks/common" eq $taskfile) { die "You cannot use 'common' as task name"; }
	next if ($taskfile =~ m/~$/);

	my $curpkg = $taskfile;
	$curpkg =~ s%tasks/%$prefix%;
	$available{$curpkg} = "n/a";

	push(@tasks, "$taskfile:$curpkg");
    }

    # Next, load their content.
    my $foo;
    for $foo (@tasks) {
	my ($taskfile, $curpkg) = $foo =~ m/^(.+):(.+)$/;
	next if ("tasks/CVS" eq $taskfile);
	
	load_task($taskfile, $curpkg);
    }
}

sub process_pkglist {
    my $pkgstring = shift;
    my @pkglist = ();
    my @missinglist = ();
    my $packages;
    for $packages (split(/\s*,\s*/, $pkgstring)) {
	print "E: double comma?: $_\n" if ($packages =~ /^\s*$/ && $debug);
	my $package_w_version;
	my $package;
	my $pkg_ok = 0;
	for $package_w_version (split(/\s*\|\s*/, $packages)) {
	    $package_w_version =~ /^([^ ]+)\s*\(*.*$/ ;
	    $package = $1 ;
	    print STDERR "Loading pkg '$package'\n" if $debug;
	    # if one package string was found accept the whole combination
	    # of alternatives or versioned depends
	    # The available veriosn numbers will currently not be checked.
	    # I'll leave this task to those who need this feature ;-) (at)
	    if ( exists $available{$package} ) {
		push(@pkglist, $packages);
		# push(@wanted, $packages);
		$included{$package} = 1;
		$pkg_ok = 1;
		last;
	    }
	}
	if ( ! $pkg_ok ) {
	    for $package_w_version (split(/\s*\|\s*/, $packages)) {
		$package_w_version =~ /^([^ ]+)\s+\(.*$/ ;
		$package = $1 ;
		print STDERR "Analyzing pkg '$package' in detail\n" if $debug;
		if ($package =~ /^-(.+)$/) {
		    $excluded{$1} = 1;
		} elsif ( !exists $available{$package} ) {
		    if ( !exists $missing{$package}) {
			$missing{$package} = 1;
		    }
		    push(@missinglist, $package_w_version);
		} elsif ( ! $included{$package} ) {
		    push(@wanted, $package_w_version);
		    $included{$package} = 1;
		}
	    }
	}
    }
    return (\@pkglist, \@missinglist);
}

sub load_task {
    my ($taskfile, $curpkg) = @_;
    open(TASKFILE, "<$taskfile") || die "Unable to open $taskfile";
    my $line;

    $taskinfo{$curpkg} = ();

    print STDERR "Loading task $curpkg\n" if $debug;

    while (<TASKFILE>) {
	chomp;
	next if (m/^\#/); # Skip comments
	$line = $_;

	# Append multi-line
	while ($line =~ /\\$/) {
	    $line =~ s/\s*\\//;
	    $_ = <TASKFILE>;
	    chomp;
	    $line .= $_;
	}
	# Remove trailing space
	$line =~ s/\s+$//;

	$_ = $line;
	$taskinfo{$curpkg}{'Section'}      = $1 if (m/^Section:\s+(.+)$/);
	$taskinfo{$curpkg}{'Architecture'} = $1 if (m/^Architecture:\s+(.+)$/);
	# This task name is needed in the Menu file but the former tag "Architecture"
	# is never used and was left out here
	$taskinfo{$curpkg}{'XBCS-Task'}    = $1 if (m/^Task:\s+(.+)$/);

	$taskinfo{$curpkg}{'Priority'}     = $1 if (m/^Priority:\s+(.+)$/);
	$taskinfo{$curpkg}{'Leaf'}         = $1 if (m/^Leaf:\s+(.+)$/);

	# Set default architecture to all to be backward compatible
	# with previous versions.  
	if (!$taskinfo{$curpkg}{'Architecture'}) {
	    $taskinfo{$curpkg}{'Architecture'} = 'all';
	}

	if (m/^Description:\s+(.+)$/) {
	    $taskinfo{$curpkg}{'Description'} = $1;
	    $taskinfo{$curpkg}{'Description-long'} = "";
	    while (<TASKFILE>) {
		# End of description, pass next line to pattern matching
		last if (m/^\S+/ || m/^\s*$/);

		$taskinfo{$curpkg}{'Description-long'} .= $_;
	    }
	}

	next unless defined $_;

	my $header;
	for $header (qw(Pre-Depends Depends Suggests Recommends)) {
	    if (m/^$header:\s+(.+)$/ && $1 !~ /^\s*$/) {
		$taskinfo{$curpkg}{$header} = ()
		    if (! exists $taskinfo{$curpkg}{$header});
		my ($pkglist, $missinglist) = process_pkglist($1);
		push(@{$taskinfo{$curpkg}{$header}}, @{$pkglist});

		# Avoid missing packages in Depends lists, allow them
		# in the two others.  Insert missing depends in
		# suggests list.
		if (@{$missinglist}) {
		    if ("Depends" eq $header) {
			push(@{$taskinfo{$curpkg}{'Suggests'}}, @{$missinglist});
		    } else {
			push(@{$taskinfo{$curpkg}{$header}}, @{$missinglist});
		    }
		}
	    }
	}

	if (/^Avoid:\s+(.+)$/) {
	    my @pkgs = split(/\s*,\s*/, $1);
	    my $packages;
	    for $packages (@pkgs) {
	        my $package;
	    	for $package (split(/\s*\|\s*/, $packages)) {
		    $excluded{$package} = 1;
		}
	    }
	}

	if (/^Ignore:\s+(.+)$/) {
	    my @pkgs = split(/\s*,\s*/, $1);
	    my $packages;
	    for $packages (@pkgs) {
	        my $package;
	    	for $package (split(/\s*\|\s*/, $packages)) {
		    # Remove explanations, ie the paranteses at the end.
		    $package =~ s/\s*\([^\)]*\)\s*$//;
		    $missing{$package} = 1;
		}
	    }
	}
    }
    close(TASKFILE);
}

sub print_excluded_packages {
    print join("\n", sort keys %excluded),"\n";
}

sub print_available_packages {
    print join("\n", @wanted),"\n";
}

sub print_all_packages {
    print STDERR "Printing all packages\n" if $debug;
    print join("\n", @wanted, keys %missing),"\n";
}

sub print_missing_packages {
    if (%missing) {
	print STDERR "Missing or avoided packages:\n";
	my $package;
	for $package (sort keys %missing) {
	    if (exists $available{$package}) {
	        print STDERR "  $package (v$available{$package} available)\n";
	    } else {
	        print STDERR "  $package\n";
	    }
	}
	exit 1 unless $opts{'i'};
    }
}
