package fs::format; # $Id$

use diagnostics;
use strict;
use String::ShellQuote;

use run_program;
use common;
use fs::type;
use fs::loopback;
use log;

my %cmds = (
    ext2     => [ 'e2fsprogs', 'mkfs.ext2', '-F' ],
    ext3     => [ 'e2fsprogs', 'mkfs.ext3', '-F' ],
    ext4     => [ 'e2fsprogs', 'mkfs.ext4', '-F' ],
    reiserfs => [ 'reiserfsprogs', 'mkfs.reiserfs', '-ff' ],
    reiser4  => [ 'reiser4progs', 'mkfs.reiser4', '-f', '-y' ],
    xfs      => [ 'xfsprogs', 'mkfs.xfs', '-f', '-q' ],
    jfs      => [ 'jfsutils', 'mkfs.jfs', '-f' ],
    hfs      => [ 'hfsutils', 'hformat' ],
    dos      => [ 'dosfstools', 'mkdosfs' ],
    vfat     => [ 'dosfstools', 'mkdosfs', '-F', '32' ],
    swap     => [ 'util-linux-ng', 'mkswap' ],
    ntfs     => [ 'ntfsprogs', 'mkntfs', '--fast' ],
   'ntfs-3g' => [ 'ntfsprogs', 'mkntfs', '--fast' ],
    btrfs    => [ 'btrfs-progs', 'mkfs.btrfs' ],
    nilfs2   => [ 'nilfs-utils', 'mkfs.nilfs2' ],
);

my %LABELs = ( #- option, length, handled_by_mount
    ext2     => [ '-L', 16, 1 ],
    ext3     => [ '-L', 16, 1 ],
    ext4     => [ '-L', 16, 1 ],
    reiserfs => [ '-l', 16, 1 ],
    reiser4  => [ '-L', 16, 1 ],
    xfs      => [ '-L', 12, 1 ],
    jfs      => [ '-L', 16, 1 ],
    hfs      => [ '-l', 27, 0 ],
    dos      => [ '-n', 11, 0 ],
    vfat     => [ '-n', 11, 0 ],
    swap     => [ '-L', 15, 1 ],
    ntfs     => [ '-L', 128, 0 ],
   'ntfs-3g' => [ '-L', 128, 0 ],
    btrfs    => [ '-L', 256, 1 ],
    nilfs2   => [ '-L', 16, 1],
);

my %edit_LABEL = ( # package, command, option
# If option is defined, run <command> <option> <label> <device>
# If no option, run <command> <device> <label>
    ext2     => [ 'e2fsprogs', 'tune2fs', '-L' ],
    ext3     => [ 'e2fsprogs', 'tune2fs', '-L' ],
    ext4     => [ 'e2fsprogs', 'tune2fs', '-L' ],
    reiserfs => [ 'reiserfsprogs', 'reiserfstune', '-l' ],
#    reiser4
    xfs      => [ 'xfsprogs', 'xfs_admin', '-L' ],
    jfs      => [ 'jfsutils', 'jfs_tune', '-L' ],
#    hfs
    dos      => [ 'mtools', 'mlabel', '-i' ],
    vfat     => [ 'mtools', 'mlabel', '-i' ],
#    swap     => [ 'util-linux-ng', 'mkswap' ],
    ntfs     => [ 'ntfsprogs', 'ntfslabel' ],
   'ntfs-3g' => [ 'ntfsprogs', 'ntfslabel' ],
#    btrfs
#    nilfs2
);

sub package_needed_for_partition_type {
    my ($part) = @_;
    my $l = $cmds{$part->{fs_type}} or return;
    $l->[0];
}

sub known_type {
    my ($part) = @_;
    to_bool($cmds{$part->{fs_type}});
}

sub check_package_is_installed_format {
    my ($do_pkgs, $fs_type) = @_;

    my ($pkg, $binary) = @{$cmds{$fs_type} || return};
    whereis_binary($binary) || $do_pkgs->ensure_binary_is_installed($pkg, $binary); #- ensure_binary_is_installed checks binary chrooted, whereas we run the binary non-chrooted (pb for Mandriva One)
}

