#!/usr/bin/perl -w

# Written by Thomas Orgis in 2008.
# Licensed under GPLv2, or, heck, v3 if you please.
# Or Artistic License, even BSD-style. Just use it! ;-)

use strict;

# Finding Param library in any UNIX-ish dir tree plus in same dir.
# A bit overly eager...
use FindBin qw($Bin);
use lib "$Bin";
use lib "$Bin/../lib";
use lib "$Bin/../lib/perl";

my $version = "1.0.0";
# Read that string;-)
my $nedit_client = 'ncl'; # You call it nc, neditc ...
my $infostring = 'A wrapper shell for editing UTF files with nedit.
The basic function is to convert the files from UTF-8 to some 8bit encoding and edit that 8bit version with nedit, converting back to UTF when done.
This wrapper features a little text shell for managing a list of files but also supports a serial editing mode without shell control (to replace ncl -wait).';

# Parameter defaults and description.
my @pars =
(
	 'editor', $nedit_client, 'e', 'editor command, typically '.$nedit_client.' (but maybe with a svrname parameter), should not block'
	,'locale', 'LANG=en_US.ISO8859-1', 'l', 'var settings, going into the line to launch editor (bourne shell...)'
	,'sum','md5sum','s','the checksum command to determine file change'
	,'backfilter', 'iconv -f ISO_8859-1 -t UTF-8', 'b', 'filter back to external encoding'
	,'filter',     'iconv -f UTF-8 -t ISO_8859-1', 'f', 'filter from external encoding to editor encoding'
	,'run', 'run', 'r', 'the command to run on the "r" (run) action'
	,'serial', 0, 'S', 'serial mode, goes once over given list of files and exits'
	,'serial-editor', $nedit_client.' -wait', 'E', 'editor command that blocks for serial mode'
);

my $p = {};

