#!/usr/bin/perl -w

use strict;

use Getopt::Std;


# Grab the options
my %options;
getopts('s:t:ce', \%options);
#print_usage(), exit if ($options{h});
my $server = $options{s} or die "Must specify a server name\n";
my $restart_time = $options{t} || '0:00';
my $ctf = $options{c} || 0;
my $easypoints = $options{e} || 0;

# Autoflush
$| = 1;

# Declare variables
use vars (qw($system_status $child_pid $grandchild_pid $wait_pid $restart_hour 
		$restart_minute));

###
### MAIN PROGRAM
###

die "File /usr/share/wine-c/nerf/System/${server}.ini does not exist"
    unless (-e "/usr/share/wine-c/nerf/System/${server}.ini");
($restart_hour, $restart_minute) = parse_time_parm();

# Change to the NAB directory
chdir "/usr/share/wine-c/nerf/System" or die "Can't change directory ($!)\n";

# Setup signal handlers
$SIG{HUP} = \&terminate;
$SIG{INT} = \&terminate;
$SIG{QUIT} = \&terminate;
$SIG{TERM} = \&terminate;

# Loop forever.  Signals will kill the process.
while (1) {

	# Save off the old log file and compress it 
	if (-s "$server.log") {
	    print "$server.log EXISTS, COMPRESSING & RENAMING\n";
	    chomp(my $now = `date +%Y-%m-%d+%T`);
	    my $filename = "$server.log.$now.gz";
	    $system_status = system("gzip <$server.log >$filename");
	    die "Compress failed ($system_status, $!)\n"
		    if ( ($system_status & 0xffff) != 0);
	    print_error('Warning: Unable to remove old log file')
		    if (! unlink "$server.log");
	} else { print "$server.log DOES *NOT* EXIST OR IS EMPTY\n"; }	
	
	undef $wait_pid;

	# Move in new .ini file if it exists
	get_new_ini();

	# Import new maps, if map files exist
	get_new_maps();

	# Fork & exec the Nerf Server
	# We don't want to do the log copy & compression again if there
	# was a fork/exec/other process-related problem, we just want to
	# try running it again, so we wrap the process stuff in a while
	# loop
	my $try_again = 0;
	do {
		if ($child_pid = fork) {
			# This is the Parent, the controlling process.
			# First, let's make sure the child started up.  
			# Sleep a bit to give it time to get going.
			sleep 20;

			# Check to see if child didn't start
			my $child_alive = kill 0, $child_pid;
			unless ($child_alive) {
				$try_again = 1;
				print_error("Child didn't start");
			} else { 
				# Check if log file doesn't exist.  New child
				# may have started before old one was
				# completely cleaned up
				unless (-e "$server.log") {
					$try_again = 1;
					kill_child();
					print_error('No log file created');
				} else {
				    $try_again = run_parent();
				}
			}
		} elsif (defined $child_pid) {
			# Child, who will exec the NAB server or exit if
			# that fails.
			run_child();
		} else {
			print_error("Fork failed ($!)");
			$try_again = 1;
		}
		
		sleep 300 if $try_again;
	} while ($try_again);
}

sub parse_time_parm
{
	# Validate time parameter and put it in a standard format
	die "Restart time must be \"[n]n:nn\" (24-hour format)\n"
		unless ($restart_time =~ /(\d{1,2}):(\d{2})/);
	die "Bad time\n"
		if ($1 < 0 or $1 > 23 or $2 < 0 or $2 > 59);
	return( $1, $2 );
}

sub run_parent
{
	# Nerf Arena Blast gets another wine program going with the same
	# command line as a child process.  Let's get the 'ps' output
	# and find out its PID.

	open PSOUT, 'ps -fC wine |';
	foreach (<PSOUT>) {
		next unless /UCC\.exe/;
		/\S+\s+(\S+)\s+(\S+)/;
		$grandchild_pid = $1, last
			if ($2 == $child_pid);
	}
	close PSOUT;
	print "GRANDCHILD PID = $grandchild_pid\n";
	$SIG{CHLD} = \&reaper;

	# Wait for the specified time and kill the child

	# Wait an hour until we're close to the hour of the restart time
	my ($second, $minute, $hour) = localtime;

	# Convert it all to minute of the day
	my $restart_mod = ($restart_hour * 60) + $restart_minute;
	my $now_mod = ($hour * 60) + $minute;
	
	# Add a day's worth of minutes to the restart time if it is earlier
	# in the day than now
	$restart_mod += 1440 if ($restart_mod <= $now_mod);

	# Wait to within 10 minutes of the restart time
	my $sleep_time = ($restart_mod - $now_mod - 10) * 60;
	# ...but only do this long wait if we're over an hour from the
	# restart time
	if ($sleep_time > 3600) {
		print "SLEEPING FOR $sleep_time SECONDS\n";	
		sleep $sleep_time;
	}

	# A dead child might have woken me up.  We'll return 1 to get
	# the fork loop to try again;
	return 1 unless ($child_pid);

	print "SHORT-TIME SLEEPING - WAITING FOR $restart_hour:$restart_minute\n";	
	# We're close!  Now, wake up every 30 seconds until the time arrives
	($second, $minute, $hour) = localtime;
	print "=== $hour:$minute vs $restart_hour:$restart_minute\n";		
	until ($restart_hour == $hour and $restart_minute == $minute) {
		sleep 30;
		($second, $minute, $hour) = localtime;
	print "=== $hour:$minute vs $restart_hour:$restart_minute\n";		
	}

	print "TIME! ($hour:$minute)\n";	
	# Kill the child, wait for it to die, then return to the main loop
	kill_child() if ($child_pid); 

	$SIG{CHLD} = 'IGNORE';
	
	# Move in new .ini file if it exists
	get_new_ini();

	return 0;
}