sub check_package_is_installed_label {
    my ($do_pkgs, $fs_type) = @_;

    my ($pkg, $binary) = @{$edit_LABEL{$fs_type} || return};
    whereis_binary($binary) || $do_pkgs->ensure_binary_is_installed($pkg, $binary); #- ensure_binary_is_installed checks binary chrooted, whereas we run the binary non-chrooted (pb for Mandriva One)
}

sub canEditLabel {
    my ($part) = @_;
    to_bool($edit_LABEL{$part->{fs_type}});
}

sub part {
    my ($all_hds, $part, $wait_message) = @_;
    if (isRAID($part)) {
	$wait_message->(N("Formatting partition %s", $part->{device})) if $wait_message;
	require raid;
	raid::format_part($all_hds->{raids}, $part);
    } elsif (isLoopback($part)) {
	$wait_message->(N("Creating and formatting file %s", $part->{loopback_file})) if $wait_message;
	fs::loopback::format_part($part);
    } else {
	$wait_message->(N("Formatting partition %s", $part->{device})) if $wait_message;
	part_raw($part, $wait_message);
    }
}

sub write_label {
    my ($part) = @_;

    $part->{device_LABEL_changed} or return;
    maybeFormatted($part) or return;

    if ($part->{encrypt_key}) {
	fs::mount::set_loop($part);
    }

    my $dev = $part->{real_device} || $part->{device};
    my ($_pkg, $cmd, @first_options) = @{$edit_LABEL{$part->{fs_type}} || die N("I do not know how to set label on %s with type %s", $part->{device}, $part->{fs_type})};
    my @args;
    if ($cmd eq 'mlabel') {
      @args = ($cmd, @first_options, devices::make($dev), '::' . $part->{device_LABEL});
    } elsif (defined $first_options[0]) {
      @args = ($cmd, @first_options, $part->{device_LABEL}, devices::make($dev));
    } else {
      @args = ($cmd, devices::make($dev), $part->{device_LABEL});
    }
    run_program::raw({ timeout => 'never' }, @args) or die N("setting label on %s failed, is it formatted?", $dev);
    delete $part->{device_LABEL_changed};
}

sub part_raw {
    my ($part, $wait_message) = @_;

    $part->{isFormatted} and return;

    if ($part->{encrypt_key}) {
	fs::mount::set_loop($part);
    }

    my $dev = $part->{real_device} || $part->{device};

    my @options = if_($part->{toFormatCheck}, "-c");
    log::l("formatting device $dev (type $part->{fs_type})");

    my $fs_type = $part->{fs_type};

    if (member($fs_type, qw(ext2 ext3 ext4))) {
	push @options, "-m", "0" if $part->{mntpoint} =~ m|^/home|;
    } elsif (isDos($part)) {
	$fs_type = 'dos';
    } elsif ($fs_type eq 'hfs') {
        push @options, '-l', "Untitled";
    } elsif (isAppleBootstrap($part)) {
	push @options, '-l', 'bootstrap';
    }

    # Preserve UUID
    if (member($fs_type, 'swap', 'ext2', 'ext3', 'ext4')) {
	push @options, '-U', $part->{device_UUID} if $part->{device_UUID};
    } elsif ($fs_type eq 'reiserfs') {
	push @options, '-u', $part->{device_UUID} if $part->{device_UUID};
    }

    if ($part->{device_LABEL}) {
	push @options, @{$LABELs{$fs_type}}[0], $part->{device_LABEL};
    }

    my ($_pkg, $cmd, @first_options) = @{$cmds{$fs_type} || die N("I do not know how to format %s in type %s", $part->{device}, $part->{fs_type})};

    my @args = ($cmd, @first_options, @options, devices::make($dev));

    if ($cmd =~ /^mkfs.ext[34]$/ && $wait_message) {
	mkfs_ext3($wait_message, @args) or die N("%s formatting of %s failed", $fs_type, $dev);
    } else {
	run_program::raw({ timeout => 'never' }, @args) or die N("%s formatting of %s failed", $fs_type, $dev);
    }

    delete $part->{device_LABEL_changed};

    # Preserve UUID on fs where we couldn't enforce it while formatting
    if ($fs_type eq 'jfs') {
	run_program::raw('jfs_tune', '-U', devices::make($dev));
    } elsif ($fs_type eq 'xfs') {
	run_program::raw('xfs_admin', '-U', devices::make($dev));
    } elsif ($fs_type eq 'btrfs') {
	#FIXME
    }
    
    if (member($fs_type, qw(ext3 ext4))) {
	disable_forced_fsck($dev);
    }

    after_formatting($part);
}

