#!/usr/bin/perl -w

use strict ;
no strict "refs" ; # for $backend{$file_type}

require Lltag::Tags ;
require Lltag::Misc ;
require Lltag::MP3 ;
require Lltag::MP3v2 ;
require Lltag::OGG ;
require Lltag::FLAC ;
require Lltag::CDDB ;
require Lltag::Parse ;
require Lltag::Rename ;

use Getopt::Long ;
Getopt::Long::Configure('noignorecase', 'noautoabbrev', 'bundling') ;

#######################################################
# main hash for globals and config
my $self = {} ;

#######################################################
# UTF-8 locale ?

use I18N::Langinfo qw(langinfo CODESET) ;

$self->{utf8} = ( langinfo (CODESET ()) eq "UTF-8" ) ;

#######################################################
# configuration file location
$self->{common_lltag_dir} = "/etc/lltag" ;
$self->{user_lltag_dir} = "$ENV{HOME}/.lltag" ;
$self->{lltag_format_filename} = "formats" ;
$self->{lltag_config_filename} = "config" ;
$self->{lltag_edit_history_filename} = "edit_history" ;

#######################################################
Lltag::Misc::init_readline ($self) ;

#######################################################
# format parameters

@{$self->{field_names}} = ('ARTIST', 'TITLE', 'ALBUM', 'NUMBER', 'GENRE', 'DATE', 'COMMENT') ;
@{$self->{field_letters}} = ('a', 't', 'A', 'n', 'g', 'd', 'c') ;

%{$self->{field_name_letter}} =
    (
     'ARTIST'  => 'a',
     'TITLE'   => 't',
     'ALBUM'   => 'A',
     'NUMBER'  => 'n',
     'GENRE'   => 'g',
     'DATE'    => 'd',
     'COMMENT' => 'c',
     ) ;

%{$self->{field_name_trailing_spaces}} =
    (
     'ARTIST' => '  ',
     'TITLE' => '   ',
     'ALBUM' => '   ',
     'NUMBER' => '  ',
     'GENRE' => '   ',
     'DATE' => '    ',
     'COMMENT' => ' ',
     ) ;

%{$self->{field_letter_name}} =
    (
     'a' => 'ARTIST',
     't' => 'TITLE',
     'A' => 'ALBUM',
     'n' => 'NUMBER',
     'g' => 'GENRE',
     'd' => 'DATE',
     'c' => 'COMMENT',
     ) ;

# change format letters into a parsing string
$self->{field_letters_union} = (join '|', @{$self->{field_letters}}) ;
# cache the list of letters for later
$self->{field_letters_string} = (join '', @{$self->{field_letters}}) ;

#######################################################
# version
my $version = "0.14.6" ;

sub version {
    print "This is lltag version $version.\n" ;
    exit 0 ;
}

#######################################################
# usage
sub usage {
    print $0." $version is a frontend to tag and rename MP3/OGG/FLAC files automagically.\n" ;
    print "Usage: ".$0." [options] files...\n" ;
    print " Tagging options:\n" ;
    print "  -F, --format <format>  Try format (multiple instances allowed)\n" ;
    print "  -G, --guess            Guess format (default)\n" ;
    print "  -C, --cddb             Query tags in CDDB\n" ;
    print "  -E, --edit             Edit tags\n" ;
    print map { "  -". $self->{field_name_letter}{$_} .", --". $_.$self->{field_name_trailing_spaces}{$_}
		    ."<val>    Add explicit value <val> for ". ucfirst($_) ."\n" } @{$self->{field_names}} ;
    print "  --tag <TAG=val>        Add explicit value <val> to tag <TAG>\n" ;
    print "  -p, --no-path          Remove the path from filenames when matching\n" ;
    print "  --spaces               Allow no or multiple spaces\n" ;
    print "  --maj                  Upcase first letters of words in tags\n" ;
    print "  --sep <s1|s2|...>      Replace |-separated strings with space in tags\n" ;
    print "  --regexp <regexp>      Apply a replace regexp to tags before tagging\n" ;
    print "  --mp3/--ogg/--flac     Force mp3, ogg or flac instead of by-extension detection\n" ;
    print "  --type <type>          Force <type> instead of by-extension detection\n" ;
    print "  --mp3v2, --id3v2       Enable experimental ID3v2 backend for MP3 files\n" ;
    print "  --mp3read=[12]         Read ID3v1 and v2 MP3 tags (default is 21)\n" ;
    print "  --clear                Clear all tags of audio files when possible\n" ;
    print "  --append               Append tags only instead of replacing old ones when possible\n" ;
    print "  --no-tagging           Do not actually tag files\n" ;
    print "  -T, --preserve-time    Preserve file modification during tagging\n" ;
    Lltag::Rename::rename_usage ($self) ;
    print " General options:\n" ;
    print "  --dry-run              Do nothing but show what would have been done\n" ;
    print "  --yes                  Tag without asking for confirmation when guessing\n" ;
    print "                         Rename without asking for confirmation\n" ;
    print "  --ask                  Always ask for confirmation before tagging\n" ;
    print "  -R, --recursive        Recursively search all files in subdirectories\n" ;
    print "  -v, --verbose          More verbose messages\n" ;
    print "  -q, --quiet            Less verbose messages\n" ;
    print "  --config <file>        Read additional configuration file\n" ;
    print "  --gencfg <file>        Generate additional configuration file\n" ;
    print " CDDB options:\n" ;
    print "  --cddb-query <query>   Start with CDDB query <query> by keywords or id\n" ;
    print "  --cddb-server <server> Change the CDDB server and port\n" ;
    print " Behavior options:\n" ;
    print "  -S                     Show all tags from files\n" ;
    print "  --show-tags <tag,..>   Show several tags from files\n" ;
    print "  -L, --list             List internal formats\n" ;
    print "  -V, --version          Show lltag version\n" ;
    print "  -h, --help             Show this help\n" ;
    print " Format is composed of anything you want with special fields:\n" ;
    print map { "  %". $self->{field_name_letter}{$_} ." means ". ucfirst($_) ."\n" } @{$self->{field_names}} ;
    Lltag::Parse::parsing_format_usage ($self) ;
    Lltag::Rename::rename_format_usage ($self) ;
    print "\n" ;
    print "Author:    Brice Goglin\n" ;
    print "Homepage:  http://bgoglin.free.fr/lltag\n" ;
    print "Report bugs to:  <lltag AT googlegroups.com>\n" ;
    exit 1;
}

