The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#  You may distribute under the terms of either the GNU General Public License
#  or the Artistic License (the same terms as Perl itself)
#
#  (C) Paul Evans, 2010-2016 -- leonerd@leonerd.org.uk

package Tangence::ObjectProxy;

use strict;
use warnings;

our $VERSION = '0.23';

use Carp;

use Tangence::Constants;

use Tangence::Types;

use Scalar::Util qw( weaken );

=head1 NAME

C<Tangence::ObjectProxy> - proxy for a C<Tangence> object in a
C<Tangence::Client>

=head1 DESCRIPTION

Instances in this class act as a proxy for an object in the
L<Tangence::Server>, allowing methods to be called, events to be subscribed
to, and properties to be watched.

These objects are not directly constructed by calling the C<new> class method;
instead they are returned by methods on L<Tangence::Client>, or by methods on
other C<Tangence::ObjectProxy> instances. Ultimately every object proxy that a
client uses will come from either the proxy to the registry, or the root
object.

=cut

sub new
{
   my $class = shift;
   my %args = @_;

   my $self = bless {
      client => $args{client},
      id     => $args{id},

      class => $args{class},

      on_error => $args{on_error},
   }, $class;

   # An ObjectProxy is useless after its connection disappears
   weaken( $self->{client} );

   return $self;
}

sub destroy
{
   my $self = shift;

   $self->{destroyed} = 1;

   foreach my $cb ( @{ $self->{subscriptions}->{destroy} } ) {
      $cb->();
   }

   undef %$self;
   $self->{destroyed} = 1;
}

=head1 METHODS

The following methods documented with a trailing call to C<< ->get >> return
L<Future> instances.

=cut

use overload '""' => \&STRING;

sub STRING
{
   my $self = shift;
   return "Tangence::ObjectProxy[id=$self->{id}]";
}

=head2 id

   $id = $proxy->id

Returns the object ID for the C<Tangence> object being proxied for.

=cut

sub id
{
   my $self = shift;
   return $self->{id};
}

=head2 classname

   $classname = $proxy->classname

Returns the name of the class of the C<Tangence> object being proxied for.

=cut

sub classname
{
   my $self = shift;
   return $self->{class}->name;
}

=head2 class

   $class = $proxyobj->class

Returns the L<Tangence::Meta::Class> object representing the class of this
object.

=cut

sub class
{
   my $self = shift;
   return $self->{class};
}

=head2 can_method

   $method = $proxy->can_method( $name )

Returns the L<Tangence::Meta::Method> object representing the named method, or
C<undef> if no such method exists.

=cut

sub can_method
{
   my $self = shift;
   return $self->class->method( @_ );
}

=head2 can_event

   $event = $proxy->can_event( $name )

Returns the L<Tangence::Meta::Event> object representing the named event, or
C<undef> if no such event exists.

=cut

sub can_event
{
   my $self = shift;
   return $self->class->event( @_ );
}

=head2 can_property

   $property = $proxy->can_property( $name )

Returns the L<Tangence::Meta::Property> object representing the named
property, or C<undef> if no such property exists.

=cut

sub can_property
{
   my $self = shift;
   return $self->class->property( @_ );
}

# Don't want to call it "isa"
sub proxy_isa
{
   my $self = shift;
   if( @_ ) {
      my ( $class ) = @_;
      return !! grep { $_->name eq $class } $self->{class}, $self->{class}->superclasses;
   }
   else {
      return $self->{class}, $self->{class}->superclasses
   }
}

sub grab
{
   my $self = shift;
   my ( $smashdata ) = @_;

   foreach my $property ( keys %{ $smashdata } ) {
      my $value = $smashdata->{$property};
      my $dim = $self->can_property( $property )->dimension;

      if( $dim == DIM_OBJSET ) {
         # Comes across in a LIST. We need to map id => obj
         $value = { map { $_->id => $_ } @$value };
      }

      my $prop = $self->{props}->{$property} ||= {};
      $prop->{cache} = $value;
   }
}

=head2 call_method

   $result = $proxy->call_method( $mname, @args )->get

Calls the given method on the server object, passing in the given arguments.
Returns a L<Future> that will yield the method's result.