sub after_formatting {
    my ($part) = @_;

    my $p = fs::type::type_subpart_from_magic($part);
    $part->{device_UUID} = $p && $p->{device_UUID};

    set_isFormatted($part, 1);
}

sub mkfs_ext3 {
    my ($wait_message, @args) = @_;

    my $cmd = shell_quote_best_effort(@args);
    log::l("running: $cmd");
    open(my $F, "$cmd |");

    local $/ = "\b";
    local $_;
    while (<$F>) {
	#- even if it still takes some time when format is over, we don't want the progress bar to stay at 85%
	$wait_message->('', $1, $2) if m!^\s*(\d+)/(\d+)\b!;
    }
    return close($F);
}

sub disable_forced_fsck {
    my ($dev) = @_;
    run_program::run("tune2fs", "-c0", "-i0", devices::make($dev));
}

sub clean_label {
    my ($part) = @_;
    if ($part->{device_LABEL}) {
	my $fs_type = $part->{fs_type};
	if ($LABELs{$fs_type}) {
	    my ($option, $length, $handled_by_mount) = @{$LABELs{$fs_type}};
	    if (length $part->{device_LABEL} > $length) {
		my $short = substr($part->{device_LABEL}, 0, $length);
		log::l("shortening LABEL $part->{device_LABEL} to $short");
		$part->{device_LABEL} = $short;
	    }
	    delete $part->{prefer_device_LABEL}
	      if !$handled_by_mount || $part->{mntpoint} eq '/' && !member($fs_type, qw(ext2 ext3 ext4));
	} else {
	    log::l("dropping LABEL=$part->{device_LABEL} since we don't know how to set labels for fs_type $fs_type");
	    delete $part->{device_LABEL};
	    delete $part->{prefer_device_LABEL};
	    delete $part->{device_LABEL_changed};
	}
    }
}

sub formatMount_part {
    my ($part, $all_hds, $fstab, $wait_message) = @_;

    if (isLoopback($part)) {
	formatMount_part($part->{loopback_device}, $all_hds, $fstab, $wait_message);
    }
    if (my $p = fs::get::up_mount_point($part->{mntpoint}, $fstab)) {
	formatMount_part($p, $all_hds, $fstab, $wait_message) if !fs::type::carry_root_loopback($part);
    }

    clean_label($part);

    if ($part->{toFormat}) {
	fs::format::part($all_hds, $part, $wait_message);
    } else {
	fs::format::write_label($part);
    }

    #- setting user_xattr on /home (or "/" if no /home)
    #- setting acl everywhere
    if (!$part->{isMounted} && member($part->{fs_type}, qw(ext2 ext3 ext4))) {
	my $default_options = 'acl';
	if ($part->{mntpoint} eq '/home' ||
	    (!fs::get::has_mntpoint('/home', $all_hds) && $part->{mntpoint} eq '/')) {
	    $default_options .= ',user_xattr';
	}
	run_program::run('tune2fs', '-o', $default_options, devices::make($part->{real_device} || $part->{device}));
    }

    fs::mount::part($part, 0, $wait_message);
}

sub formatMount_all {
    my ($all_hds, $fstab, $wait_message) = @_;
    formatMount_part($_, $all_hds, $fstab, $wait_message)
      foreach sort { isLoopback($a) ? 1 : isSwap($a) ? -1 : 0 } grep { $_->{mntpoint} } @$fstab;

    #- ensure the link is there
    fs::loopback::carryRootCreateSymlink($_) foreach @$fstab;

    #- for fun :)
    #- that way, when install exits via ctrl-c, it gives hand to partition
    eval {
	my ($_type, $major, $minor) = devices::entry(fs::get::root($fstab)->{device});
	output "/proc/sys/kernel/real-root-dev", makedev($major, $minor);
    };
}


1;