#!/usr/bin/env perl
#
# Copyright © 2019-2021 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 Getopt::Long qw(:config gnu_getopt);

use FindBin;
use lib "$FindBin::Bin";
use MPDHacks;

my $sock;

my $pl_current_length;

# Returns a hash reference mapping filenames to an array reference listing
# the queue IDs for that file in the current play queue.
sub get_tracks_in_play_queue {
	my (%matches, %idmap, $entry);
	my $pos = -1;

	MPD::exec("playlistinfo");
	while (<$sock>) {
		last if /^OK/;
		die($_) if /^ACK/;

		if (/^(\w+): (.*)$/) {
			if ($1 eq "file") {
				$entry = $matches{$2} //= [];
				$pos++;
			} elsif ($1 eq "Id") {
				push @$entry, $2;
				$idmap{$2} = $pos;
			}
		}
	}

	$pl_current_length = $pos+1;

	return (\%matches, \%idmap);
}

# 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) = (@_, *STDERR);

	print $fh "Usage: $0 [options] playlist\n";
	print $fh "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' => \$MPD::host,
	'port|p=s' => \$MPD::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
};

$sock = MPD::connect();

# Retrieve the current play queue and target play queue.
MPD::run("tagtypes", "clear");
my ($current, $idmap) = get_tracks_in_play_queue();
my $target = get_playlist_files($ARGV[0]);

my $add_start;

sub load_tracks($$) {
	my ($seq, $dst) = @_;
	my ($newlen, $count);

	my $start = $add_start // $seq;
	my $add_position = $pl_current_length;
	my $end = $seq+1;

	$dst //= $start;

	MPD::exec("load", $ARGV[0], "$start:$end");
	MPD::exec("status");
	MPD::exec("command_list_end");

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

		if (/^(\w+): (.*)$/) {
			if ($1 eq "playlistlength") {
				$newlen = int($2);
			}
		}
	}

	$count = $newlen - $pl_current_length;

	MPD::exec("command_list_begin");
	if ($newlen > $pl_current_length) {
		MPD::exec("move", "$add_position:$newlen", $dst)
			if ($add_position != $dst);
	}

	$pl_current_length = $newlen;
	undef $add_start;

	return $count;
}

MPD::exec("command_list_begin");
my ($num_added, $num_failed) = (0, 0);
for (my $i = 0; $i < @$target; $i++) {
	my $f = $target->[$i];
	my $id = shift @{ $current->{$f} };

	if (defined $id and defined $add_start) {
		my $n = $i - $add_start;
		my $m = load_tracks($i-1, $add_start - $num_failed);

		$num_added += $m;
		$num_failed += $n - $m;
	}

	if (defined $id) {
		# Try not to move tracks already in the right place.
		MPD::exec("moveid", $id, $i - $num_failed)
			if ($i - $num_failed != $idmap->{$id} + $num_added);
	} else {
		$add_start //= $i;
	}
}

# Now all unwanted tracks from the original playqueue have been moved to the
# end and can be deleted all at once.

my $pos = ($add_start // @$target) - $num_failed;
MPD::exec("delete", $pos . ":") if map { @$_ } values %$current;
MPD::exec("load", $ARGV[0], "$add_start:") if defined $add_start;
MPD::run("command_list_end");
