#!/usr/bin/perl -CSAL
use warnings;
use strict;
use utf8;
use locale;

use Carp;
use MP3::Tag;
use Getopt::Long;
use File::Temp;
use Image::Magick;

MP3::Tag->config(write_v24 => 1);

sub hashprint(%);
sub sync_tags($);
sub setsource(@);
sub copy_tag($$);
sub list_genres();


our $formatstring = "%a - %t%{c: - %c}.mp3";
our $printformatstring;
our $dorename  = 0;
our $doprint   = 0;
our $docopy    = 0;
our $zerosep   = 0;
our $verbose   = 0;
our $dosync    = 0;
our $preferid3v= 0;
our $usename   = 0;
our $onlyid3   = 0;
our $overwrite = 0;
our $eol       = "\n";
our %new_tags  = ();
our $have_new_tags = 0;
our $extractImage;
our $extractImageFormatstring = '%N - %{pict}.%{pice}';
our %new_images;

Getopt::Long::Configure(qw(bundling));
GetOptions(
	"f|formatstring=s" => \$formatstring,
	"F|print-formatstring=s" => \$printformatstring,
		# optinally a separate print string for printing.
		# implies -p
	"z|zero"           => sub { $eol = "\0" },
	"Z|Zero"           => sub { $eol = "" },

	"p|print"          => \$doprint,
		#default if not -r || -s
	"r|rename"         => \$dorename,
	"O|overwrite"      => \$overwrite,
		# while renaming
	"s|sync"           => sub {$dosync += 1},
		# use twice to force updating from empty id3 tags
		# As a side effect, prohibits reading all values from the non-prefered tag
	"v|verbose"        => \$verbose,
		# some debugging output

	"1|prefer-id3v1"   => sub {$preferid3v=1},
	"2|prefer-id3v2"   => sub {$preferid3v=2},
	"prefer-id3=i"     => \$preferid3v,
	"o"                => \$onlyid3,
		# use only specified version of id3 tag (default: 2)
		# will also inhibit creating/modifying the other tag if syncing from filename
	"only-id3=i"       => sub {my $i = shift; $preferid3v=$1; $onlyid3=1; },
		# alias --prefer-id3= -o
	"N|usename"        => sub {$usename++},
		# use twice to prefer, three times to force
	"h|help"           => \&help,
	"c|copy"           => \$docopy,
	"t|set-tag=s"      => \%new_tags,
	"G|list-genres:s"  => \&list_genres,
	"i|insert-image=s" => \%new_images,
	"x|extract-image"  => \$extractImage,
	"X|extract-image-formatstring"
	                   => \$extractImageFormatstring,
) || exit(1);

my %tags = (
	't' => 'title',
	'a' => 'artist',
	'l' => 'album',
	'n' => 'track',
	'c' => 'comment',
	'g' => 'genre',
	'y' => 'year',
);

for my $k (keys %new_tags) {
	$have_new_tags |= ($new_tags{$k} eq '')? 2 : 1;
	if (length($k) == 1) {
		if (!defined $tags{$k}) {
			die "unknown tag $k";
		}
		$new_tags{$tags{$k}} = $new_tags{$k};
		delete $new_tags{$k};
	} elsif (!grep $k, values %tags) {
		die "unknown tag $k";
	}
}

{
	my $APIC = MP3::Tag::ID3v2::APIC(1,1);
	my $IM = Image::Magick->new if %new_images;
	for my $i (keys %new_images) {
		my $j;
		my $fi = $new_images{$i};
		delete $new_images{$i};
		if ($i =~ /^\d+$/) {
			$j = $i;
			$i = MP3::Tag::ID3v2::APIC(chr($i));;
		} elsif (defined ($j = $APIC->{$i})) {
			/* $j is set */
		} else {
			die "unknown picture type $i";
		}
		my $ffi;
		open($ffi, '<', $fi) || die "open($fi): $!";
		$IM->Read(file=>\$ffi);
		close($ffi);
		my $I = $IM->[0];
		my %F;
		$F{magick} = "/" . $IM->MagickToMime($I->Get('magick'));
		$F{Data} = $I->ImageToBlob();
		$F{i} = chr($j);
		$new_images{$i} = \%F;
		@$IM=();
}	}

#print ((map "$_ -> '$new_tags{$_}', ", keys %new_tags),"\n");

if (!$dorename && !$dosync && !$docopy && !$have_new_tags && !$extractImage && !%new_images
 || $printformatstring)
	{ $doprint = 1; }

if ($preferid3v > 2 || $preferid3v < 0) {
	die "invalid value for --preferid3";
}
$preferid3v = $preferid3v || 2;