=cut

sub call_method
{
   my $self = shift;
   my ( $method, @args ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->call_method in void context no longer useful - it now returns a Future";

   my $mdef = $self->can_method( $method )
      or croak "Class ".$self->classname." does not have a method $method";

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

   my $request = Tangence::Message->new( $client, MSG_CALL )
         ->pack_int( $self->id )
         ->pack_str( $method );

   my @argtypes = $mdef->argtypes;
   $argtypes[$_]->pack_value( $request, $args[$_] ) for 0..$#argtypes;

   $client->request(
      request => $request,
   )->then( sub {
      my ( $message ) = @_;

      my $code = $message->code;

      if( $code == MSG_RESULT ) {
         my $result = $mdef->ret ? $mdef->ret->unpack_value( $message )
                                 : undef;
         Future->done( $result );
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

=head2 subscribe_event

   $proxy->subscribe_event( $event, %callbacks )->get

Subscribes to the given event on the server object, installing a callback
function which will be invoked whenever the event is fired.

Takes the following named callbacks:

=over 8

=item on_fire => CODE

Callback function to invoke whenever the event is fired

 $on_fire->( @args )

The returned C<Future> it is guaranteed to be completed before any invocation
of the C<on_fire> event handler.

=back

=cut

sub subscribe_event
{
   my $self = shift;
   my ( $event, %args ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->subscribe_event in void context no longer useful - it now returns a Future";

   ref( my $callback = delete $args{on_fire} ) eq "CODE"
      or croak "Expected 'on_fire' as a CODE ref";

   $self->can_event( $event )
      or croak "Class ".$self->classname." does not have an event $event";

   if( my $cbs = $self->{subscriptions}->{$event} ) {
      push @$cbs, $callback;
      return Future->done;
   }

   my @cbs = ( $callback );
   $self->{subscriptions}->{$event} = \@cbs;

   return Future->done if $event eq "destroy"; # This is automatically handled

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

   $client->request(
      request => Tangence::Message->new( $client, MSG_SUBSCRIBE )
         ->pack_int( $self->id )
         ->pack_str( $event ),
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_SUBSCRIBED ) {
         Future->done;
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

sub handle_request_EVENT
{
   my $self = shift;
   my ( $message ) = @_;

   my $event = $message->unpack_str();
   my $edef = $self->can_event( $event ) or return;

   my @args = map { $_->unpack_value( $message ) } $edef->argtypes;

   if( my $cbs = $self->{subscriptions}->{$event} ) {
      foreach my $cb ( @$cbs ) { $cb->( @args ) }
   }
}

=head2 unsubscribe_event

   $proxy->unsubscribe_event( $event )

Removes an event subscription on the given event on the server object that was
previously installed using C<subscribe_event>.

=cut

sub unsubscribe_event
{
   my $self = shift;
   my ( $event ) = @_;

   $self->can_event( $event )
      or croak "Class ".$self->classname." does not have an event $event";

   return if $event eq "destroy"; # This is automatically handled

   my $client = $self->{client};
   $client->request(
      request => Tangence::Message->new( $client, MSG_UNSUBSCRIBE )
         ->pack_int( $self->id )
         ->pack_str( $event ),

      on_response => sub {},
   );
}

=head2 get_property

   $value = $proxy->get_property( $prop )->get

Requests the current value of the property from the server object.

=cut

sub get_property
{
   my $self = shift;
   my ( $property ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->get_property in void context no longer useful - it now returns a Future";

   my $pdef = $self->can_property( $property )
      or croak "Class ".$self->classname." does not have a property $property";

   my $client = $self->{client};
   $client->request(
      request => Tangence::Message->new( $client, MSG_GETPROP )
         ->pack_int( $self->id )
         ->pack_str( $property ),
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_RESULT ) {
         my $value = $pdef->overall_type->unpack_value( $message );
         Future->done( $value );
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

=head2 get_property_element

   $value = $proxy->get_property_element( $property, $index_or_key )->get

Requests the current value of an element of the property from the server
object.

=cut

sub get_property_element
{
   my $self = shift;
   my ( $property, $index_or_key ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->get_property_element in void context no longer useful - it now returns a Future";

   my $pdef = $self->can_property( $property )
      or croak "Class ".$self->classname." does not have a property $property";

   my $client = $self->{client};
   $client->_ver_can_getpropelem or croak "Server is too old to support MSG_GETPROPELEM";

   my $request = Tangence::Message->new( $client, MSG_GETPROPELEM )
      ->pack_int( $self->id )
      ->pack_str( $property );

   if( $pdef->dimension == DIM_HASH ) {
      $request->pack_str( $index_or_key );
   }
   elsif( $pdef->dimension == DIM_ARRAY or $pdef->dimension == DIM_QUEUE ) {
      $request->pack_int( $index_or_key );
   }
   else {
      croak "Cannot get_property_element of a non hash, array or queue";
   }

   $client->request(
      request => $request,
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_RESULT ) {
         my $value = $pdef->type->unpack_value( $message );
         Future->done( $value );
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

=head2 prop

   $value = $proxy->prop( $property )

Returns the locally-cached value of a smashed property. If the named property
is not a smashed property, an exception is thrown.

=cut

sub prop
{
   my $self = shift;
   my ( $property ) = @_;

   if( exists $self->{props}->{$property}->{cache} ) {
      return $self->{props}->{$property}->{cache};
   }

   croak "$self does not have a cached property '$property'";
}

=head2 set_property

   $proxy->set_property( $prop, $value )->get

Sets the value of the property in the server object.

=cut

sub set_property
{
   my $self = shift;
   my ( $property, $value ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->set_property in void context no longer useful - it now returns a Future";

   my $pdef = $self->can_property( $property )
      or croak "Class ".$self->classname." does not have a property $property";

   my $client = $self->{client};
   my $request = Tangence::Message->new( $client, MSG_SETPROP )
         ->pack_int( $self->id )
         ->pack_str( $property );
   $pdef->type->pack_value( $request, $value ),

   $client->request(
      request => $request,
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_OK ) {
         Future->done;
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

=head2 watch_property

   $proxy->watch_property( $property, %callbacks )->get

=head2 watch_property_with_initial

   $proxy->watch_property_with_initial( $property, %callbacks )->get

Watches the given property on the server object, installing callback functions
which will be invoked whenever the property value changes. The latter form
additionally ensures that the server will send the current value of the
property as an initial update to the C<on_set> event, atomically when it
installs the update watches.

Takes the following named arguments:

=over 8

=item on_updated => CODE

Optional. Callback function to invoke whenever the property value changes.

 $on_updated->( $new_value )

If not provided, then individual handlers for individual change types must be
provided.

=back

The set of callback functions that are required depends on the type of the
property. These are documented in the C<watch_property> method of
L<Tangence::Object>.

=cut

sub _watchcbs_from_args
{
   my ( $pdef, %args ) = @_;

   my $callbacks = {};
   my $on_updated = delete $args{on_updated};
   if( $on_updated ) {
      ref $on_updated eq "CODE" or croak "Expected 'on_updated' to be a CODE ref";
      $callbacks->{on_updated} = $on_updated;
   }

   foreach my $name ( @{ CHANGETYPES->{$pdef->dimension} } ) {
      # All of these become optional if 'on_updated' is supplied
      next if $on_updated and not exists $args{$name};

      ref( $callbacks->{$name} = delete $args{$name} ) eq "CODE"
         or croak "Expected '$name' as a CODE ref";
   }

   return $callbacks;
}

sub watch_property              { shift->_watch_property( shift, 0, @_ ) }
sub watch_property_with_initial { shift->_watch_property( shift, 1, @_ ) }

sub _watch_property
{
   my $self = shift;
   my ( $property, $want_initial, %args ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->watch_property in void context no longer useful - it now returns a Future";

   my $pdef = $self->can_property( $property )
      or croak "Class ".$self->classname." does not have a property $property";

   my $callbacks = _watchcbs_from_args( $pdef, %args );

   # Smashed properties behave differently
   my $smash = $pdef->smashed;

   if( my $cbs = $self->{props}->{$property}->{cbs} ) {
      if( $want_initial and !$smash ) {
         return $self->get_property( $property )
            ->then( sub {
               $callbacks->{on_set} and $callbacks->{on_set}->( $_[0] );
               $callbacks->{on_updated} and $callbacks->{on_updated}->( $_[0] );
               push @$cbs, $callbacks;
               Future->done;
            });
      }
      elsif( $want_initial and $smash ) {
         my $cache = $self->{props}->{$property}->{cache};
         $callbacks->{on_set} and $callbacks->{on_set}->( $cache );
         $callbacks->{on_updated} and $callbacks->{on_updated}->( $cache );
         push @$cbs, $callbacks;
         return Future->done;
      }
      else {
         push @$cbs, $callbacks;
         return Future->done;
      }

      die "UNREACHED";
   }

   $self->{props}->{$property}->{cbs} = [ $callbacks ];

   if( $smash ) {
      if( $want_initial ) {
         my $cache = $self->{props}->{$property}->{cache};
         $callbacks->{on_set} and $callbacks->{on_set}->( $cache );
         $callbacks->{on_updated} and $callbacks->{on_updated}->( $cache );
      }

      return Future->done;
   }

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

   my $request = Tangence::Message->new( $client, MSG_WATCH )
         ->pack_int( $self->id )
         ->pack_str( $property )
         ->pack_bool( $want_initial );

   $client->request(
      request => $request,
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_WATCHING ) {
         Future->done;
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

=head2 watch_property_with_cursor

   ( $cursor, $first_idx, $last_idx ) =
      $proxy->watch_property_with_cursor( $property, $from, %callbacks )->get

A variant of C<watch_property> that installs a watch on the given property of
the server object, and additionally returns an cursor object that can be used
to lazily fetch the values stored in it.

The C<$from> value indicates which end of the queue the cursor should start
from; C<CUSR_FIRST> to start at index 0, or C<CUSR_LAST> to start at the
highest-numbered index. The cursor is created atomically with installing the
watch.

=cut

sub watch_property_with_iter
{
   my $self = shift;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->watch_property_with_iter in void context no longer useful - it now returns a Future";

   return $self->watch_property_with_cursor( @_ );
}

sub watch_property_with_cursor
{
   my $self = shift;
   my ( $property, $from, %args ) = @_;

   if( $from eq "first" ) {
      $from = CUSR_FIRST;
   }
   elsif( $from eq "last" ) {
      $from = CUSR_LAST;
   }
   else {
      croak "Unrecognised 'from' value $from";
   }

   my $pdef = $self->can_property( $property )
      or croak "Class ".$self->classname." does not have a property $property";

   my $callbacks = _watchcbs_from_args( $pdef, %args );

   # Smashed properties behave differently
   my $smashed = $pdef->smashed;

   if( my $cbs = $self->{props}->{$property}->{cbs} ) {
      die "TODO: need to synthesize a second cursor";
   }

   $self->{props}->{$property}->{cbs} = [ $callbacks ];

   if( $smashed ) {
      die "TODO: need to synthesize an cursor";
   }

   my $client = $self->{client};
   $client->_ver_can_cursor or croak "Server is too old to support MSG_WATCH_CUSR";
   $pdef->dimension == DIM_QUEUE or croak "Can only iterate on queue-dimension properties";

   $client->request(
      request => Tangence::Message->new( $client, MSG_WATCH_CUSR )
         ->pack_int( $self->id )
         ->pack_str( $property )
         ->pack_int( $from ),
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_WATCHING_CUSR ) {
         my $cursor_id = $message->unpack_int();
         my $first_idx = $message->unpack_int();
         my $last_idx  = $message->unpack_int();

         my $cursor = Tangence::ObjectProxy::_Cursor->new( $self, $cursor_id, $pdef->type );
         Future->done( $cursor, $first_idx, $last_idx );
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

sub handle_request_UPDATE
{
   my $self = shift;
   my ( $message ) = @_;

   my $prop  = $message->unpack_str();
   my $how   = TYPE_U8->unpack_value( $message );

   my $pdef = $self->can_property( $prop ) or return;
   my $type = $pdef->type;
   my $dim  = $pdef->dimension;

   my $p = $self->{props}->{$prop} ||= {};

   my $dimname = DIMNAMES->[$dim];
   if( my $code = $self->can( "_update_property_$dimname" ) ) {
      $code->( $self, $p, $type, $how, $message );
   }
   else {
      croak "Unrecognised property dimension $dim for $prop";
   }

   $_->{on_updated} and $_->{on_updated}->( $p->{cache} ) for @{ $p->{cbs} };
}

sub _update_property_scalar
{
   my $self = shift;
   my ( $p, $type, $how, $message ) = @_;

   if( $how == CHANGE_SET ) {
      my $value = $type->unpack_value( $message );
      $p->{cache} = $value;
      $_->{on_set} and $_->{on_set}->( $p->{cache} ) for @{ $p->{cbs} };
   }
   else {
      croak "Change type $how is not valid for a scalar property";
   }
}

sub _update_property_hash
{
   my $self = shift;
   my ( $p, $type, $how, $message ) = @_;

   if( $how == CHANGE_SET ) {
      my $value = Tangence::Type->new( dict => $type )->unpack_value( $message );
      $p->{cache} = $value;
      $_->{on_set} and $_->{on_set}->( $p->{cache} ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_ADD ) {
      my $key   = $message->unpack_str();
      my $value = $type->unpack_value( $message );
      $p->{cache}->{$key} = $value;
      $_->{on_add} and $_->{on_add}->( $key, $value ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_DEL ) {
      my $key = $message->unpack_str();
      delete $p->{cache}->{$key};
      $_->{on_del} and $_->{on_del}->( $key ) for @{ $p->{cbs} };
   }
   else {
      croak "Change type $how is not valid for a hash property";
   }
}

sub _update_property_queue
{
   my $self = shift;
   my ( $p, $type, $how, $message ) = @_;

   if( $how == CHANGE_SET ) {
      my $value = Tangence::Type->new( list => $type )->unpack_value( $message );
      $p->{cache} = $value;
      $_->{on_set} and $_->{on_set}->( $p->{cache} ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_PUSH ) {
      my @value = $message->unpack_all_sametype( $type );
      push @{ $p->{cache} }, @value;
      $_->{on_push} and $_->{on_push}->( @value ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_SHIFT ) {
      my $count = $message->unpack_int();
      splice @{ $p->{cache} }, 0, $count, ();
      $_->{on_shift} and $_->{on_shift}->( $count ) for @{ $p->{cbs} };
   }
   else {
      croak "Change type $how is not valid for a queue property";
   }
}

sub _update_property_array
{
   my $self = shift;
   my ( $p, $type, $how, $message ) = @_;

   if( $how == CHANGE_SET ) {
      my $value = Tangence::Type->new( list => $type )->unpack_value( $message );
      $p->{cache} = $value;
      $_->{on_set} and $_->{on_set}->( $p->{cache} ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_PUSH ) {
      my @value = $message->unpack_all_sametype( $type );
      push @{ $p->{cache} }, @value;
      $_->{on_push} and $_->{on_push}->( @value ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_SHIFT ) {
      my $count = $message->unpack_int();
      splice @{ $p->{cache} }, 0, $count, ();
      $_->{on_shift} and $_->{on_shift}->( $count ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_SPLICE ) {
      my $start = $message->unpack_int();
      my $count = $message->unpack_int();
      my @value = $message->unpack_all_sametype( $type );
      splice @{ $p->{cache} }, $start, $count, @value;
      $_->{on_splice} and $_->{on_splice}->( $start, $count, @value ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_MOVE ) {
      my $index = $message->unpack_int();
      my $delta = $message->unpack_int();
      # it turns out that exchanging neighbours is quicker by list assignment,
      # but other times it's generally best to use splice() to extract then
      # insert
      if( abs($delta) == 1 ) {
         @{$p->{cache}}[$index,$index+$delta] = @{$p->{cache}}[$index+$delta,$index];
      }
      else {
         my $elem = splice @{ $p->{cache} }, $index, 1, ();
         splice @{ $p->{cache} }, $index + $delta, 0, ( $elem );
      }
      $_->{on_move} and $_->{on_move}->( $index, $delta ) for @{ $p->{cbs} };
   }
   else {
      croak "Change type $how is not valid for an array property";
   }
}

sub _update_property_objset
{
   my $self = shift;
   my ( $p, $type, $how, $message ) = @_;

   if( $how == CHANGE_SET ) {
      # Comes across in a LIST. We need to map id => obj
      my $objects = Tangence::Type->new( list => $type )->unpack_value( $message );
      $p->{cache} = { map { $_->id => $_ } @$objects };
      $_->{on_set} and $_->{on_set}->( $p->{cache} ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_ADD ) {
      # Comes as object only
      my $obj = $type->unpack_value( $message );
      $p->{cache}->{$obj->id} = $obj;
      $_->{on_add} and $_->{on_add}->( $obj ) for @{ $p->{cbs} };
   }
   elsif( $how == CHANGE_DEL ) {
      # Comes as ID number only
      my $id = $message->unpack_int();
      delete $p->{cache}->{$id};
      $_->{on_del} and $_->{on_del}->( $id ) for @{ $p->{cbs} };
   }
   else {
      croak "Change type $how is not valid for an objset property";
   }
}

=head2 unwatch_property

   $proxy->unwatch_property( $property )

Removes a property watches on the given property on the server object that was
previously installed using C<watch_property>.

=cut

sub unwatch_property
{
   my $self = shift;
   my ( $property ) = @_;

   $self->can_property( $property )
      or croak "Class ".$self->classname." does not have a property $property";

   # TODO: mark cursors as destroyed and invalid
   delete $self->{props}->{$property};

   my $client = $self->{client};
   $client->request(
      request => Tangence::Message->new( $client, MSG_UNWATCH )
         ->pack_int( $self->id )
         ->pack_str( $property ),

      on_response => sub {},
   );
}

package # hide from index
   Tangence::ObjectProxy::_Cursor;
use Carp;
use Tangence::Constants;

=head1 CURSOR METHODS

The following methods are availilable on the property cursor objects returned
by the C<watch_property_with_cursor> method.

=cut

sub new
{
   my $class = shift;
   return bless [ @_ ], $class;
}

sub obj    { shift->[0] }
sub id     { shift->[1] }
sub client { shift->obj->{client} }

sub DESTROY
{
   my $self = shift;

   return unless $self->obj and my $id = $self->id and my $client = $self->client;

   $client->request(
      request => Tangence::Message->new( $client, MSG_CUSR_DESTROY )
         ->pack_int( $id ),

      on_response => sub {},
   );
}

=head2 next_forward

   ( $index, @more ) = $cursor->next_forward( $count )->get

=head2 next_backward

   ( $index, @more ) = $cursor->next_backward( $count )->get

Requests the next items from the cursor. C<next_forward> moves forwards
towards higher-numbered indices, and C<next_backward> moves backwards towards
lower-numbered indices. If C<$count> is unspecified, a default of 1 will
apply.

The returned future wil yield the index of the first element returned, and the
new elements. Note that there may be fewer elements returned than were
requested, if the end of the queue was reached. Specifically, there will be no
new elements if the cursor is already at the end.

=cut

sub next_forward
{
   my $self = shift;
   $self->_next( CUSR_FWD, @_ );
}

sub next_backward
{
   my $self = shift;
   $self->_next( CUSR_BACK, @_ );
}

sub _next
{
   my $self = shift;
   my ( $direction, $count ) = @_;

   # Detect void-context legacy uses
   defined wantarray or
      croak "->next_forward/backward in void context no longer useful - it now returns a Future";

   my $obj = $self->obj;
   my $id  = $self->id;
   my $element_type = $self->[2];

   my $client = $self->client;

   $client->request(
      request => Tangence::Message->new( $client, MSG_CUSR_NEXT )
         ->pack_int( $id )
         ->pack_int( $direction )
         ->pack_int( $count || 1 ),
   )->then( sub {
      my ( $message ) = @_;
      my $code = $message->code;

      if( $code == MSG_CUSR_RESULT ) {
         Future->done(
            $message->unpack_int(),
            $message->unpack_all_sametype( $element_type ),
         );
      }
      else {
         Future->fail( "Unexpected response code $code", tangence => );
      }
   });
}

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>

=cut

0x55AA;