#######################################################
# options

my $verbose_level = 0 ; # do not display menu usage information

$self->{dry_run_opt} = 0 ;
$self->{recursive_opt} = 0 ;
$self->{list_formats_opt} = 0 ;
$self->{show_tags_opt} = "" ;
$self->{no_tagging_opt} = 0 ;
$self->{preserve_time} = 0 ;

%{$self->{explicit_values}} = () ;

$self->{ask_opt} = 0 ;
$self->{guess_opt} = 0 ;
$self->{cddb_opt} = 0 ;
$self->{edit_opt} = 0 ;
$self->{no_path_opt} = 0 ;
$self->{maj_opt} = 0 ;
$self->{sep_opt} = undef ;
$self->{spaces_opt} = 0 ;
$self->{type_opt} = undef ;
$self->{mp3v2_opt} = 0 ;
$self->{mp3v2_read_opt} = Lltag::MP3v2->MP3V2_READ_V2_V1 ;
$self->{yes_opt} = 0 ;
$self->{clear_opt} = 0 ;
$self->{append_opt} = 0 ;
@{$self->{regexp_opts}} = () ;

$self->{rename_opt} = undef ;
$self->{rename_min_opt} = 0 ;
$self->{rename_sep_opt} = " " ;
$self->{rename_ext_opt} = 0 ;
@{$self->{rename_regexp_opts}} = () ;
$self->{rename_slash_opt} = "-" ;

$self->{gencfg_file} = undef ;

@{$self->{user_format_strings}} = () ;

$self->{cddb_server_name} = "tracktype.org" ;
$self->{cddb_server_port} = 80 ;

# command line given cddb query, undefined afterwards
$self->{requested_cddb_query} = undef ;

#######################################################
# parse config files first

my @additional_config_files = () ;

# process these options but kept other options in @ARGV for later
Getopt::Long::Configure('passthrough') ;
GetOptions(
	   'config=s'     => \@additional_config_files,
	   ) ;

# restore default behavior: process all options and warn on error
Getopt::Long::Configure('nopassthrough') ;

