#!/usr/bin/perl
# The Virtual Songbook 0.0
# (c) 2003 Martin Mares <mj@ucw.cz>

use Curses;
use strict;
use warnings;

### Interface with Curses ###

my $W;
my $color_mode;

sub init_terminal() {
	$W = new Curses;
	start_color;
	$color_mode = (has_colors && COLORS >= 8 && COLOR_PAIRS >= 8) unless defined $color_mode;
	cbreak; noecho;
	$W->intrflush(0);
	$W->keypad(1);
	$W->meta(1);
}

sub cleanup_terminal() {
	endwin;
}

my ($attr_normal, $attr_status, $attr_hilite, $attr_chord);

sub setup_attrs() {
	if ($color_mode) {
		init_pair(1, COLOR_YELLOW, COLOR_BLUE);
		$attr_status = COLOR_PAIR(1) | A_BOLD;
		init_pair(2, COLOR_YELLOW, COLOR_BLACK);
		$attr_chord = COLOR_PAIR(2);
	} else {
		$attr_status = A_BOLD;
		$attr_chord = A_BOLD;
	}
	$attr_normal = A_NORMAL;
	$attr_hilite = A_BOLD;
}

my $try_full_names = 1;
my $auto_enter = 1;
my $file_window_width = 20;

my ($term_w, $term_h);
my @window_list = ();
my $status_window = new VS::Window::Status;
my $main_window = new VS::Window::Main;
my $file_window = new VS::Window::File;
$file_window->reload;
my $focused_window_i = 2;
my $focused_window = $file_window;