sub run_child
{
	my $run_line = 'wine UCC.exe server ' .
		($ctf ? 'ctf-psi.nrf?game=CTF.CTFGame' : 
			'PM-Amateur.nrf') .
		'?mutator=GameLogger.GameLogger' .
		($easypoints ? '?mutator=EasyPoints.EasyPointsMutator' : '') .
		" ini=${server}.ini log=${server}.log";
	print "RUNNING: $run_line\n";
	unless (exec $run_line) {
		print_error("Exec failed ($!)");
		exit;
	}
}

sub kill_child
{
	print "KILLING $child_pid\n";		
	kill 15, $child_pid;
	print "WAITING...\n";		
	my $wait_pid = waitpid $child_pid, 0;
	print_error("Warning: There was no child to wait on ($!)") 
		if ($wait_pid < 0);
	$child_pid = 0;

	# Kill the grandchild.  It's not direcly related to us
	# so we don't wait for it.
	kill 15, $grandchild_pid if ($grandchild_pid);

	# Pause a bit for the network connection & master server
	# to recognize the death of the server
	print "PAUSING...\n";		
	sleep 30;
}

sub get_new_ini
{
	# Since the server rewrites the .ini file upon exit, we can't 
	# make changes to it (that we'd hope would take effect at the
	# next run).  Therefore, the file should be saved as
	# ${server}.ini.new, and at this point, now that the child is
	# gone, we can move it in overtop the regular .ini file.
	if (-e "/usr/share/wine-c/nerf/System/${server}.ini.new") {
		print "LOADING NEW INI FILE\n";
		print "RENAME FAILED\n"
		    unless (rename 
			"/usr/share/wine-c/nerf/System/${server}.ini.new", 
			"/usr/share/wine-c/nerf/System/${server}.ini");
	}

}

sub get_new_maps
{
	# Look for files called "${server}.maps.*".  Pick one and put
	# its map list in the server's main ini file.
	my @map_files = glob("${server}.maps.*");
	my $num_files = @map_files;
	return if ($num_files == 0);
	my $day_num = (localtime)[3];

	# The file to use is (currently) today's date modulo the number
	# of possible files.  The 'glob' function serves up its file list
	# in sorted order, so this ensures we cycle through all the files
	# over the course of as many days (restarting on the first day of
	# each month).
	my $file_to_use = $map_files[$day_num % $num_files];

	# NOTE:  This script determines which map section (PM or CTF) to
	# overwrite based on the -c parameter, *not* based on the name
	# of the map section (if any) in the .map.* file.  This is because
	# it is pointless to overwrite the PM maps when running a CTF
	# server.  However, just to be safe, if you do specify the map
	# section and it doesn't jive with the -c flag, nothing is done.
	
	# NOTE:  This script expects that your map section file will have
	# the entire set of values for the map (Maps[<number>] and MapNum).
	# It doesn't check for errors or missing entries.  The section
	# name (the line surrounded by [brackets]) is optional in your
	# map section file.

	unless (open INI, "<${server}.ini") {
		print "UNABLE TO OPEN ${server}.ini ($!)\n";
		return;
	}
	unless (open OUTI, ">${server}.ini.temp") {
		close INI;
		print "UNABLE TO OPEN ${server}.ini.temp ($!)\n";
		return;
	}
	unless (open MAPLIST, "<$file_to_use") {
		close OUTI;
		close INI;
		print "UNABLE TO OPEN $file_to_use ($!)\n";
		return;
	}

	my $map_section = ($ctf ? '[CTF.CTFMapList]' : '[NerfI.PMmaplist]');

	# The variable $print_it will be set to 0 when it should be skipping
	# the proper section of map files in the original ini file.
	my $print_it = 1;
	while (<INI>) {
		if (/^\Q$map_section\E/) {
			print OUTI $_;
			# Toss the maplist into OUTI and start ignoring
			# records from INI
			my $map_record = '';
			while (defined $map_record) {
				$map_record = <MAPLIST>;
				if (defined $map_record) {
					# Do we have a map section header?
					if ($map_record =~ /^\[/ and
					    $map_record !~ /\Q$map_section\E/) {
					    	print "INCORRECT MAP SECTION NAME IN $file_to_use ($map_record vs $map_section)\n";
						close MAPLIST;
						close OUTI;
						close INI;
						unlink "${server}.ini.temp";
						return;
					}
					next if ($map_record =~ /^\[/);
					print OUTI $map_record;
				}
			}
			$print_it = 0;
		} else {
			print OUTI $_ if $print_it;
			# Resume sending INI to OUTI if we've gone past the
			# MapNum entry for this map section.
			$print_it = 1 if ($print_it == 0 and /^MapNum/);
		}
	}
	close MAPLIST;
	close OUTI;
	close INI;
	# Juuuuuust in case...
	rename "${server}.ini", "${server}.ini.bak";
	rename "${server}.ini.temp", "${server}.ini";
}

sub reaper {
	wait;
	$child_pid = 0;
	# loathe sysV: it makes us not only reinstate
	# the handler, but place it after the wait
	$SIG{CHLD} = \&reaper;
}

sub print_error
{
	chomp(my $now = `date`);
	print STDERR $now, ':', @_, "\n";
}

sub terminate
{
	if ($child_pid) {
		kill 9, $child_pid;
		waitpid $child_pid, 0;
	}
	print_error('Exiting');
	exit;
}