sub process_option {
    $_ = shift ;
    chomp $_ ;
    if (/^format\s*=\s*"(.+)"$/) {
	push (@{$self->{user_format_strings}}, $1) ;
    } elsif (/^guess\s*=\s*(.+)$/) {
	$self->{guess_opt} = $1 ;
    } elsif (/^no_path\s*=\s*(.+)$/) {
	$self->{no_path_opt} = $1 ;
    } elsif (/^tag\s*=\s*"(.+)"$/) {
	Lltag::Tags::process_explicit_tag_value ($self, $1) ;
    } elsif (/^spaces\s*=\s*(.+)$/) {
	$self->{spaces_opt} = $1 ;
    } elsif (/^maj\s*=\s*(.+)$/) {
	$self->{maj_opt} = $1 ;
    } elsif (/^sep\s*=\s*"(.*)"$/) {
	$self->{sep_opt} = $1 ;
    } elsif (/^type\s*=\s*(.+)$/) {
	if ($1 eq "none") {
	    $self->{type_opt} = undef ;
	} else {
	    $self->{type_opt} = $1 ;
	}
    } elsif (/^regexp\s*=\s*"(.+)"$/) {
	push (@{$self->{regexp_opts}}, $1) ;
    } elsif (/^clear_tags\s*=\s*(.+)$/) {
	$self->{clear_opt} = $1 ;
    } elsif (/^append_tags\s*=\s*(.+)$/) {
	$self->{append_opt} = $1 ;
    } elsif (/^no_tagging\s*=\s*(.+)$/) {
	$self->{no_tagging_opt} = $1 ;
    } elsif (/^preserve_time\s*=\s*(.+)$/) {
	$self->{preserve_time} = $1 ;
    } elsif (/^rename_format\s*=\s*"(.*)"$/) {
	if ($1 eq "") {
	    $self->{rename_opt} = undef ;
	} else {
	    $self->{rename_opt} = $1 ;
	}
    } elsif (/^rename_min\s*=\s*(.+)$/) {
	$self->{rename_min_opt} = $1 ;
    } elsif (/^rename_sep\s*=\s*"(.*)"$/) {
	$self->{rename_sep_opt} = $1 ;
    } elsif (/^rename_slash\s*=\s*"(.*)"$/) {
	$self->{rename_slash_opt} = $1 ;
    } elsif (/^rename_regexp\s*=\s*"(.+)"$/) {
	push (@{$self->{rename_regexp_opts}}, $1) ;
    } elsif (/^rename_ext\s*=\s*(.+)$/) {
	$self->{rename_ext_opt} = $1 ;

    } elsif (/^dry_run\s*=\s*(.+)$/) {
	$self->{dry_run_opt} = $1 ;
    } elsif (/^yes\s*=\s*(.+)$/) {
	$self->{yes_opt} = $1 ;
	$self->{ask_opt} = 0 if $1 ;
    } elsif (/^ask\s*=\s*(.+)$/) {
	$self->{ask_opt} = $1 ;
	$self->{yes_opt} = 0 if $1 ;
    } elsif (/^recursive\s*=\s*(.+)$/) {
	$self->{recursive_opt} = $1 ;
    } elsif (/^verbose\s*=\s*(.+)$/) {
	$verbose_level = $1 ;
    } elsif (/^cddb\s*=\s*(.+)$/) {
	$self->{cddb_opt} = $1 ;
    } elsif (/^cddb_server_name\s*=\s*"(.+)"$/) {
	$self->{cddb_server_name} = $1 ;
    } elsif (/^cddb_server_port\s*=\s*(\d+)$/) {
	$self->{cddb_server_port} = $1 ;
    } elsif (/^edit = \s*(.+)$/) {
	$self->{edit_opt} = $1 ;
# Error
    } elsif (/^[^#]/ && !/^(\s*)$/) {
	die "Unrecognized option line #$.: \"$_\"\n" ;
    }
}

sub parse_generic_config_file {
    my $file = shift ;
    open CONF, $file
	or return ;
    while (<CONF>) {
	process_option $_ ;
    }
    close CONF ;
}

parse_generic_config_file "$self->{common_lltag_dir}/$self->{lltag_config_filename}" ;
parse_generic_config_file "$self->{user_lltag_dir}/$self->{lltag_config_filename}" ;

sub parse_additional_config_file {
    my $file = shift ;
    open CONF, $file
	or die "Failed to open additional configuration file '$file' ($!).\n" ;
    while (<CONF>) {
	process_option $_ ;
    }
    close CONF ;
}

foreach my $file (@additional_config_files) {
    parse_additional_config_file $file ;
}

#######################################################
# parse cmdline options

# user format list given on the command-line,
# to be added to front of config file user_format_strings
my @cmdline_user_format_strings = () ;

# parse options
GetOptions(
	   'F|format=s'		=> \@cmdline_user_format_strings,
	   'G|guess'		=> \$self->{guess_opt},
	   'C|cddb'		=> \$self->{cddb_opt},
	   'E|edit'		=> \$self->{edit_opt},
	   'p|no-path'		=> \$self->{no_path_opt},
	   'spaces'		=> \$self->{spaces_opt},
	   'maj'		=> \$self->{maj_opt},
	   'sep=s'		=> \$self->{sep_opt},
	   'regexp=s'		=> \@{$self->{regexp_opts}},
	   # we do not use backends here since it would require to load them
	   # before parsing the command-line, and is useless anyway
	   'mp3'		=> sub { $self->{type_opt} = "mp3" ; },
	   'ogg'		=> sub { $self->{type_opt} = "ogg" ; },
	   'flac'		=> sub { $self->{type_opt} = "flac" ; },
	   'type=s'		=> sub { shift ; $self->{type_opt} = shift ; },
	   'mp3v2|id3v2'        => \$self->{mp3v2_opt},
	   'mp3read=s'          => \$self->{mp3v2_read_opt},
	   'clear'		=> \$self->{clear_opt},
	   'append'		=> \$self->{append_opt},
	   'no-tagging'		=> \$self->{no_tagging_opt},
	   'T|preserve-time'	=> \$self->{preserve_time},
	   'rename=s'		=> \$self->{rename_opt},
	   'rename-min'		=> \$self->{rename_min_opt},
	   'rename-sep=s'	=> \$self->{rename_sep_opt},
	   'rename-slash=s'	=> \$self->{rename_slash_opt},
	   'rename-regexp=s'	=> \@{$self->{rename_regexp_opts}},
	   'rename-ext'		=> \$self->{rename_ext_opt},
	   'dry-run'		=> \$self->{dry_run_opt},
	   'R|recursive'	=> \$self->{recursive_opt},
	   'yes'		=> sub { $self->{yes_opt} = 1 ; $self->{ask_opt} = 0 ; },
	   'ask'		=> sub { $self->{ask_opt} = 1 ; $self->{yes_opt} = 0 ; },
	   'cddb-server=s'	=> sub { shift ; my $name = shift ;
					 if ($name =~ m/(.*):(.*)/) {
					     $self->{cddb_server_name} = $1 ;
					     $self->{cddb_server_port} = $2 ;
					 } else {
					     $self->{cddb_server_name} = $name ;
					     $self->{cddb_server_port} = 80 ;
					 }
				       },
	   'cddb-query=s'	=> \$self->{requested_cddb_query},
	   'v|verbose'		=> sub { shift ; $verbose_level++ ; },
	   'q|quiet'		=> sub { shift ; $verbose_level-- ; },
	   'gencfg=s'		=> \$self->{gencfg_file},
	   'h|help'		=> sub { usage () ; },
	   'V|version'		=> sub { version () ; },
	   'L|list'		=> \$self->{list_formats_opt},
	   'show-tags=s'	=> \$self->{show_tags_opt},
	   'S'			=> sub { $self->{show_tags_opt} = "all" ; },
	   'tag=s'		=> sub { shift ; Lltag::Tags::process_explicit_tag_value ($self, shift) ; },
	   map { my $field = $_ ;
		 $self->{field_name_letter}{$field} .'|'. $field .'=s' => sub { shift ; my $value = shift ; Lltag::Tags::process_explicit_tag_value ($self, "$field=$value") },
	       } @{$self->{field_names}} ,
	   ) or usage () ;

# add the command-line user format list to the front of the config file user_format_strings
unshift @{$self->{user_format_strings}},@cmdline_user_format_strings ;

# check that MP3v2 ID3v1 and v2 read order is corect
die "Invalid MP3 ID3v1 and v2 order given to --mp3read: $self->{mp3v2_read_opt}\n"
    if $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V1
    and $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V2
    and $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V1_V2
    and $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V2_V1 ;

# set verbosity options depending of the level
$self->{verbose_opt} = ($verbose_level > 1) ;
$self->{menu_usage_once_opt} = ($verbose_level > 0) ;

# if --cddb-query is passed, enable cddb too
$self->{cddb_opt} = 1
    if defined $self->{requested_cddb_query} ;

# yes/ask option status may vary with user confirmation replies
my $current_main_yes_opt = $self->{yes_opt} ;
Lltag::CDDB::init_cddb ($self) ;
Lltag::Parse::init_parsing ($self) ;
Lltag::Tags::init_tagging ($self) ;
Lltag::Rename::init_renaming ($self) ;

#######################################################
# backends

# hash backends by type
my %backends = () ;

# hash types by file extention
my %file_extension_to_backend_type = () ;

# return 0 on success, -1 on error, 1 on busy
sub register_backend {
    my $backend = shift ;
    return -1
	if not defined $backend ;

    my $backend_extension = $backend->{extension} ;
    my $backend_type = $backend->{type} ;
    my $backend_name = $backend->{name} ;

    if (defined $backends{$backend_type}) {
	print "Failed to register backend '$backend_name' since file type '$backend_type' already exists.\n"
	    if $self->{verbose_opt} ;
	return 1 ;
    }
    if (defined $file_extension_to_backend_type{$backend_extension}) {
	print "Failed to register backend '$backend_name' since file extenstion '$backend_extension' already exists.\n"
	    if $self->{verbose_opt} ;
	return 1 ;
    }

    $backends{$backend_type} = $backend ;
    $file_extension_to_backend_type{$backend_extension} = $backend_type ;

    print "Registered backend '$backend_name' for type '$backend_type' extension '$backend_extension'.\n"
	if $self->{verbose_opt} ;

    return 0 ;
}

# register MP3v2 backend if enabled, and fallback to MP3 if failed
if (!$self->{mp3v2_opt}
    or register_backend (Lltag::MP3v2::new ($self)) < 0) {
    Lltag::Misc::print_warning ("", "Failed to register MP3v2 backend, falling back to MP3.")
	if $self->{mp3v2_opt} ;
    register_backend (Lltag::MP3::new ($self)) ;
}

# register OGG backend
register_backend (Lltag::OGG::new ($self)) ;

# register FLAC backend
register_backend (Lltag::FLAC::new ($self)) ;

#######################################################
# check that the file type is correct
die "Unrecognized file type '$self->{type_opt}'.\n"
    if defined $self->{type_opt} and not defined $backends{$self->{type_opt}} ;

#######################################################
# extract backend and parsename

sub extract_parsename_and_backend {
    my $file = shift ;

    # split into parsename and extension, and remove the path if asked
    my @parts = split (/\./, $file) ;
    my $extension = pop @parts ;
    my $parsename = join (".", @parts) ;
    if ($self->{no_path_opt}) {
	my @parts = split (/\//, $parsename) ;
	$parsename = pop @parts ;
    }

    my $file_type ;
    if (defined $self->{type_opt}) {
	$file_type = $self->{type_opt} ;
    } else {
	# if not forced, get the type from the extension
	$file_type = $file_extension_to_backend_type{lc($extension)} ;
    }

    return ($parsename, undef)
	unless defined $file_type ;

    return ($parsename, $backends{$file_type}) ;
}

#######################################################
# show existing tags if --show-tags was passed

sub read_tags {
    my $file = shift ;
    my $backend = shift ;

    # extract tags as a stream
    my $read_tags_func = $backend->{read_tags} ;
    my $values = &$read_tags_func ($self, $file) ;
    Lltag::Misc::print_warning ("  ", "Failed to get tags from file.")
	unless defined $values ;
    return $values ;
}

#######################################################
# real tagging

sub tag_with_values {
    my $file = shift ;
    my $backend = shift ;
    my $values = shift ;

    # tagging command line
    my $set_tags_func = $backend->{set_tags} ;
    &{$set_tags_func}($self, $file, $values) ;
}

#######################################################
# add explicit_values

sub add_explicit_values {
    my $self = shift ;
    my $values = shift ;

    # append explicit values
    Lltag::Tags::append_tag_values ($self, $values, $self->{explicit_values}) ;

    return $values ;
}

#######################################################
# wrappers for all tag-obtention routines

sub try_parse_with_preferred {
    my $self = shift ;
    my $file = shift ;
    my $parsename = shift ;
    my $old_values = shift ;

    # save old values before merging
    $old_values = Lltag::Tags::clone_tag_values ($old_values) ;

    my ($res, $new_values) = Lltag::Parse::try_to_parse_with_preferred ($self, $file, $parsename) ;
    if ($res == Lltag::Parse->PARSE_SUCCESS) {
	return Lltag::Tags::merge_new_tag_values ($self,
						  $old_values,
						  (add_explicit_values ($self,
									$new_values))) ;
    }

    # sanity check
    die "Unknown tag return value: $res.\n"
	unless $res == Lltag::Parse->PARSE_NO_MATCH ;

    return undef ;
}

sub try_parse {
    my $self = shift ;
    my $file = shift ;
    my $parsename = shift ;
    my $try_internals = shift ;
    my $old_values = shift ;

    # save old values before merging
    $old_values = Lltag::Tags::clone_tag_values ($old_values) ;

    my ($res, $new_values) = Lltag::Parse::try_to_parse ($self, $file, $parsename, $try_internals) ;
    if ($res == Lltag::Parse->PARSE_SUCCESS or $res == Lltag::Parse->PARSE_SUCCESS_PREFERRED) {
	return Lltag::Tags::merge_new_tag_values ($self,
						  $old_values,
						  (add_explicit_values ($self,
									$new_values))) ;
    }

    # sanity check
    die "Unknown tag return value: $res.\n"
	unless $res == Lltag::Parse->PARSE_NO_MATCH or $res == Lltag::Parse->PARSE_ABORT ;

    return undef ;
}

sub try_edit {
    my $self = shift ;
    my $current_values = shift ;

    # save old values before editing
    $current_values = Lltag::Tags::clone_tag_values ($current_values) ;

    # do not add explicit here since this function might
    # be called on current values without need to add them
    my $res = Lltag::Tags::edit_values ($self, $current_values) ;
    if ($res == Lltag::Tags->EDIT_SUCCESS) {
	return $current_values ;
    }

    # sanity check
    die "Unknown edit return value: $res.\n"
	unless $res == Lltag::Tags->EDIT_CANCEL ;

    return undef ;
}

sub try_cddb {
    my $self = shift ;
    my $old_values = shift ;

    # save old values before merging
    $old_values = Lltag::Tags::clone_tag_values ($old_values) ;

    my ($res, $new_values) = Lltag::CDDB::get_cddb_tags ($self) ;
    if ($res == Lltag::CDDB->CDDB_SUCCESS) {
	return Lltag::Tags::merge_new_tag_values ($self,
						  $old_values,
						  (add_explicit_values ($self,
									$new_values))) ;
    }

    # sanity check
    die "Unknown CDDB return value: $res.\n"
	unless $res == Lltag::CDDB->CDDB_ABORT ;

    return undef ;
}

sub try_explicits {
    my $self = shift ;
    my $old_values = shift ;
    my $empty_values = {} ;

    # save old values before merging
    $old_values = Lltag::Tags::clone_tag_values ($old_values) ;

    return Lltag::Tags::merge_new_tag_values ($self,
					      $old_values,
					      (add_explicit_values ($self,
								    $empty_values))) ;
}

#######################################################
# main confirmation loop

my $main_confirm_menu_usage_forced = $self->{menu_usage_once_opt} ;

sub main_confirm_menu_usage {
    Lltag::Misc::print_usage_header ("  ", "Main menu") ;

    print "    y => Yes, use these tags (default)\n" ;
    print "    a => Always yes, stop asking for a confirmation\n" ;
    print "    P => Try to parse the file\n" ;
    print "    C => Query CDDB\n" ;
    print "    E => Edit values\n" ;
    print "    D => Only use explicit values\n" ;
    print "    Z => Reset to no values\n" ;
    print "    R => Revert to old values\n" ;
    print "    O => Display old values\n" ;
    print "    n => Skip tagging and jump to rename\n" ;
    print "    s/q => Skip this file\n" ;
    print "    Q => Quit without tagging anything anymore\n" ;
    print "    h => Show this help\n" ;

    $main_confirm_menu_usage_forced = 0 ;
# TODO update
}

use constant CONFIRM_TAG => 1 ;
use constant CONFIRM_DONT_TAG => 2 ;
use constant CONFIRM_SKIP_FILE => -1 ;
use constant CONFIRM_EXIT => -2 ;

sub main_confirm_menu {
    my $self = shift ;
    my $file = shift ;
    my $parsename = shift ;
    my $current_values = shift ;
    my $old_values = shift ;

    while (1) {
	# display current values
	if (keys %{$current_values}) {
	    print "    Current tag values are:\n" ;
	    Lltag::Tags::display_tag_values ($self, $current_values, "      ") ;
	} else {
	    print "    There are no current tag values.\n" ;
	}

	# display the menu once
	main_confirm_menu_usage
	    if $main_confirm_menu_usage_forced ;
	# ask the user for confirmation
	my $confirm_reply = Lltag::Misc::readline ("  ", "Use these tag values [yaPCEDZROnqQ] (default is yes, h for help)", "", -1) ;

	# if ctrl-d, skip this file
	$confirm_reply = 'q' unless defined $confirm_reply ;

	if ($confirm_reply =~ m/^y/ or $confirm_reply eq "") {
	    return (CONFIRM_TAG, $current_values)

	} elsif ($confirm_reply =~ m/^a/) {
	    $current_main_yes_opt = 1 ;
	    return (CONFIRM_TAG, $current_values)

	} elsif ($confirm_reply =~ m/^n/) {
	    print "    Skipping tagging for this file...\n" ;
	    return (CONFIRM_DONT_TAG, $current_values)

	} elsif ($confirm_reply =~ m/^s/ or $confirm_reply =~ m/^q/) {
	    return (CONFIRM_SKIP_FILE, undef) ;

	} elsif ($confirm_reply =~ m/^Q/) {
	    return (CONFIRM_EXIT, undef) ;

	} elsif ($confirm_reply =~ m/^P/) {
	    my $new_values = try_parse ($self, $file, $parsename, 1, $old_values) ;
	    $current_values = $new_values
		if defined $new_values ;

	} elsif ($confirm_reply =~ m/^C/) {
	    my $new_values = try_cddb ($self, $old_values) ;
	    $current_values = $new_values
		if defined $new_values ;

	} elsif ($confirm_reply =~ m/^E/) {
	    my $new_values = try_edit ($self, $current_values) ;
	    $current_values = $new_values
		if defined $new_values ;

	} elsif ($confirm_reply =~ m/^D/) {
	    $current_values = try_explicits ($self, $old_values) ;

	} elsif ($confirm_reply =~ m/^Z/) {
	    $current_values = {} ;

	} elsif ($confirm_reply =~ m/^R/) {
	    $current_values = Lltag::Tags::clone_tag_values ($old_values) ;

	} elsif ($confirm_reply =~ m/^O/) {
	    # display old values
	    if (keys %{$old_values}) {
		print "      Existing tag values were:\n" ;
		Lltag::Tags::display_tag_values ($self, $old_values, "        ") ;
	    } else {
		print "    There were no tag values.\n" ;
	    }

	} else {
	    main_confirm_menu_usage ;
	}
    }
}

#######################################################
# main process

# if -L was passed, show formats and exit
if ($self->{list_formats_opt}) {
    # read internal parsers
    Lltag::Parse::read_internal_parsers ($self) ;
    # list them
    print "Listing internal parsers:\n" ;
    Lltag::Parse::list_internal_parsers () ;
    exit 0 ;
}

# process remaining command-line arguments as files
my @files = () ;
while ( @ARGV ) {
    if ($self->{recursive_opt}) {
	my $dir = shift @ARGV ;
	open FIND, "find \"$dir\" |" ;
	my @dirfiles = <FIND> ;
	close FIND ;
	foreach my $file (@dirfiles) {
	    chomp $file ;
	    if (-f $file) {
		push (@files, $file) ;
	    }
	}
    } else {
	my $file = shift @ARGV ;
	if (-f $file) {
	    push (@files, $file) ;
	} else {
	    print "Skipping the non-file '$file'\n" ;
	}
    }
}

if (!@files) {
    print "No files to process.\n"
	if $self->{verbose_opt} ;
    # nothing to do (except maybe a config file to generate), so print the help
    usage () unless $self->{gencfg_file} ;
}

# display tags
if ($self->{show_tags_opt}) {
    my @fields_to_show = split (/,/, $self->{show_tags_opt}) ;
    foreach my $file (@files) {
	print "$file:\n"
	    if @files ; # do not print filename if there is only one file

	my ($parsename, $backend) = extract_parsename_and_backend $file ;
	if (not defined $backend) {
	    print "  Skipping this unknown-type file.\n" ;
	    next ;
	}

	my $values = read_tags $file, $backend ;

	foreach my $field (@{$self->{field_names}}, (Lltag::Tags::get_values_non_regular_keys ($self, $values))) {
	    next if not defined $values->{$field} ;
	    next unless grep { /^all$/ } @fields_to_show
		or grep { /^$field$/i } @fields_to_show ;
	    map { print "  $field=".($field =~ / -> _/ ? "<binary data>" : $_)."\n" } (Lltag::Tags::get_tag_value_array ($self, $values, $field)) ;
	}
    }

    exit 0 ;
}

# main reading/parsing/tagging/renaming loop
while ( @files ) {
    my $file = shift @files ;
    my $res ;

    print "\n" ;
    print "Processing file \"".$file."\"...\n" ;

    my ($parsename, $backend) = extract_parsename_and_backend $file ;
    if (not defined $backend) {
	print "  Skipping this file '$file' with unknown type.\n" ;
	next ;
    }

    # read old tags
    my $old_values = read_tags $file, $backend ;

    # FIXME: read the tracknumber and pass it to cddb ?
    # FIXME: parse first, to get the track number?

    my $current_values = undef ;

    # try preferred parser first
    {
	my $new_values = try_parse_with_preferred $self, $file, $parsename, $old_values ;
	if (defined $new_values) {
	    $current_values = $new_values ;
	    goto CONFIRM ;
	}
    }

    # edit, if enabled
    if ($self->{edit_opt}) {
	my $new_values = try_edit ($self, try_explicits ($self, $old_values)) ;
	if (defined $new_values) {
	    $current_values = $new_values ;
	    goto CONFIRM ;
	}
    }

    # try CDDB, if enabled
    if ($self->{cddb_opt}) {
	my $new_values = try_cddb $self, $old_values ;
	if (defined $new_values) {
	    $current_values = $new_values ;
	    goto CONFIRM ;
	}
    }

    # try to parse with all parsers, either user-provided or internals
    # if guess or any user-parser or NOTHING else enabled
    if ($self->{guess_opt}
	or @{$self->{user_format_strings}}
	or ( !(keys %{$self->{explicit_values}})
	     and !$self->{cddb_opt}
	     and !$self->{edit_opt}
	     and !$self->{rename_opt}
	) ) {
	# force guess if NOTHING enabled
	my $try_internals = ($self->{guess_opt}
			     or ( !@{$self->{user_format_strings}}
				  and !(keys %{$self->{explicit_values}})
				  and !$self->{cddb_opt}
				  and !$self->{edit_opt}
				  and !$self->{rename_opt}
				  ) ) ;

	my $new_values = try_parse $self, $file, $parsename, $try_internals, $old_values ;
	if (defined $new_values) {
	    $current_values = $new_values ;
	    goto CONFIRM ;
	}
    }

    $current_values = try_explicits ($self, $old_values) ;

  CONFIRM:
    if ($current_main_yes_opt) {
	$res = CONFIRM_TAG ;
    } else {
	($res, $current_values) = main_confirm_menu $self, $file, $parsename, $current_values, $old_values ;
    }

    if ($res == CONFIRM_EXIT) {
	print "  Exiting...\n" ;
	last ;

    } elsif ($res == CONFIRM_SKIP_FILE) {
	print "  Skipping this file...\n" ;
	next ;

    } elsif ($res == CONFIRM_TAG) {
	if (!$self->{no_tagging_opt}) {
	    # save access and modification times
	    my ($old_atime, $old_mtime) = (stat ($file)) [8, 9] ;
	    # actually tagging
	    tag_with_values $file, $backend, $current_values ;
	    # restore old times if needed
	    utime $old_atime, $old_mtime, $file if $self->{preserve_time} ;
	}
    }

    # sanity check
    die "Unknown confirm menu return value: $res.\n"
	unless $res == CONFIRM_TAG or $res == CONFIRM_DONT_TAG ;

    # renaming
    if (defined $self->{rename_opt}) {
	my $extension = $backend->{extension} ;
	Lltag::Rename::rename_with_values ($self, $file, $extension, $current_values) ;
    }
}

#############################################
# generate configuration file

sub generate_config {
    my $file = shift ;
    die "Cannot generate $file which already exists."
	if -e "$file" ;
    open NEWCFG, ">$file"
	or die "Cannot open $file ($!).\n" ;

    print NEWCFG "# This is a lltag configuration file.\n" ;
    print NEWCFG "# It was automatically generated.\n" ;
    print NEWCFG "# You may modify and reuse it as you want.\n" ;
    print NEWCFG "\n" ;

    map { print NEWCFG "format = \"$_\"\n" ; } @{$self->{user_format_strings}} ;

    print NEWCFG "guess = $self->{guess_opt}\n" ;
    print NEWCFG "edit = $self->{edit_opt}\n" ;
    print NEWCFG "no_path = $self->{no_path_opt}\n" ;

    map {
	my $key = $_ ; my @array = Lltag::Tags::get_tag_value_array($self, $self->{explicit_values}, $_) ;
	map { print NEWCFG "tag = \"$key=$_\"\n" } @array ;
    } (keys %{$self->{explicit_values}}) ;

    print NEWCFG "spaces = $self->{spaces_opt}\n" ;
    print NEWCFG "maj = $self->{maj_opt}\n" ;
    print NEWCFG "sep = \"$self->{sep_opt}\"\n"
	if defined $self->{sep_opt} ;
    map { print NEWCFG "regexp = \"$_\"\n" ; } @{$self->{regexp_opts}} ;
    print NEWCFG "type = ". (defined $self->{type_opt} ? $self->{type_opt} : "none") ."\n" ;
    print NEWCFG "clear_tags = $self->{clear_opt}\n" ;
    print NEWCFG "append_tags = $self->{append_opt}\n" ;
    print NEWCFG "no_tagging = $self->{no_tagging_opt}\n" ;
    print NEWCFG "preserve_time = $self->{preserve_time}\n" ;

    print NEWCFG "rename_format = \"" .($self->{rename_opt} ? $self->{rename_opt} : ""). "\"\n" ;
    print NEWCFG "rename_min = $self->{rename_min_opt}\n" ;
    print NEWCFG "rename_sep = \"$self->{rename_sep_opt}\"\n" ;
    print NEWCFG "rename_slash = \"$self->{rename_slash_opt}\"\n" ;
    map { print NEWCFG "rename_regexp = \"$_\"\n" ; } @{$self->{rename_regexp_opts}} ;
    print NEWCFG "rename_ext = $self->{rename_ext_opt}\n" ;

    print NEWCFG "cddb = $self->{cddb_opt}\n" ;
    print NEWCFG "cddb_server_name = \"$self->{cddb_server_name}\"\n" ;
    print NEWCFG "cddb_server_port = $self->{cddb_server_port}\n" ;

    print NEWCFG "dry_run = $self->{dry_run_opt}\n" ;
    print NEWCFG "yes = $self->{yes_opt}\n" ;
    print NEWCFG "ask = $self->{ask_opt}\n" ;
    print NEWCFG "recursive = $self->{recursive_opt}\n" ;
    print NEWCFG "verbose = $verbose_level\n" ;

    close NEWCFG ;
}

generate_config $self->{gencfg_file}
    if defined $self->{gencfg_file} ;

Lltag::Misc::exit_readline () ;
