#!/usr/bin/perl

use lib qw(/usr/lib/libDrakX ../perl-install);
use standalone;
use fsedit;
use fs::format;
use fs::type;
use resize_fat::main;
use diskdrake::resize_ntfs;
use diskdrake::resize_ext2;
use common;
use partition_table::empty;
use Carp::Heavy;

#- help getting the file in make_rescue_img
BEGIN { partition_table::raw::default_type() }

my ($server);
if ($ARGV[0] eq '-s') {
    (undef, $server, @ARGV) = @ARGV;
} 
my $action = shift @ARGV;

sub usage() { die "partimage_whole_disk [-s <server>] (save_all <dir> | rest_all <dirs>)\n" }

$ENV{PATH} = "/sbin:/usr/sbin:$ENV{PATH}";
$ENV{HOME} = '/';
log::openLog("/var/log/partimage_whole_disk.log");
my @partimage_cmd = ('partimage', if_($server, '-s', $server));
my $all_hds = fsedit::get_hds({});

if ($action eq 'save_all') {
    @ARGV == 1 or usage();
    save_all($ARGV[0]);
} elsif ($action eq 'rest_all') {
    @ARGV or usage();
    rest_all(@ARGV);
} else {
    usage();
}

sub save_all {
    my ($dir) = @_;

    my $base_dir = $dir;
    for (my $i = 0; read_part_list($dir); $i++) {
	#- find a free dir
	$dir = sprintf("$base_dir.%03d", $i);
    }

    my $hd = $all_hds->{hds}[0] or die "no main hard drive\n";
    log::l("save_all on $hd->{device}");
    my $part_list = [ partition_table::get_normal_parts($hd) ];

    foreach (@$part_list) {
	$_->{saved} = !member($_->{fs_type}, 'ntfs', 'vfat', 'swap');
    }

    #- shrink and don't save the last ext3 partition (which is the /home partition)
    if (my $part = find { isTrueLocalFS($_) } reverse @$part_list) {
	$part->{size} = min($part->{size}, 1024 * 1024 * 2); # not greater than 1GB
	$part->{saved} = 0;
    }

    foreach (grep { $_->{saved} } @$part_list) {
	run_or_die(@partimage_cmd,
		   '-V', 0, '--nombr', '--nodesc', '--nocheck', '-b', '-o',
		   'save', devices::make($_->{device}), "$dir/$_->{device}");
    }
    save_part_list($dir, $hd->{geom}, $part_list);
}
sub rest_all {
    my (@dirs) = @_;

    my %best;
    DIRS: foreach (@dirs) {
	my ($forced_geom, $part_list) = read_part_list($_) or log::l("read_part_list $_ failed"), next;
	my %h = (dir => $_, forced_geom => $forced_geom, part_list => $part_list);
	log::l("trying with $h{dir}");

	($h{hd}) = my @used_hds = uniq(map {
	    my $part = $_;
	    find { $part->{device} =~ /^\Q$_->{device}\E./ } fs::get::hds($all_hds) 
	      or log::l("can't find hard drive for partition $part->{device}"), next DIRS;
	} @$part_list);

	@used_hds >= 1 or log::l("no matching hd"), next;
	@used_hds <= 1 or log::l("multiple hds: " . join(' ', map { $_->{device} } @used_hds)), next;

	fs::type::set_fs_type($_, $_->{fs_type}) foreach @$part_list;
	put_in_hash($_, partition_table::hd2minimal_part($h{hd})) foreach @$part_list;

	($h{from_partimage}, my $other) = partition { $_->{saved} } @$part_list;
	($h{from_resize}, $h{created}) = partition { member($_->{fs_type}, 'vfat', 'ntfs') } @$other;

	$h{valid_resize} = every {
	    my $part = fs::get::device2part($_->{device}, [ fs::get::fstab($all_hds) ]) or log::l("partition to resize is missing ($_->{device})");
	    my $ok = $part && $part->{fs_type} eq $_->{fs_type};
	    $ok or log::l("partition $_->{device} doesn't have the right filesystem ($part->{fs_type} != $_->{fs_type})"); 
	    $ok;
	} @{$h{from_resize}};
	
	$h{total} = sum(map { $_->{size} } @$part_list);
	if ($h{total} > $h{hd}{totalsectors}) {
	    log::l("discarding $h{dir} since total $h{total} > $h{hd}{totalsectors}");
	} elsif ($h{valid_resize} < $best{valid_resize}) {
	    log::l("discarding $h{dir} since it has invalid resize whereas $best{dir} has valid resize");
	} elsif ($h{total} > $best{total}) {
	    log::l("$h{dir} is better than $best{dir} since total $h{total} > $best{total}");
	    %best = %h;
	} else {
	    log::l("$h{dir} is not better than $best{dir} since total $h{total} <= $best{total}");
	}
    }

    $best{dir} or die "no valid partimage data dirs\n";
    my %h = %best;
    log::l("chosen dir $h{dir}");

    foreach (@{$h{from_resize}}) {
	#- resize first
	my $part = fs::get::device2part($_->{device}, [ fs::get::fstab($all_hds) ]);
	if (!$part) {
	    log::l("partition to resize is missing ($_->{device})");
	    $_->{missing} = 1;
	    next;
	}
	if ($part->{fs_type} ne $_->{fs_type}) {
	    log::l("partition $_->{device} doesn't have the right filesystem ($part->{fs_type} != $_->{fs_type})");
	    $_->{missing} = 1;
	    next;
	}

	if (@{$h{from_resize}} == 1) {
	    my $half_size = int($h{hd}{totalsectors} / 2) - 2 * $h{hd}->cylinder_size;
	    my $suggested_total = $h{total} - $_->{size} + $half_size;
	    log::l("resizing bigger? (size $_->{size}, half_size $half_size, total $h{total}, suggested_total $suggested_total)");
	    if ($half_size > $_->{size} && $suggested_total < $h{hd}{totalsectors}) {
		log::l("prefering to resize $_->{device} to size $half_size instead of $_->{size}");
		$_->{size} = $half_size;
	    }
	}

	$_->{start} = $part->{start};
	if ($_->{size} < $part->{size}) {
	    log::l("resizing $_->{device} to $_->{size} (it is $part->{size})");
	    my $resize_pkg = $_->{fs_type} eq 'vfat' ? 'resize_fat::main' : 'diskdrake::resize_ntfs';
	    my $resize = $resize_pkg->new($_->{device}, devices::make($_->{device}));
	    $resize->resize($_->{size});
	} else {
	    log::l("no need to resize, instead setting $_->{device}'s size to $part->{size} instead of $_->{size}");
	    $_->{size} = $part->{size};
	}
    }

    my $hd = $h{hd};
    put_in_hash($hd->{geom}, $h{forced_geom});
    log::l("totalsectors $hd->{totalsectors} heads $hd->{geom}{heads} sectors $hd->{geom}{sectors}");
    partition_table::raw::compute_nb_cylinders($hd->{geom}, $hd->{totalsectors});

    #- grow the last ext3 partition
    if (my $part = find { isTrueLocalFS($_) } reverse @{$h{part_list}}) {
	$part->{ratio} = 1;
    }

    #- write the partition table
    partition_table::raw::zero_MBR($hd);
    foreach my $part (grep { $_->{rootDevice} eq $hd->{device} } @{$h{part_list}}) {
	next if $part->{missing};

	my $hole = find { isEmpty($_) && $_->{size} >= $part->{size} } partition_table::get_normal_parts_and_holes($hd) or die "not enough room for $part->{device}";
	$part->{start} = $hole->{start};
	    
	log::l("handling $part->{device}");
	my $extended = $part->{device} =~ /(\d+)$/ && $1 > 4 && $hd->hasExtended;

	my %wanted_part = %$part;
	if ($part->{ratio}) {
	    $part->{size} = $hole->{size};
	} else {
	    $part->{size} += $hd->{geom}{sectors} if $extended;
	    $part->{size} += $hd->cylinder_size if $part->{start} == 1;
	}
	log::l("adding $part->{device} with size $part->{size}");
	partition_table::add($hd, $part, $extended && 'Extended');
	foreach ('device', if_(!$part->{ratio}, 'size')) {
	    $part->{$_} eq $wanted_part{$_} or log::l("bad $_ for $part->{device}: $part->{$_} != $wanted_part{$_}");
	}
    }
    partition_table::write($hd);

    #- restore from partimage
    foreach (@{$h{from_partimage}}) {
	run_or_die(@partimage_cmd, 'restore', '-b', devices::make($_->{device}), "$h{dir}/$_->{device}");

	if ($_->{ratio}) {
	    my $resize = diskdrake::resize_ext2->new($_->{device}, devices::make($_->{device}));
	    $resize->resize($_->{size});
	}
    }

    foreach (@{$h{created}}) {
	fs::format::part_raw($_, undef);
    }

    run_program::run('guessmounts');

    if (my @missing = grep { $_->{missing} } @{$h{part_list}}) {
	my $missing = join('|', map { quotemeta($_->{device}) } @missing);
	log::l("drop missing devices from fstab and lilo.conf: $missing");
	$::prefix = '/mnt';
	substInFile { $_ = '' if m!^/dev/($missing)\s! } "$::prefix/etc/fstab";

	my $match;
	substInFile { 
	    /^\S/ and $match = m!^other=/dev/($missing)$!; 
	    $_ = '' if $match;
	} "$::prefix/etc/lilo.conf";
    }
    
    run_program::run('install_bootloader');
}

sub lst_fields() { qw(device size fs_type saved) }
sub save_part_list {
    my ($dir, $geom, $part_list) = @_;
    my @l = map { join(' ', @$_{lst_fields()}) } @$part_list;
    log::l("save_part_list $dir: $_") foreach @l;
    my $partimage = join(' ', @partimage_cmd);
    open(my $F, "| $partimage -z0 -Bfoo=bar -o save_file $dir/lst");
    print $F join("/", $geom->{heads}, $geom->{sectors}), "\n";
    print $F "$_\n" foreach @l;
}
sub read_part_list {
    my ($dir) = @_;
    my $partimage = join(' ', @partimage_cmd);
    open(my $F, "$partimage -z0 -Bfoo=bar rest_file $dir/lst |");
    my $geom_string = <$F> or return;
    my %geom; @geom{'heads', 'sectors'} = split('/', chomp_($geom_string));
    my @l = chomp_(cat__($F));
    log::l("read_part_list $dir: $_") foreach @l;
    \%geom, [ map { my %l; @l{lst_fields()} = split; \%l } @l ];
}

sub run_or_die {
    my (@l) = @_;
    run_program::raw({ timeout => 4 * 60 * 60 }, @l) or die join(' ', @l) . " failed\n";
}