The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#$Id#
package REST::Neo4p::Index;
use base 'REST::Neo4p::Entity';
use REST::Neo4p::Node;
use REST::Neo4p::Relationship;
use REST::Neo4p::Exceptions;
use Carp qw(croak carp);
use URI::Escape;
use strict;
use warnings;

BEGIN {
  $REST::Neo4p::Index::VERSION = '0.1';
}

# TODO: auto index objects ready-made

# new( 'node|relationship', $index_name )

sub new {
  my $class = shift;
  my ($index_type, $name, $config) = @_;
  # $config is for configuring an index (fulltext lucene e.g.)
  if (grep /^$name$/,qw(node relationship)) {
    my $a = $name;
    $name = $index_type;
    $index_type = $a;
  }
  unless (grep /^$index_type$/,qw(node relationship)) {
    REST::Neo4p::LocalException->throw("Index type must be either node or relationship");
  }
  my $properties = {
		    _addl_components => [$index_type],
		    name => $name
		   };
  $properties->{config} = $config if defined $config;
  return $class->SUPER::new($properties);
}

sub new_from_json_response {
  my $class = shift;
  my ($decoded_resp) = @_;
  my $obj = $class->SUPER::new_from_json_response($decoded_resp);
  $obj->_entry->{action} = $obj->_entry->{type}."_index";
  return $obj;
}

sub new_from_batch_response {
  my $class = shift;
  my ($id_token,$type) = @_;
  my $obj = $class->SUPER::new_from_batch_response($id_token);
  $obj->_entry->{type} = $type;  
  $obj->_entry->{action} = "${type}_index";
  return $obj;
}

sub remove {
  my $self = shift;
  $self->SUPER::remove($self->type);
}

# add an entity to an index
# add_entry($node, 'rating' => 'best')

sub add_entry {
  my $self = shift;
  my ($entity, $key, $value) = @_;
  unless ($self->type eq $entity->entity_type) {
    REST::Neo4p::LocalException->throw(
      "Can't add a ".$entity->entity_type." to a ".$self->type." index"
     );
  }
  unless (defined $key && defined $value) {
    REST::Neo4p::LocalException->throw("Both key and value must be supplied to add an entry");
  }
  my $agent = $REST::Neo4p::AGENT;
  my $rq = "post_".$self->_action;
  my $decoded_resp;
  eval {
    $decoded_resp = $agent->$rq([$self->name], 
				{ uri => $entity->_self_url,
				  key => $key,
				  value => uri_escape($value) }
			       );
  };
  my $e;
  if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
    # TODO : handle different classes
    $e->rethrow;
  }
  elsif ($@) {
    ref $@ ? $@->rethrow : die $@;
  }
  return 1;
}

# remove_entry(entity), remove_entry(entity, key), remove_entry(entity, key, value)
sub remove_entry {
  my $self = shift;
  my ($entity, $key, $value) = @_;
  unless ($self->type eq $entity->entity_type) {
    REST::Neo4p::LocalException->throw(
      "Can't modify a ".$self->type." index by referring to a  ".$entity->entity_type
     );
  }
  my @addl_components;
  my $agent = $REST::Neo4p::AGENT;
  my $rq = 'delete_'.$self->_action;
  if (defined $key) {
    if (defined $value) {
      @addl_components = ($key, uri_escape($value), $$entity);
    }
    else { # !defined $value
      @addl_components = ($key, $$entity);
    }
  }
  else { # !defined $key && !defined $value
    @addl_components = ($$entity);
  }
  eval {
    $agent->$rq($self->name, @addl_components);
  };
  my $e;
  if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
    # TODO : handle different classes
    $e->rethrow;
  }
  elsif ($@) {
    ref $@ ? $@->rethrow : die $@;
  }
  return 1;
}

