#! /usr/bin/perl
# This file is public domain. Originally written 2010 by Gaydov Victor.

use strict;
use utf8;

use File::Basename;
use File::Spec::Functions qw(rel2abs);
use File::Find;
use Cache::File;
use IPC::Open2;
use Env qw(HOME MUSIC VIDEO);

sub usage {
	print <<USAGE and exit
mp [-] [PLAYER-OPTIONS] [PATH] [MODE] [- GREP-OPTIONS] [-- PLAYER-OPTIONS]
Play media and print information.

This script reads ~/.m/config and use it with given options to build
a pipe of m.* programs to find, filter, print or choose player and play media.

If first option is `-' and no PATH given, `mp' reads files from STDIN
instead of invoking `m.find'.

PLAYER-OPTIONS are passed to according player, for example `m.mpc'. If
options are listed between tracks (not before or after all tracks), they
take effect on the rest tracks, listed right to options. `--' token is also
passed if any. Common options are:

  -r   random
  -z   repeat
  -k   keep current mode
  -a   apend to playlist
  -n   setup options and exit, do not play

In `:i' mode, PLAYER-OPTIONS are passed to `m.info'.
See also documentation for `m.mpc' and `m.mplayer'.

PATH

  PATH can contain:

  * shortcuts to be resolved by m.find(1)
  * audio CDs in form of `cd<N>'
  * audio CD tracks, in form of `cd<N>/<N>', `cd<N>/<N>-<N>' or `cd<N>/-'
  * mounted CD tracks, in form of `cd<N>/<SHORTCUT>'
  * file names
  * nothing to use current song path

CDs:

  CD-ROMs are listed in configuration file and numbered from zero.
  Tracks are numbered from one. It's possible to refer concrete track,
  a diapasone or all tracks.

GREP-OPTIONS are passed to m.grep(1) to filter result.

MODEs:

  :v   use `video' player
  :o   use `other' player
  :p   print file list
  :i   print file info
  :l   print lyrics

  With no MODE `mp' tries to find audio, then video. Additional modes
  can be registered in configuration file. They are expanded to other
  options and modes in-place.

PLAYERS:

  By default, `mp' uses:

  * 'audio' player if media was found in \$MUSIC
  * 'video' player if media was found in \$VIDEO
  * 'cdrom' player if media is a list of CD tracks
  * 'other' player if media was found somewhere else
    (on mounted CD or files were specified explicitly)

EXAMPLES:

  # find files, filter and play
  \$ mp dee_pur - '<80'

  # find files in \$VIDEO
  \$ mp :v chase

  # print current song lyrics and save it
  \$ mp :l - l -s

  # play first track and then all tracks randomly
  \$ mp cd0/1 -z cd0/-

  # read file list form STDIN and play
  \$ m.ls jimi/axis/ | mp -
 
USAGE
}

if (@ARGV == 1) {
	if ($ARGV[0] =~ /-h|--help/) {
		usage
	}
	if ($ARGV[0] =~ /-v|--version/) {
		print <<VERSION and exit
mp 0.0 from m.utils(1) package
VERSION
	}
}

#---------------------------------------------------------------------------
# parse config
#---------------------------------------------------------------------------

our ($DEFAULT, $STATUS, $TRANSLIT,
	 @MEDIA, @CDROM, @DEVICE, @VARIANT, %PLAYER, %MODE);
do "$HOME/.m/config" or die "mp: can't load config: " . ($! || $@) . "\n";

exec $DEFAULT || exit unless @ARGV;

my (
	@opts,         # player wrapper options
	@list,         # list of player invokations
	@grep,         # m.grep command line
	@real,         # real player command line
	%seen,         # seen cdroms
	$mode,         # output mode
	$last_mode,    # last choosed mode
	$force_mode,   # forced output mode
);

my @T; # trasliteration options
@T = ("-t", $TRANSLIT) if $TRANSLIT;

my %M = (
	v => 'video',
	o => 'other',
	p => 'print',
	i => 'info',
	l => 'lyrics',
);

my %PLAIN = (
	'print'  => 1,
	'info'   => 1,
	'lyrics' => 1,
);

#---------------------------------------------------------------------------
# parse options
#---------------------------------------------------------------------------

if (@ARGV && $ARGV[0] eq '-') {
	shift @ARGV;
	push @list, { type => 'local', tracks => [ map { chomp; $_ } <STDIN> ] };
}

