=head1 NAME

EPrints::Plugin::Event

=for Pod2Wiki

=head1 DESCRIPTION

Event plugins are called by the indexer to actually do work. L<EPrints::DataObj::EventQueue> objects are stored in the database and executed once their start_time is due. The object contains the plugin ID, action and optionally parameters.

Action is the sub called on the plugin. This sub must return undef or a valid response constant recognised by L<EPrints::DataObj::EventQueue/execute>.

Parameters can contain any Perl data structure that can be serialised by L<Storable>. As a special case parameters that look like an internal id are re-instantiated as the referenced object before the plugin action is called.

Events also support scheduled (or repeating) events by calling L</cron> on this class with the actual plugin id/action/parameters to call. The scheduled event isn't called directly but is triggered via a new event object with a start_time of now.

=head1 SYNOPSIS

	EPrints::DataObj::EventQueue->create_unique( $repo, {
		pluginid => "Event::Hello",
		action => "hello",
		params => ["John Smith"],
	});
	
	EPrints::DataObj::EventQueue->create_unique( $repo, {
		pluginid => "Event",
		action => "cron",
		params => ["0,15,30,45 * * * *",
			"Event::Hello",
			"hello",
			"John Smith",
		],
	});

=cut

package EPrints::Plugin::Event;

@ISA = qw( EPrints::Plugin );

use strict;

my @RANGES = (
	"0-59",
	"0-23",
	"1-31",
	"1-12",
	"0-6",
);

sub new
{
	my( $class, %params ) = @_;

	$params{visible} = exists $params{visible} ? $params{visible} : "all";
	$params{advertise} = exists $params{advertise} ? $params{advertise} : 1;

	return $class->SUPER::new(%params);
}

# debug utility, just log whatever the parameters are
sub echo
{
	my( $class, @params ) = @_;

	for(@params)
	{
		$_ = $_->internal_uri if UNIVERSAL::isa( $_, "EPrints::DataObj" );
	}

	warn join ', ', @params;

	return;
}

sub cron
{
	my( $self, $time_spec, $pluginid, $action, @params ) = @_;

	my $event = $self->{event};

	# queue the actual action
	EPrints::DataObj::EventQueue->create_unique( $self->{session}, {
		pluginid => $pluginid,
		action => $action,
		params => \@params,
	});

	my $next_t = $self->_next_execution_time( $time_spec );
	$event->set_value( "start_time", EPrints::Time::iso_datetime( $next_t ) );

	return EPrints::Const::HTTP_RESET_CONTENT;
}

sub _next_execution_time
{
	my( $self, $time_spec ) = @_;

	my @entry = split /\s+/, $time_spec;
	Carp::croak( "Error in $time_spec: expected 5 entries but got ".@entry )
		if @entry != 5;

	local $_;

	my $i = 0;
	for(@entry)
	{
		$_ = $RANGES[$i] if $_ eq '*';
		$_ =~ s/([0-9]{1,2})-([0-9]{1,2})/join(',',$1 .. $2)/eg;
		$_ = {
			map { $_ => 1 }
			split /,/, $_
		};
		$i++;
	}

	my $next_t = time();
	$next_t -= $next_t % 60;
	$next_t += 60;

	# this algorithm increments $next_t until it is an acceptable value for the
	# cron entry
	# for better efficiency it increments by the largest non-matching criteria
	# (e.g. a month if the current month is no good)
	while(_increment( $next_t, @entry ))
	{
		EPrints->abort( "Out of cron range" )
			if $next_t > time() + 31622400;
	}

	return $next_t;
}

# return true if we incremented $t
sub _increment
{
	my( $t, @entry ) = @_;

	# @entry[0] = hash of minutes to run
        # @entry[1] =    "    hours to run
        #       [2] =    "    days of month
        #       [3] =    "    months to run
        #       [4] =    "    days of week to run

	my @t = (gmtime($t))[0..7];
	$t[4]++; # month

	# month: if this month isn't in the months we should run
	#  increment to midnight of the start of next month.
	if( !exists $entry[3]{$t[4]} )
	{
		$_[0] -= 86400 * ($t[3] - 1);
		$_[0] -= $_[0] % 86400;
		if( $t[4] == 12 )
		{
			@t = (gmtime($_[0]))[0..7];
			$t[4] = 0; # month
			$t[5]++; # year
			$_[0] = Time::Local::timegm(@t);
		}
		else
		{
			@t = (gmtime($_[0]))[0..7];
			$t[4]++; # month
			$_[0] = Time::Local::timegm(@t);
		}
		return 1;
	}

	# day of week / day of month
	# if today isn't a day of the week we should run
	#      OR
	#    today isn't a day of the month we should run
	# increment to tomorrow.
	if( !exists $entry[4]{$t[6]} || !exists $entry[2]{$t[3]} )
	{
		$_[0] -= $_[0] % 86400;
		$_[0] += 86400;
		return 1;
	}

	# hour: if this hour isn't an hour we should run, increment to the next hour
	if( !exists $entry[1]{$t[2]} )
	{
		$_[0] -= $_[0] % 3600;
		$_[0] += 3600;
		return 1;
	}

	# minute
	if( !exists $entry[0]{$t[1]} )
	{
		$_[0] += 60;
		return 1;
	}

	return 0;
}

1;

=head1 COPYRIGHT

=for COPYRIGHT BEGIN

Copyright 2022 University of Southampton.
EPrints 3.4 is supplied by EPrints Services.

http://www.eprints.org/eprints-3.4/

=for COPYRIGHT END

=for LICENSE BEGIN

This file is part of EPrints 3.4 L<http://www.eprints.org/>.

EPrints 3.4 and this file are released under the terms of the
GNU Lesser General Public License version 3 as published by
the Free Software Foundation unless otherwise stated.

EPrints 3.4 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with EPrints 3.4.
If not, see L<http://www.gnu.org/licenses/>.

=for LICENSE END

