#!/usr/bin/env perl
#
# Copyright (c) 2022 Omar Polo <op@omarpolo.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use strict;
use warnings;
use v5.12;

use open ":std", ":encoding(UTF-8)";
use utf8;

use Curses;
use POSIX qw(:sys_wait_h setlocale LC_ALL);
use Text::CharWidth qw(mbswidth);
use IO::Poll qw(POLLIN);
use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
use Getopt::Long qw(:config bundling);
use Pod::Usage;

my $run = 1;

my $pfile;
my $trim = "";

my $pair_n = 1;

my @songs;
my $current_song;
my $playlist_cur;
my $playlist_max;
my $time_cur;
my $time_dur;
my $status;
my $mode;

my $last_lines;

sub round {
	return int(0.5 + shift);
}

sub max {
	my ($a, $b) = @_;
	return $a > $b ? $a : $b;
}

sub excerpt {
	my $lines = shift;
	my @tmp;
	my ($n, $idx, $cur) = (0, 0, -1);

	open (my $fh, "-|", "amused", "show", "-p");
	while (<$fh>) {
		chomp;
		s,$trim,,;
		$tmp[$idx] = $_;

		if (m/^>/) {
			$cur = $n;
			$current_song = s/^> //r;
		}

		$n++;
		$idx = ++$idx % $lines;

		last if $cur != -1 && $n - $cur > int($lines/2) &&
		    $#tmp == $lines-1;
	}
	close($fh);

	return ("Empty playlist.") unless @tmp;

	# reorder the entries
	my @r;
	my $len = $#tmp + 1;
	$idx = $idx % $len;
	for (1..$len) {
		push @r, $tmp[$idx];
		$idx = ++$idx % $len;
	}
	return @r;
}

sub playlist_numbers {
	my ($cur, $tot, $found) = (0, 0, 0);
	open (my $fh, "-|", "amused", "show", "-p");
	while (<$fh>) {
		$tot++;
		$cur++ unless $found;
		$found = 1 if m/^>/;
	}
	close($fh);
	return ($cur, $tot);
}

sub status {
	my ($pos, $dur, $mode);

	open (my $fh, "-|", "amused", "status", "-f",
	      "status,time:raw,mode:oneline");

	<$fh> =~ m/([a-z]+) (.*)/;
	my ($status, $current_song) = ($1, $2);

	while (<$fh>) {
		chomp;
		$pos = s/position //r if m/^position /;
		$dur = s/duration //r if m/^duration /;
		$mode = $_ if m/^repeat/;
	}
	close($fh);
	return ($status, $current_song, $pos, $dur, $mode);
}

sub showtime {
	my $seconds = shift;
	my $str = "";

	if ($seconds > 3600) {
		my $hours = int($seconds / 3600);
		$seconds -= $hours * 3600;
		$str = sprintf("%02d:", $hours);
	}

	my $minutes = int($seconds / 60);
	$seconds -= $minutes * 60;
	$str .= sprintf "%02d:%02d", $minutes, $seconds;
	return $str;
}

sub center {
	my ($str, $pstr) = @_;
	my $width = mbswidth($str);
	return $str if $width > $COLS;
	my $pre = round(($COLS - $width) / 2);
	my $lpad = $pstr x $pre;
	my $rpad = $pstr x ($COLS - $width - $pre);
	return ($lpad, $str, $rpad);
}

sub offsets {
	my ($y, $x, $cur, $max) = @_;
	my ($pre, $c, $post) = center(" $cur / $max ", '-');
	addstring $y, $x, "";

	my $p = COLOR_PAIR($pair_n);

	attron $p;
	addstring $pre;
	attroff $p;

	addstring $c;

	attron $p;
	addstring $post;
	attroff $p;
}

sub progress {
	my ($y, $x, $pos, $dur) = @_;

	my $pstr = showtime $pos;
	my $dstr = showtime $dur;

	my $len = $COLS - length($pstr) - length($dstr) - 4;
	return if $len <= 0 or $dur <= 0;
	my $filled = round($pos * $len / $dur);

	addstring $y, $x, "$pstr [";
	addstring "#" x $filled;
	addstring " " x max($len - $filled, 0);
	addstring "] $dstr";
}

sub show_status {
	my ($y, $x, $status) = @_;
	my ($pre, $c, $post) = center($status, ' ');
	addstring $y, $x, $pre;
	addstring $c;
	addstring $post;
}

sub show_mode {
	my ($y, $x, $mode) = @_;
	my ($pre, $c, $post) = center($mode, ' ');
	addstring $y, $x, $pre;
	addstring $c;
	addstring $post;
}