sub focus_next() {
	$focused_window->{"focused"} = 0;
	do {
		$focused_window_i++;
		if ($focused_window_i > $#window_list) { $focused_window_i=0; }
		$focused_window = $window_list[$focused_window_i];
	} while (!$focused_window->{"focusable"} || !$focused_window->{"visible"});
	$focused_window->{"focused"} = 1;
}

sub recalc_windows() {
	my $w = COLS;
	my $h = LINES;
	$term_w = $w;
	$term_h = $h;
	$status_window->place(0, 0, 1, $w);
	if ($file_window->{"visible"}) {
		my $fww = $file_window_width;
		$main_window->place(1, 0, $h-1, $w-$fww-1);
		$W->attrset($focused_window == $file_window ? $attr_hilite : $attr_normal);
		$W->vline(2, $w-$fww-1, ACS_VLINE, $h-3);
		$W->hline(1, $w-$fww, ACS_HLINE, $fww);
		$W->hline($h-1, $w-$fww, ACS_HLINE, $fww);
		$W->addch(1, $w-$fww-1, ACS_ULCORNER);
		$W->addch($h-1, $w-$fww-1, ACS_LLCORNER);
		$W->attrset($attr_normal);
		$file_window->place(2, $w-$fww, $h-3, $fww);
	} else {
		$main_window->place(1, 0, $h-1, $w);
		$file_window->place(0, 0, 0, 0);
	}
}

sub toggle_window($) {
	my $win = shift;
	if ($win->{"visible"} = !$win->{"visible"}) {
		while ($focused_window != $win) { focus_next; }
	} else {
		if ($focused_window == $win) { focus_next; }
	}
	recalc_windows;
}

init_terminal;
setup_attrs;
recalc_windows;

for(;;) {
	$W->move($term_h-1, $term_w-1);
	$W->refresh;
	my $key = $W->getch;
	if ($key eq "\033" || $key eq "q") {
		cleanup_terminal;
		exit 0;
	} elsif ($key eq "f") {
		toggle_window($file_window);
	} elsif ($key eq "\t") {
		focus_next;
		recalc_windows;
	} elsif ($key eq "\014") {
		$curscr->clearok(1);
	} elsif ($key eq "<" && $file_window_width < $term_w-1) {
		$file_window_width++;
		recalc_windows;
	} elsif ($key eq ">" && $file_window_width > 1) {
		$file_window_width--;
		recalc_windows;
	} elsif ($key eq "j") {
		$file_window->key(KEY_DOWN);
	} elsif ($key eq "k") {
		$file_window->key(KEY_UP);
	} elsif ($key eq "h") {
		$file_window->key(KEY_LEFT);
	} elsif ($key eq "l") {
		$file_window->key(KEY_RIGHT);
	} else {
		$focused_window->key($key);
	}
}

### Window Objects ###

package VS::Window;

sub new($) {
	my $w = {
		"visible" => 1,
		"focusable" => 1,
		"focused" => 0,
		"x" => -1,
		"y" => -1,
		"w" => -1,
		"h" => -1
	};
	push @window_list, $w;
	return bless $w;
}

sub place($$$$$) {
	my ($w,$nx,$ny,$nh,$nw) = @_;
	if ($w->{"visible"}) {
		if (!defined $w->{"win"} || $w->{"x"} != $nx || $w->{"y"} != $ny
		 || $w->{"w"} != $nw || $w->{"h"} != $nh) {
			$w->{"win"} = $W->subwin($nh,$nw,$nx,$ny);
			$w->{"x"} = $nx;
			$w->{"y"} = $ny;
			$w->{"w"} = $nw;
			$w->{"h"} = $nh;
			$w->redraw;
		}
	} else {
		delete $w->{"win"};
	}
}

sub key($) { }
sub redraw($) { }

package VS::Window::Main;
use Curses;
BEGIN { our @ISA = qw(VS::Window); }

sub new($) {
	my $w = new VS::Window;
	$w->{"file"} = "";
	$w->{"attrs"} = {};
	$w->{"lines"} = ["", "", "   The Virtual Songbook 0.0\n", "   (c) 2003 Martin Mares <mj\@ucw.cz>"];
	$w->{"linetype"} = [0,0,0,0];
	$w->{"n"} = 4;
	$w->{"top"} = 0;
	return bless $w;
}

sub view($$$) {
	my ($w,$f,$x) = @_;
	if ($w->{"file"} ne $f) {
		$w->{"file"} = $f;
		$w->{"xfile"} = $x;
		$f =~ s@^./@@;
		$x =~ s@^./@@;
		if (open X, $f) {
			my %attrs = ();
			while (<X>) {
				chomp;
				/^$/ && last;
				if (/^(\w+):\s*(.*)/ && !defined $attrs{$1}) {
					$attrs{$1} = $2;
				}
			}
			my @lines = ();
			my @types = ();
			while (<X>) {
				chomp;
				if (s/^!//) { push @types, 1; } else { push @types, 0; }
				push @lines, $_;
			}
			close X;
			$w->{"attrs"} = \%attrs;
			$w->{"lines"} = \@lines;
			$w->{"linetypes"} = \@types;
			$w->{"top"} = 0;
			$w->{"n"} = scalar @lines;
			$w->redraw;
			if (defined $attrs{"Name"}) {
				$x = $attrs{"Name"};
				$x = $attrs{"Author"} . ": $x" if defined $attrs{"Author"};
			}
			$status_window->tell($x);
		} else {
			$status_window->tell("Cannot open $f");
		}
	}
}

sub redraw_line($$) {
	my ($w,$i) = @_;
	my $win = $w->{"win"};
	my $l = $w->{"lines"}->[$i];
	if ($w->{"linetypes"}->[$i]) { $win->attrset($attr_chord); }
	if (length $l < $w->{"w"}) {
		$win->addstr($i-$w->{"top"}, 0, $l);
		$win->clrtoeol;
	} else {
		$win->addstr($i-$w->{"top"}, 0, substr($l, 0, $w->{"w"}));
	}
	$win->attrset($attr_normal);
}

sub redraw($) {
	my $w = shift @_;
	my $win = $w->{"win"};
	my $top = $w->{"top"};
	my $cnt = $w->{"n"} - $w->{"top"};
	if ($cnt > $w->{"h"}) { $cnt = $w->{"h"}; }
	for (my $i=$top; $i<$top+$cnt; $i++) { $w->redraw_line($i); }
	if ($cnt < $w->{"h"}) {
		$win->move($cnt, 0);
		$win->clrtobot;
	}
	$win->noutrefresh;
}

sub go($$) {
	my ($w,$delta) = @_;
	my $win = $w->{"win"};
	my $top = $w->{"top"} + $delta;
	if ($top + $w->{"h"} > $w->{"n"}) { $top = $w->{"n"} - $w->{"h"}; }
	if ($top < 0) { $top = 0; }
	my $otop = $w->{"top"};
	$w->{"top"} = $top;
	if ($top < $otop - $w->{"h"}/2) {
		$w->redraw;
	} elsif ($top < $otop) {
		my $j = $otop - $top;
		$win->scrollok(1);
		$win->scrl(-$j);
		$win->scrollok(0);
		for (my $i=0; $i<$j; $i++) { $w->redraw_line($top+$i); }
	} elsif ($top == $otop) {
		# Nothing happens
	} elsif ($top < $otop + $w->{"h"}/2) {
		my $j = $top - $otop;
		$win->scrollok(1);
		$win->scrl($j);
		$win->scrollok(0);
		for (my $i=$j; $i>0; $i--) { $w->redraw_line($top+$w->{"h"}-$i); }
	} else {
		$w->redraw;
	}
	$win->noutrefresh;
}

sub key($$) {
	my ($w,$key) = @_;
	if ($key eq KEY_UP) { $w->go(-1); }
	elsif ($key eq KEY_DOWN) { $w->go(1); }
	elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
	elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
	elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
	elsif ($key eq KEY_END) { $w->go(1000000000); }
	else { $status_window->tell("Unknown key <$key>"); }
}

package VS::Window::File;
use Curses;
BEGIN { our @ISA = qw(VS::Window); }

sub new($) {
	my $w = new VS::Window;
	$w->{"dir"} = "./";
	$w->{"xdir"} = "./";
	return bless $w;
}

sub reload($) {
	my $w = shift;
	my $p = $w->{"dir"};
	my @l = `cd $p && ls`;
	my @fn = ();
	my @full = ();
	if ($p ne "./") { push @fn, "../"; push @full, "<parent>"; }
	foreach my $x (@l) {
		chomp $x;
		if (-f "$p/$x") {
			push @fn, $x;
			my $fullname = $x;
			if ($try_full_names && open(X, "$p/$x")) {
				while (<X>) {
					chomp;
					/^$/ && last;
					if (/^Name:\s*(.*)/) {
						$fullname = $1;
						last;
					}
				}
				close X;
			}
			push @full, $fullname;
		} elsif (-d "$p/$x") { push @fn, "$x/"; push @full, "$x/"; }
	}
	$w->{"flist"} = \@fn;
	$w->{"list"} = \@full;
	$w->{"n"} = scalar @fn;
	$w->{"i"} = 0;
	$w->{"1st"} = 0;
}

sub redraw_line($$) {
	my ($w,$i) = @_;
	my $line = $i - $w->{"1st"};
	if ($line < 0 || $line >= $w->{"h"}) { return; }
	my $win = $w->{"win"};
	my $item = ($i < $w->{"n"}) ? substr($w->{"list"}->[$i], 0, $w->{"w"}) : "";
	if ($i == $w->{"i"}) { $win->bkgdset($attr_status); }
	$win->addstr($line, 0, $item);
	$win->clrtoeol if length $item < $w->{"w"};
	$win->bkgdset($attr_normal);
}

sub redraw($) {
	my $w = shift @_;
	my $win = $w->{"win"};
	# Window size might have changed...
	if ($w->{"1st"} + $w->{"h"} > $w->{"n"}) { $w->{"1st"} = $w->{"n"} - $w->{"h"}; }
	if ($w->{"1st"} < 0) { $w->{"1st"} = 0; }
	$win->idlok(1);
	for (my $i=0; $i<$w->{"h"}; $i++) {
		$w->redraw_line($w->{"1st"} + $i);
	}
	$win->noutrefresh;
}

sub go($$) {
	my ($w,$delta) = @_;
	my $i = $w->{"i"};
	my $oldi = $i;
	$i += $delta;
	if ($i < 0) { $i = 0; }
	if ($i >= $w->{"n"}) { $i = $w->{"n"}-1; }
	$w->{"i"} = $i;
	if ($w->{"visible"}) {
		$w->redraw_line($oldi);
		if ($i < $w->{"1st"}) {
			my $j = $w->{"1st"} - $i;
			$w->{"1st"} = $i;
			if ($j >= $w->{"h"}/2) {
				$w->{"win"}->scrollok(1);
				$w->{"win"}->scrl(-$j);
				$w->{"win"}->scrollok(0);
				for (my $k=0; $k<$j; $k++) { $w->redraw_line($i+$k); }
			} else { $w->redraw; }
		} elsif ($i >= $w->{"1st"} + $w->{"h"}) {
			my $j = $i - $w->{"1st"} - $w->{"h"} + 1;
			$w->{"1st"} += $j;
			if ($j < $w->{"h"}/2) {
				$w->{"win"}->scrollok(1);
				$w->{"win"}->scrl($j);
				$w->{"win"}->scrollok(0);
				for (my $k=1; $k<=$j; $k++) { $w->redraw_line($i-$j+$k); }
			} else { $w->redraw; }
		} else { $w->redraw_line($i); }
		$w->{"win"}->noutrefresh;
	}
	if ($auto_enter && $i < $w->{"n"} && $w->{"flist"}->[$i] !~ /\/$/) { $w->select; }
}

sub select($) {
	my ($w) = @_;
	if ($w->{"i"} < $w->{"n"}) {
		my $f = $w->{"flist"}->[$w->{"i"}];
		my $x = $w->{"list"}->[$w->{"i"}];
		if ($f =~ /\/$/) {
			if ($f eq "../") {
				$w->{"dir"} =~ s@([^/]*/)$@@;
				my $back = $1;
				$w->{"xdir"} =~ s@[^/]*/$@@;
				$w->reload;
				for (my $i=0; $i<$w->{"n"}; $i++) {
					if ($w->{"flist"}->[$i] eq $back) {
						$w->{"i"} = $i;
						$w->{"1st"} = $i - int($w->{"h"}/2);
						last;
					}
				}
			} else {
				$w->{"dir"} .= $f;
				$w->{"xdir"} .= $x;
				$w->reload;
			}
			$w->redraw;
		} else {
			$main_window->view($w->{"dir"} . $f, $w->{"xdir"} . $x);
		}
	}
}

sub key($$) {
	my ($w,$key) = @_;
	if ($key eq KEY_UP) { $w->go(-1); }
	elsif ($key eq KEY_DOWN) { $w->go(1); }
	elsif ($key eq KEY_PPAGE) { $w->go(-$w->{"h"}-1); }
	elsif ($key eq KEY_NPAGE) { $w->go($w->{"h"}-1); }
	elsif ($key eq KEY_HOME) { $w->go(-1000000000); }
	elsif ($key eq KEY_END) { $w->go(1000000000); }
	elsif ($key eq "\n" || $key eq "\r" || $key eq KEY_RIGHT) { $w->select; }
	elsif ($key eq KEY_LEFT) {
		if ($w->{"list"}->[0] eq "<parent>") {
			$w->{"i"} = 0;
			$w->select;
		}
	}
}

package VS::Window::Status;
BEGIN { our @ISA = qw(VS::Window); }

sub new($) {
	my $w = new VS::Window;
	$w->{"focusable"} = 0;
	$w->{"msg"} = "";
	return bless $w;
}

sub redraw($) {
	my $w = shift @_;
	my $win = $w->{"win"};
	$win->bkgdset($attr_status);
	$win->addstr(0, 0, $w->{"msg"});
	$win->clrtoeol;
	$win->refresh;
}

sub tell($$) {
	my ($w,$m) = @_;
	if ($w->{"msg"} ne $m) {
		$w->{"msg"} = $m;
		$w->redraw;
	}
}