sub find_entries {
  my $self = shift;
  if ($self->is_batch) {
    REST::Neo4p::NotSuppException->throw("find_entries method not supported in batch mode (yet)");
  }
  my ($key, $value) = @_;
  my ($query) = @_;
  my $decoded_resp;
  my $agent = $REST::Neo4p::AGENT;
  my $rq = 'get_'.$self->_action;
  if ($value) { # exact key->value match
    eval {
      $decoded_resp = $agent->$rq( $self->name,
				   $key, uri_escape($value) );
    };
    my $e;
    if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
      # TODO : handle different classes
      $e->rethrow;
    }
    elsif ($@) {
      ref $@ ? $@->rethrow : die $@;
    }
  }
  else { # a lucene query string is first arg
    # note in below: cannot pass { query => $query } to 
    # request, neo4j interface doesn't work with "form fills"
    # must add the ?query string to the request url.
    eval {
      $decoded_resp = $agent->$rq( $self->name,
				   "?query=".uri_escape($query) );
    };
    my $e;
    if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
      # TODO : handle different classes
      $e->rethrow;
    }
    elsif ($@) {
      ref $@ ? $@->rethrow : die $@;
    }
  }
  my @ret; 
  my $class = $self->type eq 'node' ? 'REST::Neo4p::Node' :
    'REST::Neo4p::Relationship';
  for (@$decoded_resp) {
    push @ret, $class->new_from_json_response($_);
  }
  return @ret;
}


# index name
sub name { ${$_[0]} }
# index type (node or relationship)
sub type { shift->_entry->{type} }
sub _action { shift->_entry->{action} }

# unused Entity methods
sub set_property { not_supported() }
sub get_property { not_supported() }
sub get_properties { not_supported() }
sub remove_property { not_supported() }

sub not_supported {
  REST::Neo4p::NotSuppException->throw( __PACKAGE__." does not support this method" );
}

=head1 NAME

REST::Neo4p::Index - Neo4j index object

=head1 SYNOPSIS

 $node_idx = REST::Neo4p::Index('node', 'my_node_index');
 $rel_idx = REST::Neo4p::Index('relationship', 'my_rel_index');
 $fulltext_idx = REST::Neo4p::Index('node', "my_ft_index,
                                    { type = 'fulltext',
                                      provider = 'lucene' });
 $node_idx->add_entry( $ShaggyNode, 'pet' => 'ScoobyDoo' );
 @returned_nodes = $node_idx->find_entries('pet' => 'ScoobyDoo');
 @returned_nodes = $node_idx->find_entries('pet:Scoob*');
 $node_idx->remove_entry( $JosieNode, 'hair' => 'red' );

=head1 DESCRIPTION

C<REST::Neo4p::Index> objects represent Neo4j node and relationship indexes.

=head1 METHODS

=over

=item new()

 $node_idx = REST::Neo4p::Index('node', 'my_node_index');
 $rel_idx = REST::Neo4p::Index('relationship', 'my_rel_index');
 $fulltext_idx = REST::Neo4p::Index('node', "my_ft_index,
                                    { type = 'fulltext',
                                      provider = 'lucene' });

Creates a new index of the type given in the first argument, with the
name given in the second argument. The optional third argument is a
hashref containing an index configuration as provided for in the Neo4j
API.

=item remove()

 $index->remove()

=item name()

 $idx_name = $index->name()

=item type()

 if ($index->type eq 'node') { $index->add_entry( $node, $key ); }

=item add_entry()

 $index->add_entry( $node, $key => $value );

=item remove_entry()

 $index->remove_entry($node);
 $index->remove_entry($node, $key);
 $index->remove_entry($node, $key => $value);

=item find_entries()

 @returned_nodes = $node_index->find_entries($key => $value);
 @returned_rels = $rel_index->find_entries('pet:Scoob*');

In the first form, an exact match is sought. In the second (i.e., when
a single string argument is passed), the argument is interpreted as a
query string and passed to the index as such. The Neo4j default is
L<Lucene|http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html>.

C<find_entries()> is not supported in batch mode.

=back

=head1 SEE ALSO

L<REST::Neo4p>, L<REST::Neo4p::Relationship>, L<REST::Neo4p::Node>.

=head1 AUTHOR

    Mark A. Jensen
    CPAN ID: MAJENSEN
    majensen -at- cpan -dot- org

=head1 LICENSE

Copyright (c) 2012 Mark A. Jensen. This program is free software; you
can redistribute it and/or modify it under the same terms as Perl
itself.

=cut

1;