#!/usr/bin/perl
# This needs to be stackless so it can run from init, and so it can signal.
use strict;
use warnings;
#use Mooix::Thing;
use POSIX qw{:sys_wait_h SIGUSR1};

# Keys are queue items, values are hashes of the info for queue items.
my %queue;
my $last_queue_run=time;
my $debug = 0;

sub debug {
	print STDERR localtime(time)." heartbeat: $_[0]\n" if $debug;
}

sub deleteitem {
	my $this=shift;
	my $item=shift;
	debug("deleting $item");
	delete $queue{$item};
	$this->remove(item => $item, noupdate => 1);
}

sub runitem {
	my $this=shift;
	my $item=shift;

	my $object=$this->$item;
	if (! ref $object) {
		deleteitem($this, $item);
		return;
	}
	
	debug("time: running $item");
	return 1 unless $this->background;

	my $method = $queue{$item}->{method};
	
	my $paramsfile=$item.".params";
	my @params;
	foreach ($this->params) {
		if (/^\"(.*)\"$/) {
			push @params, $1;
		}
		else {
			push @params, Mooix::Thing->get($_);
		}
	}

	# Run method with callstack based on the stack of the method that
	# registered it.
	$ENV{MOOIX_NEW_CALLSTACK} = $this->id."/$item.stack";
	$ENV{MOOIX_NONSTANDARD} = 1;
	$object->$method(@params);
	exit;
}

sub quit {
	my $this=shift;
	
	$this->_heartbeat_running(0);
	exit;
}

sub load_queue {
	my $this=shift;
	debug("loading queue");
	%queue=();
	opendir (DIR, ".") || die "$!";
	while ($_=readdir(DIR)) {
		next unless /^item\d+$/;
		my $infofile="$_.info";
		my %info = $this->$infofile;
		$queue{$_} = \%info;
		updateruntime($this, $_);
	}
}

# Calculate the time a job should next run.
sub updateruntime {
	my $this=shift;
	my $item=shift;
	my $nextrun=0;
	if (! exists $queue{$item}) {
		return -1;
	}
	my %info = %{$queue{$item}};
	my $time = time;

	if (! ref $this->$item) {
		# Item went away, so remove from queue.
		debug("item went away");
		deleteitem($this, $item);
		return -1;
	}
	
	if ($info{date}) {
		$nextrun = $info{date};
		# This is pretty stupid, and here only for completeness.
		# If you want this, you should just check against rand
		# before registering the job in the first place..
		if ($info{probability} && rand > $info{probability}) {
			$nextrun = -1;
		}
	}
	elsif ($info{interval}) {
		if (defined $info{enddate} && $info{enddate} && $time > $info{enddate}) {
			debug("enddate $info{enddate} passed");
			$nextrun = -1;
		}
		elsif (defined $info{startdate} && $info{startdate} >= $time) {
			$nextrun = $info{startdate} - $time;
		}
		else {
			# From the startdate, how long to the next
			# number of seconds evenly divisible by the
			# interval?
			my $delta = $time - ( defined $info{startdate} ? $info{startdate} : 0);
	
			$nextrun = $time + $info{interval} - ($delta % $info{interval});

			if ($info{probability} && rand > $info{probability}) {
				# XXX isn't there a way to do this
				# mathematically, rather than
				# algorithmically?
				my $c = 1;
				while (rand > $info{probability}) {
					$c++;
					last if $c == 100; # don't loop forever
				}
				$nextrun = $nextrun + $c * $info{interval};
			}
		}
	}
	
	$queue{$item}->{nextruntime} = $nextrun;
	
	# Remove an item that will never run again.
	if ($nextrun == -1) {
		debug("next run time is -1");
		deleteitem($this, $item);
	}
	
	return $nextrun;
}

# Runs any items that are ready, and returns the time to sleep until the next
# most recent item will be ready.
sub process_queue {
	my $this=shift;

	debug("process_queue");
	
	my $time=time();
	my $mintime=-1;
	foreach (keys %queue) {
		next if $queue{$_}->{nextruntime} <= $last_queue_run;
		my $til = $queue{$_}->{nextruntime} - $time;
		if ($til < 1) {
			runitem($this, $_);
			my $nextrun = updateruntime($this, $_);
			if ($nextrun != -1) {
				$til = updateruntime($this, $_) - $time;
				# Its next run time may still be the lowest run
				# time there is.
				if ($til < $mintime || $mintime == -1) {
					$mintime=$til;
				}
			}
		}
		elsif ($til < $mintime || $mintime == -1) {
			$mintime=$til;
		}
	}

	# Reap children.
	while (waitpid(-1, WNOHANG) != -1) {};
	
	return $mintime;
}

run sub {
	my $this=shift;
	%_=@_;
	if ($_{debug}) {
		$debug=$_{debug};
	}
	
	if ($this->_heartbeat_running) {
		debug("heartbeat already running; sugusr1");
		$this->signal(with => SIGUSR1);
		exit;
	}
	
	$SIG{INT} = $SIG{TERM} = sub { quit($this) };
	$SIG{USR1} = sub { load_queue($this) };
	
	$this->_heartbeat_running(1);
	load_queue($this);
	while (1) {
		my $until_next=process_queue($this);
		if ($until_next == -1) {
			debug("exit on empty queue");
			last; # queue empty
		}
		debug("sleeping $until_next");
		sleep($until_next);
	}
	quit($this);
}