{
	my @id3;
	if ($onlyid3 || $dosync > 1) {
		@id3 = ($preferid3v == 1)? ("ID3v1") : ("ID3v2");
	} else {
		@id3 = ($preferid3v == 1)? ("ID3v1", "ID3v2") : ("ID3v2", "ID3v1");
	}

	if ($usename > 2) {
		setsource("filename");
	} elsif ($usename > 1) {
		setsource("filename", @id3);
	} elsif ($usename) {
		setsource(@id3, "filename");
	} else {
		setsource(@id3);
	}
}

for ($formatstring, $printformatstring) {
	if (!defined $_)
		{ next; }
	if ($_ eq 's') {
		$_ = "%a - %t";
	} elsif ($_ eq 'l') {
		$_ = <<EOF;
File   : >%N%E<
Artist : >%a<
Title  : >%t<%{l:
Album  : >%l<}%{n:
Track  : >%n<}%{c:
Comment: >%c<}%{y:
Year   : >%y<}%{g:
Genre  : >%g<}
EOF
#Times  : >%A<\t>%B<
#Speed  : >%z< [%Z]
	} elsif ($_ eq 'L') {
		$_ = <<EOF;
File   : >%N%E<
Artist : >%a<
Title  : >%t<
Album  : >%l<
Track  : >%n<
Comment: >%c<
Year   : >%y<
Genre  : >%g<
EOF
#Times  : >%A<\t>%B<
#Speed  : >%z< [%Z]
}	}

# main()
if ($docopy) {
	if ($dosync) {
		die "Can't copy and sync";
	}
	if ($dorename) {
		die "Can't copy and rename";
	}
	if (2 != @ARGV) {
		die "copy fromfile tofile";
	}
	exit copy_tag($ARGV[0], $ARGV[1]);
}

for my $f (@ARGV) {
	my $tag = MP3::Tag->new($f);
	if (!$tag) {
		print STDERR "$f: $!\n";
		next;
	}
	$tag->get_tags;
	if (%new_tags) {
		if (defined $new_tags{comment} && defined (my $id3v2 = $tag->{ID3v2})) {
			my @comments = grep(/^COMM/, keys %{$id3v2->get_frame_ids()});
			for my $comment (@comments)
				{$id3v2->remove_frame($comment)};
		}
		$tag->update_tags(\%new_tags);
	}
	my $s = $tag->interpolate($formatstring);
	if ($dosync) {
		sync_tags($tag);
	} elsif (%new_tags) {
		$tag->{ID3v2}->write_tag if $tag->{ID3v2} && (!$onlyid3 || $preferid3v==2);
        $tag->{ID3v1}->write_tag if $tag->{ID3v1} && (!$onlyid3 || $preferid3v==1);
	}
	if ($doprint) {
		if (defined $printformatstring) {
			print $tag->interpolate($printformatstring), $eol;
		} else {
			print $s, $eol;
		}
	}
	$tag->close() unless ($extractImage || %new_images) && !$dorename;
	if ($dorename) {
		my $S = $s;
		$S =~ s.\s*/\s*., .;
		next if $f eq $s;
		if (!$overwrite && -e $S) {
			$S = File::Temp::tempnam( "./", $s."." );
		}
		print "mv \"\Q$f\E\" \"\Q$S\E\"\n" if $verbose;
		if (rename($f, $S)) {
			$f = $S;
		} else {
			print STDERR "rename(\"\Q$f\E\", \"\Q$S\E\": $!\n";
			next;
		}
	}
	if (defined $extractImage || %new_images) {
		$tag = MP3::Tag->new($f) unless !$dorename;
		if (!$tag) {
			print STDERR "$f: $! (after renaming)\n";
			next;
		}
	}
	if (defined $extractImage && defined $tag->{ID3v2}) {
		my ($info, $name, @frames) = $tag->{ID3v2}->get_frame('APIC');
		unshift(@frames, $info);

		for my $frame (@frames) {
			die if (ref $frame ne "HASH");
			if (exists $frame->{_Data}
			&&  exists $frame->{"Picture Type"}
			&& exists $frame->{"MIME type"}) {
				my $type = $frame->{"MIME type"};
				$type =~ s,.*/,,; $type =~ s,%,%%,g;
				my $ptype = $frame->{"Picture Type"};
				$ptype =~ s,/.*,,; $ptype =~ s,%,%%,g;
				my $jf = $extractImageFormatstring;
				$jf =~ s/%{pict}/$ptype/g;
				$jf =~ s/%{pice}/$type/g;
				$jf = $tag->interpolate($jf);

				if (open(F, '>',$jf)) {
					print "extracting $jf\n" if $verbose;
					syswrite(F, $frame->{_Data});
					close(F);
				} else {
					print STDERR "open($jf): $!\n";
					next;
				}
			} else {
				die; # should never happen?
			}
		}

	}
	for my $i (keys %new_images) {
		my %F;
		%F = %{$new_images{$i}};
		$tag->new_tag("ID3v2") if !defined $tag->{ID3v2};
		my $id3v2 = $tag->{ID3v2};
		my @APICS = grep(/^APIC/, keys %{$id3v2->get_frame_ids()});
		for my $APIC (@APICS) {
			my ($info, $name) = $id3v2->get_frame($APIC);
			if ($info->{"Picture Type"} eq $i) {
				$id3v2->remove_frame($APIC);
			}
		}
		$id3v2->add_frame("APIC", 0, $F{magick}, $F{i}, "", $F{Data});
	}
	$tag->{ID3v2}->write_tag if %new_images;
	$tag->close() if $extractImage || %new_images;
}