my ($prev, $in_options, $in_x_options);
while (local $_ = shift @ARGV) {

	my $ixo = $in_x_options;
	$in_x_options = ($prev eq $_) if /^--?$/;

	# grep option
	#
	if (/^--$/ .. /^-$/) {
		next if !$in_x_options++;
		push @real, $_;

	} elsif (/^-$/ .. ($ixo && /^--?$/)) {
		next if !$in_x_options++;
		push @grep, $_;

	} else {
		# player option
		#
		if (/^-/) {
			$last_mode = '';
			unless ($in_options++) {
				my @o;
				@o = @{$list[-1]{options}} if @list && $list[-1]{options};
				push @list, { options => [ @o, $_ ] };
				next;
			}
			push @{$list[-1]{options}}, $_;
			next;
		}
		$in_options = 0;

		# mode
		#
		if (/^:(.*)/) {
			# built-in mode
			#
			if (defined $M{lc $1}) {
				$mode = $M{lc $1};
				if ($force_mode && $force_mode ne $mode) {
					if (!$PLAIN{$mode}) {
						die "mp: can't use `$mode' mode:"
						  . "`$force_mode' should be used\n";
					}
				}
				$force_mode = $mode;

			# mode from %MODE in config file
			#
			} else {
				defined $MODE{$1} or die "mp: unknown mode: $1\n";
				unshift @ARGV, split ('\s+', $MODE{$1});
			}
			next;
		}

		# CD track
		#
		if (my ($cd_n, $c) = /^cd(\d+)(?:\/(.*))?/i) {
			my $cd;
			# mounted CD
			#
			if (opendir (my $unused, $CDROM[$cd_n])) {
				$cd = "other";

			# audio CD
			#
			} else {
				$cd = "cdrom";
			}

			if (!$PLAIN{$mode}) {
				if ($mode && $mode ne $cd) {
					push @list, {} if $last_mode;
					$mode = $force_mode = 'other';
				} else {
					$mode = $cd;
				}
				$last_mode++;
			}

			if (@list && %seen && !$seen{+$cd_n}++) {
				push @list, { options => [] };
			} elsif (@list
					&& (!$list[-1]{cdrom} || $list[-1]{cdrom} ne $cd_n)) {
				push @list, {};
			}
			@list = ({}) unless @list;

			$list[-1]{type} = $cd eq 'cdrom' ? 'cdrom' : 'short';
			$list[-1]{cdrom} = $cd_n;
			push @{$list[-1]{tracks}}, $c;

		# file name
		#
		} else {
			# file path or shortcut
			#
			my $m = -e $_ ? 'local' : '';
			if (!$PLAIN{$mode}) {
				if ($mode ne $m) {
					push @list, {} if $last_mode;
					$force_mode = 'other' if $mode;
					$mode = 'other';
				} else {
					$mode = $m;
				}
				$last_mode++;
			}
			@list = ({}) unless @list;

			$list[-1]{type} = $m || 'short';
			push @{$list[-1]{tracks}}, $_;
		}
	}
} continue {
	$prev = $_;
}

if (@list && !$list[-1]{type}) {
	@opts = @{$list[-1]{options}};
	pop @list;
}
unshift @real, "--" if @real && $real[0] ne "--" && $mode ne 'info';

#---------------------------------------------------------------------------
# parse input files
#---------------------------------------------------------------------------

if ($mode eq 'video') {
	push @opts, '-v';
}

my @play;
my $mdir;
my $plain = $PLAIN{$mode};

my $cache = Cache::File->new (cache_root => "$HOME/.m/cache")
	or die "mp: can't open cache: $!.\n";

