package fs::format;

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

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

=head1 SYNOPSYS

B<fs::format> enables to format filesystems.

=head1 Variables

=over

=item %cmds

Commands to format filesystem:

For each filesystem, list: [ package_name, command_to_use, options_to_use ]

=cut

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


=item %LABELs

mkfs option to use in order to set the label + label specs.

For each filesystem, list: [ option, max_length, handled_by_mount ]

=cut

my %LABELs = (
    ext2     => [ '-L', 16, 1 ],
    ext3     => [ '-L', 16, 1 ],
    ext4     => [ '-L', 16, 1 ],
    exfat    => [ '-L', 16, 1 ],
    f2fs     => [ '-l', 16, 1 ],
    reiserfs => [ '-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 ],
);

=item %edit_LABEL

Commands to set the file system label.

For each filesystem, list: [ package, command, option ]

If option is defined, run <command> <option> <label> <device>

If no option, run <command> <device> <label>

=cut

my %edit_LABEL = ( # 
    ext2     => [ 'e2fsprogs', 'tune2fs', '-L' ],
    ext3     => [ 'e2fsprogs', 'tune2fs', '-L' ],
    ext4     => [ 'e2fsprogs', 'tune2fs', '-L' ],
    reiserfs => [ 'reiserfsprogs', 'reiserfstune', '-l' ],
    xfs      => [ 'xfsprogs', 'xfs_admin', '-L' ],
    jfs      => [ 'jfsutils', 'jfs_tune', '-L' ],
#    hfs
    dos      => [ 'mtools', 'mlabel', '-i' ],
    exfat    => [ 'exfatprogs', 'tune.exfat', '-L' ],
    vfat     => [ 'mtools', 'mlabel', '-i' ],
    swap     => [ 'util-linux', 'swaplabel', '-L' ],
    ntfs     => [ 'ntfs-3g', 'ntfslabel' ],
   'ntfs-3g' => [ 'ntfs-3g', 'ntfslabel' ],
    btrfs    => [ 'btrfs-progs', 'btrfs', 'filesystem', 'label' ],
    nilfs2   => [ 'nilfs-utils', 'nilfs-tune', '-L' ],
);

=item %preserve_UUID

For each filesystem, list: [ option, max_length, handled_by_mount ]

Those are used in order to preserve UUID on fs where we couldn't enforce it while formatting.

=cut

my %preserve_UUID = ( # package, command
    jfs      => [ 'jfsutils', 'jfs_tune', ],
    xfs      => [ 'xfsprogs', 'xfs_admin' ],
    nilfs2   => [ 'nilfs-utils', 'nilfs-tune' ],
);


=back

=head1 Functions

=over

=item package_needed_for_partition_type($part)

Return the package needed for that partition's type.

=cut

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 Mageia 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 Mageia One)
}

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

=item part($all_hds, $part, $wait_message)

Frontend to part_raw()

=cut

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);
    }
    undef $part->{toFormat};
}

=item write_label($part)

Set the label on the filesystem hold in $part.

=cut

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 ($cmd eq 'btrfs') {
      # btrfs needs reverse ordering
      @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 write_btrfs_uuid {
    my ($UUID, $dev) = @_;
    $dev = devices::make($dev);
    my $status = system("echo y|btrfstune -U $UUID $dev") == 0;
    die "failed to set UUID to '$UUID' on $dev (status=$status)" if !$status;
}

=item sub option_to_preserve_UUID_while_formating($part, $fs_type)

Return the options needed to preserve UUID while formating

=cut

sub option_to_preserve_UUID_while_formating {
    my ($part, $fs_type) = @_;
    if (member($fs_type, qw(swap ext2 ext3 ext4))) {
	return '-U', $part->{device_UUID} if $part->{device_UUID};
    } elsif ($fs_type eq 'reiserfs') {
	return '-u', $part->{device_UUID} if $part->{device_UUID};
    }
    return ();
}

=item part_raw($part, $wait_message)

Actually format the $part partition disk. $wait_message is only used when formating ext3/4.
If not set, ext[3-4] will be formated without any progression bar, like other fses...

=cut

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';
    }

    push @options, option_to_preserve_UUID_while_formating($part, $fs_type);
    
    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_after_format($dev, $part, $fs_type);

    if (member($fs_type, qw(ext3 ext4))) {
	disable_forced_fsck($dev);
    }

    after_formatting($part);
}

=item preserve_UUID_after_format($dev, $part, $fs_type)

Preserve UUID on fs where we couldn't enforce it while formatting

=cut

sub preserve_UUID_after_format {
    my ($dev, $part, $fs_type) = @_;
    if (my $uuid_cmd = $preserve_UUID{$fs_type}) {
	my (undef, $cmd) = @$uuid_cmd;
	run_program::raw({}, $cmd, '-U', $part->{device_UUID}, devices::make($dev)) if $cmd;
    } elsif ($fs_type eq 'btrfs' && $part->{device_UUID}) {
	write_btrfs_uuid($part->{device_UUID}, $dev);
    }
}

=item after_formatting($part)

Misc post formating tags (rereading UUID & setting state)

=cut

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);
}

=item mkfs_ext3($wait_message, @args)

Display a progression bar whike formating ext3/4

=cut

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);
}

=item disable_forced_fsck($dev)

Disable automatic fsck on extX (infinite number of mounts & duration between 2 fsck runs)

=cut

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)
    if (!$part->{isMounted} && member($part->{fs_type}, qw(ext2 ext3 ext4))
	  && ($part->{mntpoint} eq '/home' ||
		!fs::get::has_mntpoint('/home', $all_hds) && $part->{mntpoint} eq '/')) {
	run_program::run('tune2fs', '-o', 'user_xattr', 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);
    };
}

=back

=cut

1;
