#!/usr/bin/env perl
#
# Copyright © 2019 Nick Bowler
#
# Replace the current MPD play queue with a saved playlist, by rearranging
# existing queue entries when possible.  This avoids losing the current
# player state when the loaded playlist is similar to the current queue.
#
# License GPLv3+: GNU General Public License version 3 or any later version.
# This is free software: you are free to change and redistribute it.
# There is NO WARRANTY, to the extent permitted by law.

use strict;
use utf8;

use Encode::Locale qw(decode_argv);
decode_argv(Encode::FB_CROAK);

binmode(STDOUT, ":utf8");
use IO::Socket::INET6;
use IO::Socket::UNIX;

use Getopt::Long qw(:config gnu_getopt);

my $host = $ENV{MPD_HOST} // "localhost";
my $port = $ENV{MPD_PORT} // 6600;
my $sock;

# Quotes the argument so that it is presented as a single argument to MPD
# at the protocol level.
sub escape {
	my $s = @_[0] // $_;

	# No way to encode literal newlines in the protocol, so we
	# convert any newlines in the arguments into a space, which
	# can help with quoting.
	$s =~ s/\n/ /g;

	if (/[ \t\\"]/) {
		$s =~ s/[\\"]/\\$&/g;
		return "\"$s\"";
	}

	$s =~ s/^\s*$/"$&"/;
	return $s;
}

# Submit a command to the MPD server; each argument to this function
# is quoted and sent as a single argument to MPD.
sub mpd_exec {
	my $cmd = join(' ', map { escape } @_);

	print $sock "$cmd\n";
}

# Returns a hash reference containing all tracks in the current play queue.
# The hash keys are filenames.
sub get_tracks_in_play_queue {
	my %matches;
	my $entry;

	mpd_exec("playlistinfo");
	while (<$sock>) {
		last if /^OK/;
		die($_) if /^ACK/;
	
		if (/^(\w+): (.*)$/) {
			if ($1 eq "file") {
				if (exists($matches{$2})) {
					$entry = $matches{$2};
				} else {
					$entry = {};
					$matches{$2} = $entry;
				}
			}

			if (exists($entry->{$1})) {
				$entry->{$1}->{$2} = 1;
			} else {
				$entry->{$1} = { $2 => 1 }
			}
		}
	}

	return \%matches;
}

# Given an MPD playlist name, returns a reference to an array containing
# (in order) the files in the playlist.
sub get_playlist_files {
	my ($plname) = @_;
	my @files;

	mpd_exec("listplaylist", $plname);
	while (<$sock>) {
		last if /^OK/;
		die($_) if /^ACK/;

		if (/^(\w+): (.*)$/) {
			if ($1 eq "file") {
				push @files, $2;
			}
		}
	}

	return \@files;
}

sub print_version {
	print <<EOF
mpdreload.pl 0.8
Copyright © 2019 Nick Bowler
License GPLv3+: GNU General Public License version 3 or any later version.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
EOF
}

sub print_usage {
	my $fh = $_[1] // *STDERR;

	print $fh "Usage: $0 [options] playlist\n";
	print "Try $0 --help for more information.\n" unless (@_ > 0);
}

sub print_help {
	print_usage(*STDOUT);
	print <<EOF
This is "mpdreload": a tool to reload a stored playlist into the MPD queue.

Options:
  -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
  -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
  -V, --version     Print a version message and then exit.
  -H, --help        Print this message and then exit.

Report bugs to <nbowler\@draconx.ca>
EOF
}

GetOptions(
	'host|h=s' => \$host,
	'port|p=s' => \$port,

	'V|version' => sub { print_version(); exit },
	'H|help'    => sub { print_help(); exit },
) or do { print_usage(); exit 1};

if (@ARGV != 1) {
	print STDERR "Playlist name is required\n" unless @ARGV;
	print STDERR "Excess command-line arguments\n" if @ARGV;

	print_usage(); exit 1
};

# Connect to MPD.
if ($host =~ /^[@\/]/) {
	$host =~ s/^@/\0/;
	$sock = new IO::Socket::UNIX(Type => SOCK_STREAM(), Peer => $host);
} else {
	$sock = new IO::Socket::INET6(PeerAddr => $host,
	                              PeerPort => $port,
	                              Proto    => 'tcp');
}
$sock or die "failed to connect to MPD: $!";
binmode($sock, ":utf8");

if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
	die "MPD failed to announce version: $!";
}

# Retrieve the current play queue and target play queue.
my $current = get_tracks_in_play_queue();
my $target = get_playlist_files($ARGV[0]);

mpd_exec("command_list_begin");
for (my $i = 0; $i < @$target; $i++) {
	my $f = $target->[$i];
	my $ids = $current->{$f}->{Id};

	my $id = (keys %$ids)[0];
	delete $ids->{$id};

	# Remove tracks with no unused queue IDs
	delete $current->{$f} unless (keys %$ids > 0);

	if (defined $id) {
		mpd_exec("moveid", $id, $i);
	} else {
		mpd_exec("addid", $f, $i);
	}
}

# Remove any tracks left from the old play queue.
foreach (keys %$current) {
	my $ids = $current->{$_}->{Id};
	foreach (keys %$ids) {
		mpd_exec("deleteid", $_);
	}
}

mpd_exec("command_list_end");
while (<$sock>) {
	last if /^OK$/;
	die($_) if /^ACK/;
}
