#!/usr/bin/perl

######################################################################
# LegacyWorlds Beta 5
# System control script
#
# Without arguments:
#   should run as root; creates a FIFO from which it reads commands.
#
# With arguments:
#   writes arguments to the pipe
######################################################################


###
## Configuration
#

$fifoPath  = "/tmp/.lwFifo";
$ctrlPath  = "/tmp/.lwControl";
$fsUser    = 0;
$fsGroup   = 33;




###
## Main code below
#

use IO::File;
require POSIX;

# If we have arguments, write to the FIFO and exit
if (@ARGV > 0) {
	if ($ARGV[0] eq '--start') {
		&start();
	} elsif ($ARGV[0] eq '--stop') {
		&stop();
	} else {
		&sendMessage(@ARGV);
		exit(0);
	}
}

# Find the script's path
use FindBin qw($Bin);
if (! -f "$Bin/legacyworlds.xml") {
	die "$0: could not find 'legacyworlds.xml' in '$Bin'\n";
}
$lwConf = "$Bin/legacyworlds.xml";

# Fork and detach
$pid = fork();
if ($pid < 0) {
	die("$0: failed to fork\n");
} elsif ($pid != 0) {
	exit(0);
}

# Detach
POSIX::setsid();
close STDIN; close STDOUT; close STDERR;
open(STDIN, "</dev/null"); open(STDOUT, ">/dev/null"); open(STDERR, ">/dev/null");

# First create the pipe if it doesn't exist
if (! -p $fifoPath) {
	if (-e $fifoPath) {
		die "$0: '$fifoPath' is not a pipe\n";
	} else {
		POSIX::mkfifo($fifoPath, 0400)
			or die "$0: unable to create '$fifoPath'\n";
	}
}
# Set the pipe's owner and group
if ($> == 0) {
	chown $fsUser, $fsGroup, $fifoPath;
} else {
	chown $>, $fsGroup, $fifoPath;
}
chmod 0620, $fifoPath;

# Create the control directory if needed
if (! -d $ctrlPath) {
	if (-e $ctrlPath) {
		die "$0: '$ctrlPath' is not a directory\n";
	} else {
		mkdir $ctrlPath, 0700
			or die "$0: unable to create '$ctrlPath'\n";
	}
}
# Set the owner and group
if ($> == 0) {
	chown $fsUser, $fsGroup, $ctrlPath;
} else {
	chown $>, $fsGroup, $ctrlPath;
}
chmod 0770, $ctrlPath;

# Define commands
%commands = (
	DIE	=> \&endController,
	MERGE	=> \&mergeConfiguration,
	TMPID	=> \&tickManagerID,
	TMINIT	=> \&tickManagerStart,
	TMSTOP	=> \&tickManagerStop,
	READY	=> \&gameReady,
	START	=> \&gameStart,
	SETEND	=> \&gameEnd,
	NOEND	=> \&gameCancelEnd,
	"END"	=> \&gameChangeEnd,
	SETDEF	=> \&setDefaultGame,
);

# Reader loop
while (1) {
	# Wait for someone to write to the pipe
	close(FIFO);
	open(FIFO, "< $fifoPath")
		or die "$0: unable to open '$fifoPath'\n";

	# Read it
	$command = <FIFO>;
	next unless defined $command;
	chomp($command);
	next if $command =~ /[^A-Za-z0-9\s]/;

	# Extract the actual command
	my @args = split /\s+/, $command;
	$command = shift @args;

	# Check if the command is allowed
	next unless defined $commands{$command};

	#print "Got command $command, arguments = (" . join(', ', @args) . ")\n";
	&{$commands{$command}}(@args);
}



###
## Helper functions
#

sub sendMessage {
	my @args = @_;
	die "$0: FIFO '$fifoPath' doesn't exist\n" unless -p $fifoPath;
	open(FIFO, "> $fifoPath") or die "$0: unable to open FIFO '$fifoPath'\n";
	print FIFO (join(' ', @args) . "\n");
	close(FIFO);
}


