package Net::Curl::Multi::EV;

use strict;
use warnings;
use EV;
use Net::Curl::Easy qw(CURLE_OPERATION_TIMEDOUT);
use Net::Curl::Multi qw(CURLMSG_DONE CURL_SOCKET_TIMEOUT /^CURL_POLL_/ /^CURL_CSELECT_/ CURLMOPT_SOCKETFUNCTION CURLMOPT_TIMERFUNCTION);
use Scalar::Util qw(weaken);

our $VERSION = '0.08';

BEGIN {
	Net::Curl::Multi->can('CURLMOPT_TIMERFUNCTION') or
		die "Net::Curl::Multi is missing timer callback,\nrebuild Net::Curl with libcurl 7.16.0 or newer\n";
}


sub curl_ev {
	my ($multi) = @_;

	my %finish = (); # easy => finish
	my %timer  = (); # easy => sub

	# perform and call any callbacks that have finished
	my $multi_active = -1;
	my $socket_action = sub {

		my $active = $multi->socket_action(@_);
		return if $multi_active == $active;
		$multi_active = $active;

		while (my ($msg, $easy, $result) = $multi->info_read()) {
			if ($msg == CURLMSG_DONE) {
				delete $timer{$easy};
				$multi->remove_handle($easy);
				$finish{$easy}->($easy, $result);
				delete $finish{$easy};
			} else {
				die "I don't know what to do with message $msg.\n";
			}
		}
	};

	my $cb_timeout = sub { $socket_action->(CURL_SOCKET_TIMEOUT) };

	my $add_handle_timer;

	# add one handle and kickstart download
	my $add_handle = sub {
		my ($easy, $finish, $timeout) = @_;

		$finish{$easy} = $finish;

		# Calling socket_action with default arguments will trigger
		# socket callback and register IO events.
		#
		# It _must_ be called _after_ add_handle(); EV will take care
		# of that.
		#
		# We are delaying the call because in some cases socket_action
		# may finish inmediatelly (i.e. there was some error or we used
		# persistent connections and server returned data right away)
		# and it could confuse our application -- it would appear to
		# have finished before it started.

		$add_handle_timer = EV::timer 0, 0, $cb_timeout;

		$multi->add_handle($easy);

		if ($timeout) {
			$timer{$easy} = EV::timer $timeout, 0, sub {
				delete $timer{$easy};
				$multi->remove_handle($easy);
				$finish{$easy}->($easy, bless( do{\(my $o = CURLE_OPERATION_TIMEDOUT)}, 'Net::Curl::Easy::Code' ));
				delete $finish{$easy};
			};
		}
	};


	# socket callback: will be called by curl any time events on some
	# socket must be updated
	my %wr = (); # socket => $w
	my %ww = (); # socket => $w
	my $cb_socket = sub {
		my ($multi, $easy, $socket, $poll) = @_;

		# Right now $socket belongs to that $easy, but it can be
		# shared with another easy handle if server supports persistent
		# connections.
		# This is why we register socket events inside multi object
		# and not $easy.

		# register read event
		if ($poll == CURL_POLL_IN or $poll == CURL_POLL_INOUT) {
			$wr{$socket} ||= EV::io $socket, EV::READ, sub { $socket_action->($socket, CURL_CSELECT_IN) };
		} else {
			delete $wr{$socket};
		}

		# register write event
		if ($poll == CURL_POLL_OUT or $poll == CURL_POLL_INOUT) {
			$ww{$socket} ||= EV::io $socket, EV::WRITE, sub { $socket_action->($socket, CURL_CSELECT_OUT) };
		} else {
			delete $ww{$socket};
		}

		return 1;
	};


	# timer callback: It triggers timeout update. Timeout value tells
	# us how soon socket_action must be called if there were no actions
	# on sockets. This will allow curl to trigger timeout events.
	my $timer;
	my $cb_timer = sub {
		my ($multi, $timeout_ms) = @_;

		if ($timeout_ms < 0) {
			# Negative timeout means there is no timeout at all.
			# Normally happens if there are no handles anymore.
			#
			# However, curl_multi_timeout(3) says:
			#
			# Note: if libcurl returns a -1 timeout here, it just means
			# that libcurl currently has no stored timeout value. You
			# must not wait too long (more than a few seconds perhaps)
			# before you call curl_multi_perform() again.

			$timer = EV::timer 10, 10, $cb_timeout;
		} else {
			# This will trigger timeouts if there are any.
			my $t = $timeout_ms / 1000;
			$timer = EV::timer $t, $t, $cb_timeout;
		}

		return 1;
	};

	$multi->setopt(CURLMOPT_SOCKETFUNCTION, $cb_socket);
	$multi->setopt(CURLMOPT_TIMERFUNCTION,  $cb_timer);

	weaken $multi;

	return $add_handle;
}


1;

__END__

=head1 NAME

Net::Curl::Multi::EV - Using Net::Curl::Multi with EV.

=head1 SYNOPSIS

 use EV;
 use Net::Curl::Multi;
 use Net::Curl::Easy qw(/^CURLOPT_/);
 use Net::Curl::Multi::EV;

 my $multi   = Net::Curl::Multi->new();
 my $curl_ev = Net::Curl::Multi::EV::curl_ev($multi);

 my $easy = Net::Curl::Easy->new();

 $easy->setopt(CURLOPT_URL, $url);
 # ...

 my $finish = sub {
 	my ($easy, $result) = @_;
 	# ... $resul is Net::Curl::Easy::Code
	# ...
 	EV::break();
 };

 my $timeout =  4 * 60
 $curl_ev->($easy, $finish, $timeout);

 EV::run();

=head1 DESCRIPTION

Using Net::Curl::Multi with EV.

The module consists of the only curl_ev method, that receives the Net::Curl::Multi object and returns closures-function aimed in Net::Curl::Easy objects registration. When registering an object please define also the callback function and timeout in seconds (optional). The callback function is called when the work with Net::Curl::Easy is finished and accepts two arguments Net::Curl::Easy and Net::Curl::Easy::Code.

See example.pl file.

=head1 AUTHOR

Nick Kostyria

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2013 by Nick Kostyria

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.14.2 or,
at your option, any later version of Perl 5 you may have available.

=head1 SEE ALSO

L<EV>
L<Net::Curl>
L<http://search.cpan.org/~syp/Net-Curl/lib/Net/Curl/examples.pod#Multi::Event>

=cut