#######################################################

sub setsource(@) {
	for my $a (qw(autoinfo title artist album year comment track genre)) {
		MP3::Tag->config($a, @_);
	}
}

sub sync_tags($) {
	my $tag = shift;
	my $s_preferid3v = ($preferid3v == 1)? "ID3v1" : "ID3v2";

	if ($usename > 2) {
		# overwrite with data from filename
		if ($onlyid3) {
			# only overwrite specified tag, leave alone the other
			$tag->new_tag($s_preferid3v);
		} else {
			# default: overwrite both tags
			$tag->new_tag("ID3v1");
			$tag->new_tag("ID3v2");
		}
	} elsif ($dosync > 1) {
		# overwrite with data from other tag
		my $s_killid3v = ($preferid3v == 1)? "ID3v2" : "ID3v1";
		$tag->new_tag($s_killid3v);
	} elsif ($usename && $onlyid3) {
		# only create/modify prefered tag
		if (!exists $tag->{$s_preferid3v}) {
			$tag->new_tag($s_preferid3v);
		}
	# otherwise, check if we need to create tags.
	# if there are no tags, we may still get new data from the filename
	} elsif (exists $tag->{ID3v1}) {
		if (!exists $tag->{ID3v2}) {
			# sync from existing v2 to non-existing v1
			$tag->new_tag("ID3v2");
		}
		# otherwise, both tags do exist
	} else {
		if (!exists $tag->{ID3v2}) {
			if ($usename) {
				# no tag exists, but we get data from the filename
				# -> create both tags
				$tag->new_tag("ID3v1");
				$tag->new_tag("ID3v2");
			} else {
				# no data to sync, no data from the filename, we are done
				return 0;
			}
		}
		$tag->new_tag("ID3v1");
	}

	$tag->update_tags(\%{$tag->autoinfo()}, 1);
	$tag->update_tags(\%new_tags, 1);

	$tag->{ID3v2}->write_tag if $tag->{ID3v2};
	$tag->{ID3v1}->write_tag if $tag->{ID3v1};
	return 1;
}

sub copy_tag($$) {
	my ($src, $dst) = @_;
	my $taga = MP3::Tag->new($src) || die "Could not open $src: $!";
	my $tagb = MP3::Tag->new($dst) || die "Could not open $dst: $!";

	if (my $old_tags = $tagb->autoinfo()) {
		# delete old tags
		$tagb->new_tag("ID3v1");
		$tagb->new_tag("ID3v2");
	}
	$tagb->update_tags(scalar $taga->autoinfo());
	$tagb->update_tags(\%new_tags) if %new_tags;

	if ($doprint) {
		if (defined $printformatstring) {
			print $tagb->interpolate($printformatstring), $eol;
		} else {
			print $tagb->interpolate($formatstring), $eol;
		}
	}
	
	$tagb->{ID3v2}->write_tag if $tagb->{ID3v2} && (!$onlyid3 || $preferid3v==2);
	$tagb->{ID3v1}->write_tag if $tagb->{ID3v1} && (!$onlyid3 || $preferid3v==1);
	$tagb->close();
	$taga->close();
	return 0;
}

sub isset($) {
	my $val = shift;
	return 0 if !defined $val;
	return ($val =~ /\S/);
}

sub hashprint(%) {
	my %x = @_;
	return join(', ', map("\"$_\" => $x{$_}", keys %x));
}