sub start {
	$pid = fork();
	if ($pid == -1) {
		die "$0: could not fork\n";
	} elsif ($pid) {
		print "LegacyWorlds - Initialising game\n";
		print " -> Controller\n";
		sleep(1);
		print " -> Ticks manager\n";
		&sendMessage("TMINIT");
		exit(0);
	}
}


sub stop {
	print "LegacyWolrds - Shutting down\n";
	print " -> Ticks manager\n";
	&sendMessage("TMSTOP");
	sleep(1);
	print " -> Controller\n";
	&sendMessage("DIE");
	exit(0);
}


###
## Command functions
#

#
# Function that terminates the controller
#
sub endController {
	exit(0);
}


#
# Function that adds a new game
#
sub mergeConfiguration {
	my $sourceFile = shift;

	return unless -f "$ctrlPath/config.$sourceFile.xml";

	# Read the new snippet
	open(NEWCONF, "< $ctrlPath/config.$sourceFile.xml");
	my @newConfiguration = <NEWCONF>;
	close(NEWCONF);

	# Read the old configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Merge it
	my @merged = ();
	foreach my $oldLine (@oldConfiguration) {
		if ($oldLine =~ /^\s+<\/Games>\s*$/) {
			@merged = (@merged, @newConfiguration, "\n");
		}
		@merged = (@merged, $oldLine);
	}

	# Write the new configuration file
	open(OLDCONF, "> $lwConf");
	print OLDCONF @merged;
	close OLDCONF;

	# Remove the source file
	unlink "$ctrlPath/config.$sourceFile.xml";
}

#
# Tick manager PID update
#
sub tickManagerID {
	my $pid = shift;
	return unless $pid;

	open(PIDFILE, "> $ctrlPath/tickManager.pid");
	print PIDFILE "$pid " . time() . "\n";
	close(PIDFILE);
}

#
# Start tick manager
#
sub tickManagerStart {
	return if &tickManagerStatus();
	return unless -f "$Bin/ticks.php";
	if ($> == 0) {
		system("runuser -u lwticks -- bash -c 'cd $Bin; php ticks.php'");
	} else {
		system("cd $Bin; php ticks.php");
	}
}

#
# Stop tick manager
#
sub tickManagerStop {
	my $pid;
	return unless ($pid = &tickManagerStatus());
	kill 15, $pid;
	unlink("$ctrlPath/tickManager.pid");
}

#
# Check tick manager status
#
sub tickManagerStatus {
	return 0 unless -f "$ctrlPath/tickManager.pid";

	open(PIDFILE, "< $ctrlPath/tickManager.pid");
	my $data = <PIDFILE>;
	close(PIDFILE);

	chomp($data);
	my ($pid, $time) = split / /, $data;
	return (time() - $time < 22 ? $pid : 0);
}

#
# Make a game ready
#
sub gameReady {
	my $gName = shift;

	# Read the current configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Generate new configuration
	my @newConf = ();
	foreach my $line (@oldConfiguration) {
		if ($line =~ /<Game\s+id="$gName".*\s+public="0"\s+canjoin="0"/) {
			$line =~ s/\spublic="0"\s+canjoin="0"/ public="1" canjoin="1"/;
		}
		push @newConf, $line;
	}

	# Write configuration file
	open(NEWCONF, "> $lwConf");
	print NEWCONF @newConf;
	close(NEWCONF);
}

#
# Start the game earlier or later
#
sub gameStart {
	my $gName = shift;
	my $when = shift;

	return if ($when ne "EARLY" && $when ne "LATE");
	$when = ($when eq 'EARLY') ? -1 : 1;
	$when *= 24 * 60 * 60;

	# Read the current configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Generate new configuration
	my @newConf = ();
	my $state = 0;
	foreach my $line (@oldConfiguration) {
		if ($state == 0 && $line =~ /<Game\s+id="$gName".*\s+public="1"\s+canjoin="1"/) {
			$state = 1;
		} elsif ($state == 1) {
			if ($line =~ /<\/Game>/) {
				$state = 0;
			} elsif ($line =~ /<Tick\s+script="[a-z]+"\s+first="([0-9]+)"\s+interval="[0-9]+"\s*\/>/) {
				my $fTick = $1;
				$fTick += $when;
				$line =~ s/\sfirst="[0-9]+"/ first="$fTick"/;
			}
		}
		push @newConf, $line;
	}

	# Write configuration file
	open(NEWCONF, "> $lwConf");
	print NEWCONF @newConf;
	close(NEWCONF);
}

