#!/usr/bin/perl -w

use strict;

my $PREFIX = "/usr/local";	# For config files etc
my %types;			# Hash (by type) of arrays of viewers

#
# Read the configuration file(s) for genericview
#
sub read_config() {
	local *CONFIG;		# Filehandle for reading config file(s)
	my $foundconfig = 0;	# Did we find any config file?
	my $continuing = 0;	# Are we processing a line continuation?
	my $errors = "";	# Errors during parsing

	foreach my $filename ("$ENV{HOME}/.genericviewrc",
		"$PREFIX/etc/genericview.conf",
		"$PREFIX/etc/defaults/genericview.conf") {
		my $lastline;		# Last line read, after continuation
					# processing
		if (open(CONFIG, "<$filename")) {
			$foundconfig = 1;
			line: while (<CONFIG>) {
				my $types;	# File types this match
				my @viewers;	# Which viewers we have
				chomp;
				# Drop pure comments
				next if /^\s*#/;
				#
				# Process line continuations
				#
				if ($continuing) {
					$lastline .= $_;
				} else {
					$lastline = $_;
				}
				if ($lastline =~ /\\\s*$/) {
					$continuing = 1;
					next;
				} else {
					$continuing = 0;
				}
				# Drop empty lines
				next if $lastline =~ /^$/;
				# Process normal line
				($lastline =~ s/^([a-zA-Z0-9_.\s]+):\s*//) or
					$errors .= ":$.:$filename: Cannot parse type\n";
				$types = $1;
				# Strip whitespace at the start and end
				$types =~ s/^\s*(.*?)\s*$/$1/;

				my $viewer = undef;
				# Loop until we only have whitespace or a comment
				while ($lastline !~ /^\s*$/ && $lastline !~ /^\s*#/) {
					if ($lastline =~ s/^-page\s*//) {
						die ":$.:$filename: -page on its own\n" unless $viewer;
						die ":$.:$filename: -page for viewer that already has -xterm\n" if
							$viewer->{xterm};
						$viewer->{page} = 1;
					} elsif ($lastline =~ s/^-xterm\s*//) {
						die ":$.:$filename: -xterm on its own\n" unless $viewer;
						die ":$.:$filename: -xterm for viewer that already has -page\n" if
							$viewer->{page};
						$viewer->{xterm} = 1;
					} elsif ($lastline =~ s/^-multi\s*//) {
						die ":$.:$filename: -multi on its own\n" unless $viewer;
						$viewer->{multi} = 1;
					} elsif ($lastline =~ s/^-requires\s+([^\s]+)\s*//) {
						$viewer->{requires} = [] if !$viewer->{requires};
						push @{$viewer->{requires}}, $1;
					} elsif ($lastline =~ s/^"((\\\\|\\"|[^"])+)"\s*// || 
					    $lastline =~ s/^((\\.|[^\s])+)\s*//) {
						$viewer = {};
						$viewer->{viewer} = $1;
						# Strip escapes
						$viewer->{viewer} =~ s/\\(.)/$1/g;
						push(@viewers, $viewer);
					} else {
						$errors .= ":$.:$filename: Cannot parse viewers\n";
						next line;
					}
				}
				foreach my $type (split(/\s+/, $types)) {
					# Skip this entry if we already have a
					# spec for that type.
					if (!exists($types{$type})) {
						$types{$type} = [];
						push(@{$types{$type}}, @viewers);
					}
				}

			}
			close(CONFIG) or
				$errors .= "genericview: Error reading $filename\n";
		}
	}
	$foundconfig or $errors = <<EOM;
Cannot find any configuration file for genericview.
Tried:
	$ENV{HOME}/.genericviewrc
	$PREFIX/etc/genericview.conf
	$PREFIX/etc/defaults/genericview.conf
EOM
	!$errors or die $errors;
}

#
# Fix up our environment to be reasonable for running helpers in a separate
# xterm.
# Specifically, modify the $LESS environment variable to not have any instant
# quit options.
#
sub fixenv() {
	# -E/--QUIT-AT-EOF makes less exit after a single EOF.
	# -F/--quit-if-one-screen makes less exit if the data fits in a single
	# screen.
	#
	# This is not OK, as that would make the command exit immediately in
	# many cases!
	#
	# -+ reset to default.
	#
	$ENV{LESS} .= "-+EF" if $ENV{LESS};
}

sub find_xterm() {
	return "xterm" if !$types{"xterm"};
viewer: foreach my $viewer (@{$types{"xterm"}}) {
		foreach my $viewerexe (@{$viewer->{requires}}) {
			if (system("which", "-s", $viewerexe) != 0) {
				next viewer if $? >= 256;	# which could not find the program
				die "genericview: Unable to start \"which $viewerexe\"\n" if $? == -1;
				if ($? & 128) {
					die "genericview: which dumped core from signal " . ($? & 127) . "\n";
				} else {
					die "genericview: which exited due to signal " . ($? & 127) . "\n";
				}
			}
		}
		return $viewer->{viewer};
	}
	return "xterm";
}
#
# Main program for genericview
#
MAIN: {
	my $type;			# What type of file is supposed to be shown?
	my $filename;			# What filename is supposed to be shown?
	my $page = 0;			# Suppress xterm+pager for programs that output to stdout, and make
					# genericview prefer programs that wants to be paged.
					# Intended for cases where the caller supplies its own (e.g, gentoo)
	my $canpage = 0;		# Indicate that the calling program will page output if there is any,
					# but gracefully tolerate there being no output.
	my $notfiles = 0;		# The parameters supplied are not files, and should not be prefixed to
					# from absolute paths.
	#
	# Use exceptions to be able to output errors to stderr or xterm in
	# *one* place.  Anything that die's inside this block will have the
	# die parameter printed to stderr (if a terminal) or an xterm brought
	# up for the purpose.
	#
	eval {
		while (@ARGV && $ARGV[0] =~ /^-/) {
			if ($ARGV[0] =~ /^-page/) {
				$page = 1;
			} elsif ($ARGV[0] =~ /^-canpage/) {
				$canpage = 1;
			} elsif ($ARGV[0] =~ /^-notfiles/) {
				$notfiles = 1;
			} else {
				die "genericview: Unknown option $ARGV[0]\n";
			}
			shift @ARGV;
		}
		@ARGV != 0 or die <<EOM;
genericview: Missing type parameter.
This is probably a result of misconfiguration of a program that
u\010us\010se\010e genericview.
EOM
		$type = shift @ARGV;
		@ARGV != 0 or die <<EOM;
genericview: Missing file parameter.
This is probably a result of misconfiguration of a program that
u\010us\010se\010e genericview.
EOM
		$filename = shift @ARGV;
		die "genericview: No such file \"$filename\"\n" unless -e $filename;
		read_config();
		die "genericview: No viewers for type \"$type\"\n" unless exists $types{$type};

		#
		# Sort prefers something that does output on stdout if we have
		# a program that expects to page output itself.
		#
		my @viewers;
		if ($page) {
			# Anything that want to output to a pager is preferred
			push(@viewers, map { ($_->{page} && !$_->{nopager}) ? $_ : () } @{$types{$type}});
			# But we are willing to use anything we can get
			push(@viewers, map { !($_->{page} && !$_->{nopager}) ? $_ : () } @{$types{$type}});
		} else {
			# Use the user's order
			@viewers = @{$types{$type}};
		}
		if (@ARGV) {
			# Only allow viewers that can handle multiple files if
			# we got multiple files
			@viewers = map { $_->{multi} ? $_ : () } @viewers;
			die "genericview: No viewers for type \"$type\" in multi-mode\n" unless @viewers;
		}
	viewer:	foreach my $viewer (@viewers) {
			if (!exists($viewer->{requires}))  {
				my $viewerexe;			# Executable used by the viewer
				# Skip methods that needs to run in an xterm window if
				# we have something that does output capture.
				$viewerexe = $viewer->{viewer};
				# Strip any initial space/parentheses (shell wrapping)
				# This is for cases like RPM that needs to run several
				# commands to provide decent output, and then be fed
				# into the pager.
				$viewerexe =~ s/^\s*\(\s*//g;
				# Strip any arguments
				$viewerexe =~ s/\s+.*//;
				$viewer->{requires} = [ $viewerexe ];
			}
			foreach my $viewerexe (@{$viewer->{requires}}) {
				if (system("which", "-s", $viewerexe) != 0) {
					next viewer if $? >= 256;	# which could not find the program
					die "genericview: Unable to start \"which $viewerexe\"\n" if $? == -1;
					if ($? & 128) {
						die "genericview: which dumped core from signal " . ($? & 127) . "\n";
					} else {
						die "genericview: which exited due to signal " . ($? & 127) . "\n";
					}
				}
			}
			# We found a viewer - now use it!
			$filename =~ s/([^A-Za-z0-9_\-\/.\+\@])/\\$1/g;
			my $pwd = `pwd` or
				die "genericview: Unable to find present working directory\n";
			# Make sure we pass around absolute paths
			$filename = "$pwd/$filename" unless $filename =~ m|^/| || $notfiles;
			chomp $pwd;
			if (@ARGV) {
				# Add the files from @ARGV if multiple files
				# were passed.
				foreach my $name (@ARGV) {
					$name =~ s/([^A-Za-z0-9_\-\/.\+\@])/\\$1/g;
					$name = "$pwd/$name" unless $name =~ m|^/| || $notfiles;
					$filename .= " $name";
				}
			}
			my $command = $viewer->{viewer};
			$command .= " $filename" unless $command =~ s/\${FILENAME}/$filename/g;
			if ($viewer->{page}) {
				fixenv();
				if ($page || $canpage && !$viewer->{nopager}) {
					# If possible, use the internal pager of the calling program
				} elsif (!$viewer->{nopager} && $ENV{X11PAGER}) {
					# Otherwise, use X11PAGER if possible
					$command = "($command) | $ENV{X11PAGER}";
				} else {
					# Otherwise, use less if possible
					$command = "($command) | \${PAGER:-less -c}" unless $viewer->{nopager};
					# Run it in a specially started xterm
					$command = find_xterm() . "-T 'genericviewer $type' -n $type " .
						"-e sh -c '$command'";
				}
			} else {
				print "View of \"$filename\" ($type) shown elsewhere\n" if $page;
			}
			exec($command);
			die "genericview: Unable to exec \"$command\"\n";
		}
		die "genericview: Found no installed viewers for \"$type\"\n" .
		    "Alternatives tried:\n\t" . join("\n\t", map { $_->{viewer} } @{$types{$type}}) . "\n";
	};
	if ($@) {
		if (-t STDERR) {
			print STDERR $@;
		} else {
			fixenv();
			if ($page) {
				print STDOUT "Error from genericview:\n$@\n";
			} else {
				system(find_xterm(), "-T", "Error from genericview", "-n", "Error",
					"-e", "/bin/sh", "-c", 
					"cat << EOM | less -c\n" . $@ . "EOM\n");
			}
		}
		exit 1;
	}
}