# no files
#
if (!@list) {
	my $mode = $mode;
	# no files, have options: run player with options
	#
	if (@opts && $mode ne 'info') {
		$mode = 'music' if !$mode || $plain;

		0 == system ($PLAYER{$mode} || $PLAYER{other}, @opts, "-n")
			or die "mp: $opts[0]: $!\n";
		exit unless $plain;
	}

	# no files, no options, get current track
	#
	@play = ({
		options => [ !$plain ? $PLAYER{$mode} : () ],
		path =>    [ split /\n+/ => `$STATUS` ]
	});

# parse input files
#
} else {
for (@list) {
	my $mode = $mode;
	my (@path, @exec, @file);

	@file = @{$_->{tracks}} if $_->{tracks};

	# existing file, just get full name
	#
	if ($_->{type} eq 'local') {
		@path = map rel2abs($_), @file;

	# shortcut, pass to m.find
	#
	} elsif ($_->{type} eq 'short') {

		# choose directories to search
		#
		my @mdir = @MEDIA;
		@mdir = ($MUSIC, $VIDEO) unless @mdir;

		if (defined $_->{cdrom}) {
			my $n = $_->{cdrom};
			@mdir = map { "$CDROM[$n]/$_" } @VARIANT;
		} elsif ($mode eq "video") {
			@mdir = $VIDEO;
		}

		# try to find shortcut in all directories
		#
		for my $t (@{$_->{tracks}}) {
			for (@mdir) {
				open my $find, "-|", "m.find", "-d", $_, @T, $t
					or die "mp: m.find: $!\n";
				my @p;
				if (@p = map { chomp ; $_ } <$find>) {
					if (!$plain) {
						if (!$mode) {
							$mode = "video" if $_ eq $VIDEO;
							$mode = "music" if $_ eq $MUSIC;
						}
						if ($mdir && $mdir ne $_) {
							if ($force_mode && $force_mode ne 'other') {
							die "mp: can't use `$force_mode' mode"
							  . "because media is in different directories.\n"
							  . "(`other' mode should be used)\n";
							}
							$mode = $force_mode = 'other';
						}
					}
					$mdir = $_;
					push @path, @p;
					last;
				}
			}
		}

	# audio CD track
	#
	} elsif ($_->{type} eq 'cdrom') {
		if (!$plain) {
			@exec = ("-c", $DEVICE[$_->{cdrom}]);
			@path = @file;
		} else {
			warn "mp: warning: skipping cd$_->{cdrom}/.. in `$mode' mode.\n";
		}
	}

	if (!@path) {
		my $f = $file[0];
		$f = "cd$_->{cdrom}/$f" if defined $_->{cdrom};
		die "mp: no matches found: `$f'.\n";
	}

	# add player command
	#
	if (!$plain) {

		if (!@play || $_->{options}) {
			push @exec, $mdir ? ("-d", $mdir) : ();
			push @play, {
				player  => ($PLAYER{$mode} || $PLAYER{other}),
				options => [ $PLAYER{$mode} || $PLAYER{other}, @opts ],
				path => [],
			};
		}

		push @{$play[-1]{options}}, @{$_->{options}} if $_->{options};
		push @{$play[-1]{options}}, @exec;
		push @{$play[-1]{path}}, @path;

	# plain mode: only add resolved paths
	#
	} else {
		push @play, { path => [] } if !@play;
		push @{$play[-1]{path}}, @path;
	}
}}

=for debug

use Data::Dumper;

print "-- List of tracks to find --\n";
print Dumper (\@list);
print "\n";
print "-- List of commands to run --\n";
print Dumper (\@play);
print "\n";
print "-- Info --\n";
print "MODE = $mode, GREP-OPTIONS = ", join (" ", @grep), "\n";
print "PLAYER-OPTIONS = ", join (" ", @opts), " | ", join (" ", @real), "\n";
exit;

=cut

#---------------------------------------------------------------------------
# run commands
#---------------------------------------------------------------------------

my (@G, $grep);

# lyrics mode: pass files to m.grep
#
if ($mode eq 'lyrics') {
	unshift @grep, '-o', '%t (%ar)\n\n%l', 'ar';

# print mode: print files and exit
#
} elsif ($mode eq "print") {
	unless (@grep) {
		print $_, "\n" for @{$play[0]{path}};
		exit;
	}

# info mode: pass files to m.info
#
} elsif ($mode eq 'info') {
	$grep ++;
	@G = ("m.info", @opts, @real, "-", @T, @grep);
}

@G = ("m.grep", @T, @grep) unless @G;

# run all commands
#
for (@play) {
	my $key = join ("//", @{$_->{path}}, @grep);

	# pass to m.grep if necessary
	#
	if (@grep || $grep) {
		my ($v, $ok) = (undef, 1);

		if (!$plain && $cache->exists($key)) {

			$v = $cache->thaw ($key);
			my $time = $v->{time};
			my $s = sub {
				my $t = (stat)[9];
				if ($t > $time) {
					$ok = 0;
					last FILE;
				}
			};

			FILE: for my $f (@{$_->{path}}) {
				if (!-d $f) {
					$_ = $f;
					$s->();
				} else {
					find ({
						wanted => $s,
						no_chdir => 1,
						follow_fast => 1
					}, $f);
				}
			}
		}

		if ($v && $ok) {
			$_->{path} = $v->{path};

		} else {
			my ($in, $out);

			unless ($plain) {
				open2 ($in, $out, @G) or die "mp: m.grep: $!\n";
			} else {
				open ($out, "|-", @G) or die "mp: m.grep: $!\n";
			}

			# do not fork in plain mode: m.grep will print results and exit
			#
			if ($plain or 0 == fork) {
				close $in if $in;
				print $out $_, "\n" for @{$_->{path}};
				exit;
			}

			# get filtered files
			#
			close $out;
			@{$_->{path}} = ();
			while (my $l = <$in>) {
				chomp $l;
				push @{$_->{path}}, $l;
			}

			$cache->freeze ($key, {
				'time' => time,
				'path' => $_->{path}
			});
		}
	}

	# pass (possibly filtered) files to player
	#
	if ($_->{path} && @{$_->{path}}) {

		open my $pipe, "|-", @{$_->{options}}, @real
			or die "mp: $_->{options}->[0]: $!\n";

		print $pipe $_, "\n" for @{$_->{path}};
	}
}