# Optionally load Param library and parse command line.
if(eval { require Param })
{
	$p = Param::Parse({'info' => $infostring, 'version' => $version},\@pars);
}
else
{
	print STDERR "!! WARNING: Param library not found; -p/--parameter processing not available !!\nContinuing with defaults.\n\n";
	for(my $i = 0; $i <= $#pars; $i += 4){ $p->{$pars[$i]} = $pars[$i+1]; }
}

my $sum = $p->{sum};
my $editor="$p->{locale} ".($p->{serial} ? $p->{'serial-editor'} : $p->{editor});
my @files;
my $rl = undef;

# ReadLine only optional here for file completion.
# I do the action key stuff myself via stty, a UNIX way.
if(not $p->{serial} and eval { require Term::ReadLine; })
{
	print STDERR "Have readline!\n";
	$rl = new Term::ReadLine 'unedit';
	print STDERR "It's ".$rl->ReadLine()."\n";
}

keymode();

foreach my $a (@ARGV)
{
	add_file($a);
}

print "Initialized, awaiting your orders (type 'h' for help).\n";
list_all() if @files;

while( not $p->{serial} and my $com=getc )
{
	if($com eq "q"){ last; }
	elsif($com eq "w"){ write_back_all(0); }
	elsif($com eq "f"){ write_back_all(1); }
	elsif($com eq "r")
	{
		run_all();
	}
	elsif($com eq "h")
	{
		print "Commands:
	w:  write back if changed
	f:  force write back
	e:  (re)start editor(s) $p->{editor}
	r:  write (if changed) and run $p->{run}
	l:  list open files
	o:  open a new file
	c:  close a file (select from list)
	s:  start a subshell (using $ENV{SHELL})... for whatnot
	q:  write (if changed) and quit
";
	}
	elsif($com eq 'e')
	{
		print "starting editor(s)\n";
		run_editor_all();
	}
	elsif($com eq 'l')
	{
		list_all();
	}
	elsif($com eq 'o')
	{
		add_file();
	}
	elsif($com eq 'c')
	{
		close_file();
	}
	elsif($com eq 's')
	{
		run_shell();
	}
}

close_all();

linemode();

# --------------------------------
# The library.

sub run_shell
{
	print "Entering subshell...\n";
	linemode();
	system($ENV{SHELL});
	keymode();
	print "*Bling* -- back in unedit.\n";
}

# working on file list

sub add_file
{
	my $name = shift;
	unless(defined $name)
	{
		$name = get_a_line('file: ');
	}
	if(not defined $name or $name eq '')
	{
		print STDERR "Refusing to open/create a file with empty name!\n";
		return;
	}
	my $spec = create($name);
	if(defined $spec)
	{
		push(@files, $spec);
	}
}

sub close_file
{
	print "Choose file to close:\n";
	list_all();
	my $file = get_a_line('filename/index: ');
	my $i = -1;
	if($file =~ /^\d+$/)
	{ # use as direct index number
		$i = $file;
	}
	else
	{ # search for the file name.
		print "searching for $file...\n";
		for($i=0; $i<=$#files; ++$i)
		{
			print "> $files[$i]{file}\n";
			last if($files[$i]{file} eq $file);
		} 
	}
	if($i < 0 or $i > $#files)
	{
		print STDERR "Invalid file selection!\n";
		return;
	}
	my $removed = splice @files, $i, 1;
	finish($removed);
}

sub close_all
{
	print "Closing all files.\n";
	foreach my $s (@files)
	{
		finish($s);
	}
	@files = ();
}

sub write_back_all
{
	print "Triggering write back on all files.\n";
	my $force = shift;
	foreach my $s (@files)
	{
		write_back($s, $force);
	}
}

sub run_all
{
	print "Running $p->{run} on all files.\n";
	foreach my $s (@files)
	{
		write_back($s);
		linemode();
		system("$p->{run} '$s->{file}'");
		keymode();
	}
}

sub run_editor_all
{
	print "(Re)starting editor on all files.\n";
	foreach my $s (@files)
	{
		run_editor($s);
	}
}

sub list_all
{
	print "Files (count ".($#files+1)."):\n";
	for(my $i=0; $i<=$#files; ++$i)
	{
		print "\t$i: $files[$i]{file}\n";
	}
}

# working on single file

sub create
{
	my %spec;
	$spec{file} = shift;
	my $begin = $spec{file};
	my $end   = "";
	my $convert = 1;
	if($spec{file} =~ /^(.*)(\.[^.]+)$/)
	{
		$begin = $1;
		$end = $2;
	}
	$spec{tmpfile}="$begin.latin1$end";
	print "$spec{file}  <->  $spec{tmpfile}\n";

	if(-e $spec{tmpfile})
	{
		print "$spec{tmpfile} exists already ... what to do?\n";
		while(1)
		{
			print "[e]dit this one [o]verwrite it, [a]bort?\n";
			my $com=getc;
			if($com eq 'a')
			{
				return undef;
			}
			elsif($com eq 'e')
			{
				$convert = 0;
			}
			elsif($com ne 'o')
			{
				next;
			}
			last;
		}
	}
	unless(-e $spec{file})
	{
		print "$spec{file} does not exist yet, create it? (y/n)\n";
		while(1)
		{
			my $com=getc;
			if($com eq 'y')
			{
				unless(open(DAT,">$spec{file}"))
				{
					print STDERR "Failed to create $spec{file} ($!)!\n";
					return undef;
				}
				close(DAT);
			}
			elsif($com eq 'n'){ print "OK... doing nothing.\n"; return undef; }
			else{ next; }

			last;
		}
	}
	if($convert)
	{
		if(system("$p->{filter} < '$spec{file}' > '$spec{tmpfile}'"))
		{
			unlink($spec{tmpfile});
			print STDERR "Failed to convert $spec{file} ($!)\n";
			return undef;
		}
	}
	$spec{hash} = `$p->{sum} "$spec{tmpfile}"`;
	chomp($spec{hash});
	if($spec{hash} eq '')
	{
		print STDERR "Cannot hash $spec{tmpfile} ($!)!\n";
		unlink($spec{tmpfile});
		return undef;
	}
	unless($convert)
	{ # In case we work with existing tmpfile, make sure we have the current state.
		print "Syncing main file $spec{file}.\n";
		write_back(\%spec, 1);
	}
	print "initial hash: $spec{hash}\n";
	unless(run_editor(\%spec))
	{
		print STDERR "Cannot run editor ($!)!\n";
		unlink($spec{tmpfile});
		return undef;
	}
	return \%spec;
}

sub run_editor
{
	my $spec = shift;
	return not system("$editor '$spec->{tmpfile}'")
}

sub write_back
{
	my $spec = shift;
	my $force = shift;
	my $new_hash =`$sum '$spec->{tmpfile}'`;
	chomp($new_hash);
	if($new_hash ne $spec->{hash} or $force)
	{
		print "$spec->{file}: changed or force applied, write back.\n";
		$spec->{hash} = $new_hash;
		system("$p->{backfilter} < '$spec->{tmpfile}' > '$spec->{file}'") and print STDERR "Oh, trouble converting!\n";
	}
	else{ print "$spec->{file}: no change\n"; }
}

sub finish
{
	my $spec = shift;
	return unless defined $spec;
	write_back($spec);
	unlink($spec->{tmpfile});
}

# terminal control

sub get_a_line
{
	my $name = '';
	my $prompt = shift;
	$prompt = 'line: ' unless defined $prompt;
	linemode();
	if(defined $rl)
	{
		$name = $rl->readline($prompt);
		$name =~ s/(\S)\s+$/$1/; # readline likes to append space?
	}
	else
	{
		print STDERR "$prompt";
		$name = <STDIN>;
		chomp($name);
	}
	keymode();
	return $name;
}

# let's see if it works...
#system "stty cbreak < /dev/tty > /dev/tty 2>&1 && stty -echo";
sub keymode
{
	system("stty -echo");
	system "stty", '-icanon', 'eol', "\001";
}

sub linemode
{
	#system "stty -cbreak < /dev/tty > /dev/tty 2>&1 && stty echo";
	system "stty", 'icanon', 'eol', '^@'; # ASCII null
	system("stty echo");
}