sub list_genres() {
	my $mode=$_[1];
	my $sort = 0;
	if (($mode =~ s/s//) == 1) {
		$sort = 1;
	}
	my @genres = @{MP3::Tag->genres()};
	if (!$sort) {
		if ("$mode" eq "l") {
			for (my $i=0; $i < @genres; $i++) {
				printf "%3i %s%s", $i, $genres[$i], $eol;
			}
		} elsif ("$mode" eq "n") {
			for (my $i=0; $i < @genres; $i++) {
				print $genres[$i], $eol;
			}
		} elsif ("$mode" eq "t" || $mode eq "") {
			my $l = 0;
			for (my $i=0; $i < @genres; $i++) {
				if ((my $L=length($genres[$i])) > $l) {
					$l = $L;
			}	}
			my $n = int( (79+2) / ($l + 6) );
			my $i = 0;
			do {
				for (my $t = 0; $t < $n; $t++, $i++) {
					printf "%s%3i %-$l.$l"."s",
						   ($t?"  ":""), $i, $genres[$i];
				}
				print $eol;
			} while $i < @genres;
		} else {
			die "Listing mode $mode is not supported\n";
		}
	} else {
		my %genres;
		for (my $i=0; $i < @genres; $i++) {
			$genres{$genres[$i]} = $i;
		}
		@genres = sort keys %genres;
		if ("$mode" eq "l") {
			for my $g (@genres) {
				printf "%3i %s%s", $genres{$g}, $g, $eol;
			}
		} elsif ("$mode" eq "n") {
			print join($eol, @genres), $eol;
		} elsif ("$mode" eq "t" || $mode eq "") {
			my $l = 0;
			for (my $i=0; $i < @genres; $i++) {
				if ((my $L=length($genres[$i])) > $l) {
					$l = $L;
			}	}
			my $n = int( (79+2) / ($l + 6) );
			do {
				for (my $t = 0; $t < $n; $t++, shift(@genres)) {
					printf "%s%3i %-$l.$l"."s",
						   ($t?"  ":""), $genres{$genres[0]}, $genres[0];
				}
				print $eol;
			} while @genres;
		} else {
			die "Listing mode $mode is not supported\n";
		}
	}
}

sub help() {
	print <<EOF;
1) Printing information about an mp3 file (-p, --print)

  Default: "%a - %t%{c: - %c}.mp3" (with an EOL appended)

Change using:
 -f: Format string for all operations (like rename)
 --formatstring
 -F: Format string for printing only, forces printing
 --print-formatstring
Shorthands:
-fs
  equals -f"%a - %t"
-fl
  long info, with unset fields suppressed
-fL
  long info, all fields I like

Options for printing:
 -p, --print
   do print (default if no other operation (-r or -s)
 -z, --zero
   set eol string to "\\0"
 -Z, --Zero
   set eol string to "" (no printing of an extra EOL


2) Renaming (-r, --rename)

  Renames files using the format string

Options:

 -O, --overwrite
   If the destination file exists, id3 will usually append a random string.
   With -O, it will overwrite the files instead.
 -v, --verbose
   give some debug output.

3) Syncing id3v1 and id3v2 tags (-s, --sync)

Copy the values from the prefered tag into all tags.
If specified once, values will be searched in all tags.
If specified twice, only the prefered tag will be used.
(See below about using the filename)

4) Tag-Copying (-c, --copy)

Copy the tag data from the first file to the second file.
This is not a binary copy, but a semantic copy. Thre tags
will be parsed, combined and re-encoded.

Copying cannot be combined with syncing or renaming.

5) Editing tags (-t tagname=value, --set-tag tagname=value)

Sets the named tag to value.
Supported tags are:
	't' or 'title',
	'a' or 'artist',
	'l' or 'album',
	'n' or 'track',
	'c' or 'comment',
	'g' or 'genre',
	'y' or 'year',

Setting a tag to '' (empty string) will delete this tag.
Updating tags will be done before using them for printing or syncing.

6) Listing the possible genres for ID3v1 (-G mode)

Supported modes are:
	't': Tabular list (default)
	'l': Long list, one entry per line
	'n': Names only, one per line
	's': Sort the list by name


Selecting the information source:
  -1, --prefer-id3v1
    use id3v1 tag first
  -2, --prefer-id3v2 (default)
    use id3v2 tag first
  --prefer-id3={1|2}
    use specified tag first
  -o
    only use prefered tag
    Do not create/modify the non-prefered tag when syncing from the filename
  --only-id3={1|2}
	alias --prefer-id3= -o
  -N, --usename
    once: use the name of the file as a source
    twice: prefer the information from the name to the tag data
    three times: use only the name

Examples:
 id3 -ta=artist_name *.mp3
    sets the artist name
 id3 -s -1 -N *.mp3
    syncs id3v2 to the values of id3v1, and adds information from the filename
 id3 -r *.mp3 -f"%n - %t"
    renames all mp3s to track_nr - title
 id3 -r -Fl -s -N -tl="album_name" -r -ta="artist_name" *.mp3
    Takes the information from the id3 tags and the name,
    sets the album name and the artist name, updates the tag,
    prints the long information and renames the files to "%a - %t"
    (e.g. "42 - title.mp3" is renamed to "artist_name - title.mp3"
     and has 42 stored as it\'s track number)

EOF
}