sub render {
	erase;
	if ($LINES < 4 || $COLS < 20) {
		addstring "window too small";
		refresh;
		return;
	}

	my $song_pad = "";
	my $longest = 0;
	$longest = max $longest, length($_) foreach @songs;
	if ($longest < $COLS) {
		$song_pad = " " x (($COLS - $longest)/2);
	}

	my $line = 0;
	map {
		attron(A_BOLD) if m/^>/;
		addstring $line++, 0, $song_pad . $_;
		standend;
	} @songs;

	offsets $LINES - 4, 0, $playlist_cur, $playlist_max;
	progress $LINES - 3, 0, $time_cur, $time_dur;
	show_status $LINES - 2, 0, "$status $current_song";
	show_mode $LINES - 1, 0, $mode;

	refresh;
}

sub getsongs {
	$last_lines = $LINES;
	@songs = excerpt $LINES - 4;
}

sub getnums {
	($playlist_cur, $playlist_max) = playlist_numbers;
}

sub save {
	return unless defined $pfile;

	open(my $fh, ">", $pfile);
	open(my $ph, "-|", "amused", "show", "-p");

	print $fh $_ while (<$ph>);
}

sub hevent {
	my $fh = shift;
	my $l = <$fh>;
	die "monitor quit" unless defined($l);

	$status = "playing" if $l =~ m/^play/;
	$status = "paused"  if $l =~ m/^pause/;
	$status = "stopped" if $l =~ m/^stop/;

	($time_cur, $time_dur) = ($1, $2) if $l =~ m/^seek (\d+) (\d+)/;

	$mode = $1 if $l =~ m/^mode (.*)/;

	getnums if $l =~ m/load|jump|next|previous/;
	getsongs if $l =~ m/load|jump|next|previous/;
}

sub hinput {
	my ($ch, $key) = getchar;
	if (defined $key) {
		if ($key == KEY_BACKSPACE) {
			system "amused", "seek", "0";
		}
	} elsif (defined $ch) {
		if ($ch eq " ") {
			system "amused", "toggle";
		} elsif ($ch eq "<" or $ch eq "p") {
			system "amused", "previous";
		} elsif ($ch eq ">" or $ch eq "n") {
			system "amused", "next";
		} elsif ($ch eq ",") {
			system "amused", "seek", "-5";
		} elsif ($ch eq ".") {
			system "amused", "seek", "+5";
		} elsif ($ch eq "S") {
			system "amused show | sort -u | amused load";
		} elsif ($ch eq "R") {
			system "amused show | sort -R | amused load";
		} elsif ($ch eq "s") {
			save;
		} elsif ($ch eq "q") {
			$run = 0;
		} elsif ($ch eq "\cH") {
			system "amused", "seek", "0"
		}
	}
}

GetOptions(
	"p:s" => \$pfile,
	"t:s" => \$trim,
    ) or pod2usage(1);

my $mpid = open(my $monitor, "-|", "amused", "monitor")
    or die "can't spawn amused monitor";

setlocale(LC_ALL, "");
initscr;
start_color;
use_default_colors;
init_pair $pair_n, 250, -1;

timeout 1000;
scrollok 0;
curs_set 0;
keypad 1;

my $poll = IO::Poll->new();
$poll->mask(\*STDIN => POLLIN);
$poll->mask($monitor => POLLIN);

if (`uname` =~ "OpenBSD") {
	use OpenBSD::Pledge;
	use OpenBSD::Unveil;

	my $prog = `which amused`;
	chomp $prog;

	unveil($prog, 'rx') or die "unveil $prog: $!";
	if (defined($pfile)) {
		unveil($pfile, 'wc') or die "unveil $pfile: $!";
		pledge qw(stdio wpath cpath tty proc exec) or die "pledge: $!";
	} else {
		pledge qw(stdio tty proc exec) or die "pledge: $!";
	}
}

getsongs;
getnums;
($status, $current_song, $time_cur, $time_dur, $mode) = status;
render;

while ($run) {
	$poll->poll();
	hinput if $poll->events(\*STDIN) & POLLIN;
	hevent $monitor if $poll->events($monitor) & POLLIN;

	getsongs if $LINES != $last_lines;

	render;
}

endwin;
save;

kill 'INT', $mpid;
wait;

__END__

=pod

=head1 NAME

amused-monitor - curses interface for amused(1)

=head1 SYNOPSIS

B<amused-monitor> [B<-p> I<playlist>] [B<-t> I<string>]

=head1 DESCRIPTION

amused-monitor is a simple curses interface for amused(1).

The following options are available:

=over 12

=item B<-p> I<playlist>

Save the current playling queue to the file I<playlist> upon exit or
I<s> key.

=item B<-t> I<string>

Trim out the given I<string> from every song in the playlist view.

=back

The following key-bindings are available:

=over 8

=item backspace or C-h

Seek back to the beginning of the track.

=item space

Toggle play/pause.

=item < or p

Play previous song.

=item > or n

Play next song.

=item ,

Seek backward by five seconds.

=item .

Seek forward by five seconds.

=item R

Randomize the playlist.

=item S

Sort the playlist.

=item s

Save the status to the file given with the B<-p> flag.

=item q

Quit.

=back

=head1 SEE ALSO

amused(1)

=cut
