#!/usr/bin/perl

my $version = '21 (20.10.2013)';

# A script to run the latex processor just enough times to also get cross-references and TOC right.
# Copyright (c) 2005-2013 Thomas Orgis <thomas@orgis.org>
# This is Open Source, Distribution and Modification under the terms of the Artistic License

#latex results (normal file):
#first run:
#LaTeX Warning: Reference ... on page ... undefined ...
#LaTeX Warning: Citation ... on page ... undefined ...
#LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.
#second run:
#LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.
#final run:no warning
#errors:
#./notizen.tex:2136: Undefined control sequence

#on every successful run:
#Output written on ...

#it is ok for the first run to have undefined references
#it is ok when the second run says "rerun to get cross-references right"
#it is not ok when there are any warings in run 3
#errors are never ok!

use Config::Param;

my $p = Config::Param::get
(
	{
	  'info', 'Run me one time to run latex many times (for correct refs & Co.).' 	, 'version', $version
	},
	[
		 'latex', $0 =~ m@(^|/)pdflatexer$@ ? 'pdflatex' : 'latex' ,'l','the latex command to run'
		,'context',4,'c', 'context lines to print after errors/warnings'
		,'underfull',0,'u', 'show "Underfull..." warnings'
		,'overfull',1,'o', 'show "Overfull..." warnings'
		,'verbose',0,'v','show all latex compiler output messages'
		,'local',0,'L','work in the directory of the main latex file'
		,'cleanup',0,'C','remove the files from the latex run on failure'
		,'limit',0,'m','limit number of latex runs (if > 0)'
		,'fixer',[],'X','a list of commands that take the document name without ending; executed when there are warnings about undefined references (idea: specify bibtex, makeglossaries and friends here (perhaps you want to limit number of runs)'
	]
);

my $file = $ARGV[0];

die "Hey - some document to work on!\n"
	unless defined $file;

if($p->{local})
{
	require File::Basename;
	my $dir = File::Basename::dirname($file);
	$file = File::Basename::basename($file);
	print "=== Entering directory: $dir, file is now $file ===\n";
	chdir($dir) or die "Cannot chdir()! ($!)\n";
}
my $base = $file;
$base =~ s:\.[Tt][Ee][Xx]$::;

my $exitval = 0;
my $shown = 0;
my $ignoreunder = $p->{underfull} ? 0 : 1;
my $ignoreover  = $p->{overfull}  ? 0 : 1;

my $res;
my $run = 0;
do
{
	# check for changes in .toc file
	# it is possible to have changes there without latex telling you to rerun because cross-references are not affected - but table of contents is!
	my $tocfile = $base.'.toc';
	my $tochash = file_hash($tocfile);
	print "=== Run ".++$run." with $file:\n";
	if($res->{undefrefs} and @{$p->{fixer}})
	{
		print "=== Got undefined references and fixers available --- fix, run.\n";
		for my $fixer (@{$p->{fixer}})
		{
			system($fixer, $base) and die "Error running $fixer! ($!)\n";
		}
	}
	$res = OneRun($file);
	if(not ($res->{rerun} or $res->{errors}))
	{
		if(file_hash($tocfile) ne $tochash)
		{
			print "=== Looks like your table of contents changed --- rerun.\n";
			$res->{rerun} = 1;
		}
		$res->{rerun} = 1
			if($res->{undefrefs} and @{$p->{fixer}});
	}
	print "=== Bottom line: ".($res->{success} ? '' : 'no')." success with $res->{errors} errors, $res->{undefrefs} undefined references and $res->{warnings} other warnings; rerun: ".($res->{rerun} ? 'yes' : 'no')."\n";
	print "=== I won't run again because there are errors: Please fix these first!\n" if ($res->{rerun} and $res->{errors});
}
while($run != $p->{limit} and $res->{success} and $res->{rerun} and not $res->{errors});

if($res->{overs} or $res->{unders})
{
	print "=== There were $res->{overs} overfull and $res->{unders} underfull boxes!\n";
}

++$exitval if $res->{errors};
$exitval += 2 unless $res->{success};