#
# Set a running game to end
#
sub gameEnd {
	my $gName = shift;
	my $when = shift;

	$when = $when * 60 * 60 + time();

	# Read the current configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Generate new configuration
	my @newConf = ();
	my $state = 0;
	foreach my $line (@oldConfiguration) {
		if ($state == 0 && $line =~ /<Game\s+id="$gName".*\s+public="1"\s+canjoin="1"/) {
			$state = 1;
		} elsif ($state == 1) {
			if ($line =~ /<\/Game>/) {
				$state = 0;
			} elsif ($line =~ /<Tick\s+script="[a-z]+"\s+first="([0-9]+)"\s+interval="[0-9]+"\s*\/>/) {
				my $fTick = $1;
				$line =~ s/\sfirst="[0-9]+"/ first="$fTick" last="$when"/;
			}
		}
		push @newConf, $line;
	}

	# Write configuration file
	open(NEWCONF, "> $lwConf");
	print NEWCONF @newConf;
	close(NEWCONF);
}

#
# Set an ending game's status back to running/victory
#
sub gameCancelEnd {
	my $gName = shift;

	# Read the current configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Generate new configuration
	my @newConf = ();
	my $state = 0;
	foreach my $line (@oldConfiguration) {
		if ($state == 0 && $line =~ /<Game\s+id="$gName".*\s+public="1"\s+canjoin="1"/) {
			$state = 1;
		} elsif ($state == 1) {
			if ($line =~ /<\/Game>/) {
				$state = 0;
			} elsif ($line =~ /<Tick\s+script="[a-z]+"\s+/) {
				$line =~ s/\slast="[0-9]+"//;
			}
		}
		push @newConf, $line;
	}

	# Write configuration file
	open(NEWCONF, "> $lwConf");
	print NEWCONF @newConf;
	close(NEWCONF);
}

#
# End the game earlier or later
#
sub gameChangeEnd {
	my $gName = shift;
	my $when = shift;

	return if ($when ne "EARLY" && $when ne "LATE" && $when ne "NOW");
	if ($when ne 'NOW') {
		$when = ($when eq 'EARLY') ? -1 : 1;
		$when *= 24 * 60 * 60;
	}

	# Read the current configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Generate new configuration
	my @newConf = ();
	my $state = 0;
	foreach my $line (@oldConfiguration) {
		if ($state == 0 && $line =~ /<Game\s+id="$gName".*\s+public="1"\s+canjoin="1"/) {
			$state = 1;
		} elsif ($state == 1) {
			if ($line =~ /<\/Game>/) {
				$state = 0;
			} elsif ($line =~ /<Tick\s+script="[a-z]+".*\slast="([0-9]+)"/) {
				my $lTick = $1;
				if ($when eq 'NOW') {
					$lTick = time();
				} else {
					$lTick += $when;
				}
				$line =~ s/\slast="[0-9]+"/ last="$lTick"/;
			}
		}
		push @newConf, $line;
	}

	# Write configuration file
	open(NEWCONF, "> $lwConf");
	print NEWCONF @newConf;
	close(NEWCONF);
}

#
# Changes the default game
#
sub setDefaultGame {
	my $gName = shift;
	return if ($gName eq "");

	# Read the current configuration
	open(OLDCONF, "< $lwConf");
	my @oldConfiguration = <OLDCONF>;
	close(OLDCONF);

	# Generate new configuration
	my @newConf = ();
	foreach my $line (@oldConfiguration) {
		if ($line =~ /<Games\s+default="[a-z0-9]+"/) {
			$line =~ s/<Games\s+default="[0-9a-z]+"/<Games default="$gName"/;
		}
		push @newConf, $line;
	}

	# Write configuration file
	open(NEWCONF, "> $lwConf");
	print NEWCONF @newConf;
	close(NEWCONF);
}