if($p->{cleanup} and not $res->{success})
{
	print "=== Got errors, cleaning up... ===\n";
	my $base = $file;
	$base =~ s:\.tex$::;
	# This list is getting ridiculous .. what about just removing everything not *.tex? What about main.bak or such?
	my @suffixes = qw(toc aux log out glg acn acr alg bbl bcf blg glo gls ist run.xml xdy wrk);
	push(@suffixes, $p->{latex} =~ 'pdf' ? 'pdf' : 'dvi');
	foreach my $suffix (@suffixes)
	{
		my $file = $base.'.'.$suffix;
		if(-f $file)
		{
			print "$file\n";
			unlink($file);
		}
	}
}

exit($exitval);

sub OneRun
{
	my $file = shift;
	my $fileb = $file;
	$fileb =~ s/\.[tT][eE][xX]$//;
	my %result = ('rerun',0,'errors',0,'warnings',0,'undefrefs',0,'success',0);
	$result{overs}  = 0; # Count of overfull  boxes.
	$result{unders} = 0; # Count of underfull boxes.

	local(*LATTE);
	if(open(LATTE, "$p->{latex} -file-line-error-style -interaction=nonstopmode ".quotemeta($file)." |"))
	{
		my $fatal = 0;
		my $show = 0; # number of lines to show as context of something previous
		my $cfile = $file;
		while(<LATTE>)
		{
			# get the currently active file
			# man, that's so inconsistent
			if(/\(([^\(]+\.tex)\s*\[\d+\]\s*$/ or /^(.+\.tex):\d+:/){ $cfile = $1 if -e $1; }
			$shown = 0;
			if($show or $p->{verbose})
			{
				print "\t";
				show_line(\$_);
				--$show unless $p->{verbose};
			}
			if($fatal
			   or /^\S+:\s*(LaTeX Error:|Extra|Missing|Undefined control sequence)/
			   or /^!/ #used to look our for specific message: (File ended while|LaTeX Error:)/
			   or /^Runaway argument\?$/
			   or /:\d+: (I can't find file|.*Error:|Misplaced)/
			   or /:\d+:  ==> Fatal/)
			{
				show_line(\$_);
				# does this work anytime?
				if(/^(<\*>|l\.\d+)\s/ or /^Enter file name:/) #nedit's perl highlighting screws up here
				{
					++$result{errors};
					print "=== Closing after this error in $cfile...\n";
					last;
					#$fatal = 0;
				}
				else{ $fatal = 1; }
			}
			if($_ =~ /^.*$cfile:\d+:/)
			{
				$show = $p->{context} unless $fatal;
				++$result{errors};
			}
			# No file diplom.aux.
			# No file diplom.toc.
			elsif(/^\s*No file $fileb\.[^.]+\.$/){ $result{rerun} = 1; }
			elsif(/^(Over|Under)full.*at line/)
			{
				my $showit = 1;
				my $under = $1 eq 'Under';
				if($under){ ++$result{unders}; $showit=0 if $ignoreunder; }
				else      { ++$result{overs};  $showit=0 if $ignoreover;  }

				if($showit)
				{
					print "Warning($cfile): ";
					show_line(\$_);
					# Context only for overfull... making sense?
					$show = $p->{context} if $1 ne "Under";
				}
				++$result{warnings};
			}
			elsif(/^LaTeX Warning:\s*(.*)$/)
			{
				my $warn = $1;
				if($warn =~ /^(Reference|Citation).+on\s+page.+undefined/){ ++$result{undefrefs}; }
				elsif( $warn =~ /^Label\(s\)\s+may\s+have\s+changed.\s+Rerun/ )
				{
					$result{rerun} = 1; #if no error occured, of course
				}
				elsif( not $_ =~ /^LaTeX Warning: There were undefined references.$/)
				{
					++$result{warnings}; #an unspecified warning
					$show = $p->{context};
				}
				if($shown)
				{
					print "Warning was in $cfile.\n";
				}
				else
				{
					print "Warning($cfile): ";
					$warn .= "\n";
					show_line(\$warn);
				}
			}
			elsif($_ =~ /^Output\s+written\s+on\s+/){ $result{success} = 1; }
		}
		close(LATTE);
		
	}
	return \%result;
}

sub show_line
{
	unless($shown)
	{
		my $line = shift;
		print ${$line};
		$shown = 1;
	}
}

sub file_hash
{
	my $file = shift;
	return -e $file ? `md5sum $file`  : -1;
}
