# -*- coding: utf-8 -*-
#
# Copyright 2012 James Thornton (http://jamesthornton.com)
# BSD License (see LICENSE for details)
#
"""
Bulbs supports pluggable backends. This is the Neo4j Server client.

"""
import re

from bulbs.config import Config, DEBUG
from bulbs.registry import Registry
from bulbs.utils import get_logger

# specific to this client
from bulbs.json import JSONTypeSystem
from bulbs.base import Client, Response, Result
from bulbs.rest import Request, RESPONSE_HANDLERS, server_error
from bulbs.utils import json, build_path, get_file_path, urlsplit
from bulbs.groovy import GroovyScripts

# TODO: Clean up and generalize Yaml
from .cypher import Cypher, Yaml


# The default URI
NEO4J_URI = "http://localhost:7474/db/data/"

# The logger defined in Config
log = get_logger(__name__)

# Neo4j Server resource paths
# TODO: local path vars would be faster
vertex_path = "node"
edge_path = "relationship"
index_path = "index"
gremlin_path = "ext/GremlinPlugin/graphdb/execute_script"
cypher_path = "ext/CypherPlugin/graphdb/execute_query"


class Neo4jResult(Result):
    """
    Container class for a single result, not a list of results.

    :param result: The raw result.
    :type result: dict

    :param config: The graph Config object.
    :type config: Config 

    :ivar raw: The raw result.
    :ivar data: The data in the result.

    """
    def __init__(self, result, config):
        self.config = config

        # The raw result.
        self.raw = result

        # The data in the result.
        self.data = self._get_data(result)

        self.type_map = dict(node="vertex",relationship="edge")

    def get_id(self):
        """
        Returns the element ID.

        :rtype: int

        """
        uri = self.raw.get('self')
        return self._parse_id(uri)
       
    def get_type(self):
        """
        Returns the element's base type, either "vertex" or "edge".

        :rtype: str

        """
        uri = self.get_uri()
        neo4j_type = self._parse_type(uri)
        return self.type_map[neo4j_type]
        
    def get_data(self):
        """
        Returns the element's property map.

        :rtype: dict

        """
        return self.data

    def get_uri(self):
        """
        Returns the element URI.

        :rtype: str

        """
        return self.raw.get('self')
                 
    def get_outV(self):
        """
        Returns the ID of the edge's outgoing vertex (start node).

        :rtype: int

        """
        uri = self.raw.get('start')
        return self._parse_id(uri)
        
    def get_inV(self):
        """
        Returns the ID of the edge's incoming vertex (end node).

        :rtype: int

        """
        uri = self.raw.get('end')
        return self._parse_id(uri)

    def get_label(self):
        """
        Returns the edge label (relationship type).

        :rtype: str

        """
        return self.raw.get('type')

    def get_index_name(self):
        """
        Returns the index name.

        :rtype: str

        """
        return self.raw.get('name')
   
    def get_index_class(self):
        """
        Returns the index class, either "vertex" or "edge".

        :rtype: str

        """
        uri = self.raw.get('template') 
        neo4j_type = self._parse_index_type(uri)
        return self.type_map[neo4j_type]

    def get(self, attribute):
        """
        Returns the value of a client-specific attribute.
        
        :param attribute: Name of the attribute. 
        :type attribute: str

        :rtype: str

        """
        return self.raw[attribute]

    def _get_data(self, result):
        if type(result) is dict:
            return result.get('data') 

    def _parse_id(self, uri):
        """Parses the ID out of a URI."""
        if uri:
            _id = int(uri.rpartition('/')[-1])
            return _id

    def _parse_type(self, uri):
        """Parses the type ouf of a normal URI."""
        if uri:
            root_uri = uri.rpartition('/')[0]
            neo4j_type = root_uri.rpartition('/')[-1]
            return neo4j_type
    
    def _parse_index_type(self, uri):
        """Parses the type out of an index URI."""
        if uri:
            path = urlsplit(uri).path
            segments = path.split("/")
            neo4j_type = segments[-4]
            return neo4j_type


class Neo4jResponse(Response):
    """
    Container class for the server response.

    :param response: httplib2 response: (headers, content).
    :type response: tuple

    :param config: Config object.
    :type config: bulbs.config.Config

    :ivar config: Config object.
    :ivar headers: httplib2 response headers, see:
        http://httplib2.googlecode.com/hg/doc/html/libhttplib2.html
    :ivar content: A dict containing the HTTP response content.
    :ivar results: A generator of Neo4jResult objects, a single Neo4jResult object, 
        or None, depending on the number of results returned.
    :ivar total_size: The number of results returned.
    :ivar raw: Raw HTTP response. Only set when log_level is DEBUG.

    """
    result_class = Neo4jResult

    def __init__(self, response, config):
        self.config = config
        self.handle_response(response)
        self.headers = self.get_headers(response)
        self.content = self.get_content(response)
        self.results, self.total_size = self.get_results()
        self.raw = self._maybe_get_raw(response, config)

    def _maybe_get_raw(self,response, config):
        """Returns the raw response if in DEBUG mode."""
        # don't store raw response in production else you'll bloat the obj
        if config.log_level == DEBUG:
            return response

    def handle_response(self, response):
        """
        Check the server response and raise exception if needed.
        
        :param response: httplib2 response: (headers, content).
        :type response: tuple

        :rtype: None

        """
        headers, content = response

        # Temporary hack to catch Gremlin Plugin exceptions that return 200 status
        # See https://github.com/neo4j/community/issues/343
        # Example: '"java.lang.IllegalArgumentException: Unknown property type on..."'
        if re.search(b"^\"java.(.*).Exception:", content):
            # raise error...
            server_error(response)
        
        response_handler = RESPONSE_HANDLERS.get(headers.status)
        response_handler(response)

    def get_headers(self, response):
        """
        Returns a dict containing the headers from the response.

        :param response: httplib2 response: (headers, content).
        :type response: tuple
        
        :rtype: httplib2.Response

        """
        # response is a tuple containing (headers, content)
        # headers is an httplib2 Response object, content is a string
        # see http://httplib2.googlecode.com/hg/doc/html/libhttplib2.html
        headers, content = response
        return headers

    def get_content(self, response):
        """
        Returns a dict containing the content from the response.
        
        :param response: httplib2 response: (headers, content).
        :type response: tuple
        
        :rtype: dict or None

        """
        # content is a JSON string
        headers, content = response

        # Neo4jServer returns empty content on update
        if content:
            content = json.loads(content.decode('utf-8'))
            return content

    def get_results(self):
        """
        Returns the results contained in the response.

        :return:  A tuple containing two items: 1. Either a generator of Neo4jResult objects, 
                  a single Neo4jResult object, or None, depending on the number of results 
                  returned; 2. An int representing the number results returned.
        :rtype: tuple

        """
        if type(self.content) == list:
            results = (self.result_class(result, self.config) for result in self.content)
            total_size = len(self.content)
        elif self.content and self.content != "null":
            # Checking for self.content.get('data') won't work b/c the data value
            # isn't returned for edges with no properties;
            # and self.content != "null": Yep, the null thing is sort of a hack. 
            # Neo4j returns "null" if Gremlin scripts don't return anything.
            results = self.result_class(self.content, self.config)
            total_size = 1
        else:
            results = None
            total_size = 0
        return results, total_size

    def _set_index_name(self, index_name):
        """Sets the index name to the raw result."""
        # this is pretty much a hack becuase neo4j doesn't include the index name in response
        self.results.raw['name'] = index_name
        

class Neo4jRequest(Request):
    """Makes HTTP requests to Neo4j Server and returns a Neo4jResponse.""" 
    
    response_class = Neo4jResponse


class Neo4jClient(Client):
    """
    Low-level client that sends a request to Neo4j Server and returns a response.

    :param config: Optional Config object. Defaults to default Config.
    :type config: bulbs.config.Config

    :ivar config: Config object.
    :ivar registry: Registry object.
    :ivar scripts: GroovyScripts object.  
    :ivar type_system: JSONTypeSystem object.
    :ivar request: Neo4jRequest object.

    Example:

    >>> from bulbs.neo4jserver import Neo4jClient
    >>> client = Neo4jClient()
    >>> response = client.get_all_vertices()
    >>> result = response.results.next()

    """ 
    #: Default URI for the database.
    default_uri = NEO4J_URI

    #: Request class for the Client.
    request_class = Neo4jRequest


    def __init__(self, config=None):
        self.config = config or Config(self.default_uri)
        self.registry = Registry(self.config)
        self.type_system = JSONTypeSystem()
        self.request = self.request_class(self.config, self.type_system.content_type)

        # Neo4j supports Gremlin so include the Gremlin-Groovy script library
        self.scripts = GroovyScripts(self.config)
        
        # Also include the Neo4j Server-specific Gremlin-Groovy scripts
        scripts_file = get_file_path(__file__, "gremlin.groovy")
        self.scripts.update(scripts_file)

        # Add it to the registry. This allows you to have more than one scripts namespace.
        self.registry.add_scripts("gremlin", self.scripts)
        

    # Gremlin

    def gremlin(self, script, params=None): 
        """
        Executes a Gremlin script and returns the Response.

        :param script: Gremlin script to execute.
        :type script: str

        :param params: Param bindings for the script.
        :type params: dict

        :rtype: Neo4jResponse

        """
        path = gremlin_path
        params = dict(script=script, params=params)
        return self.request.post(path, params)

    # Cypher

    def cypher(self, query, params=None):
        """
        Executes a Cypher query and returns the Response.

        :param query: Cypher query to execute.
        :type query: str

        :param params: Param bindings for the query.
        :type params: dict

        :rtype: Neo4jResponse

        """
        path = cypher_path
        params = dict(query=query,params=params)
        resp = self.request.post(path, params)

        # Cypher data hack
        resp.total_size = len(resp.results.data)
        resp.results = (Neo4jResult(result[0], self.config) for result in resp.results.data)
        return resp

    # Vertex Proxy

    def create_vertex(self, data):
        """
        Creates a vertex and returns the Response.

        :param data: Property data.
        :type data: dict

        :rtype: Neo4jResponse

        """
        if self.config.autoindex is True:
            index_name = self.config.vertex_index
            return self.create_indexed_vertex(data, index_name, keys=None)
        path = vertex_path
        params = self._remove_null_values(data)
        return self.request.post(path, params)

    def get_vertex(self, _id):
        """
        Gets the vertex with the _id and returns the Response.

        :param data: Vertex ID.
        :type data: int

        :rtype: Neo4jResponse

        """
        path = build_path(vertex_path, _id)
        params = None
        return self.request.get(path, params)
        
    def get_all_vertices(self):
        """
        Returns a Response containing all the vertices in the Graph.

        :rtype: Neo4jResponse

        """
        script = self.scripts.get("get_vertices")
        params = None
        return self.gremlin(script, params)

    def update_vertex(self, _id, data):
        """
        Updates the vertex with the _id and returns the Response.

        :param _id: Vertex ID.
        :type _id: dict

        :param data: Property data.
        :type data: dict

        :rtype: Neo4jResponse

        """
        if self.config.autoindex is True:
            index_name = self.config.vertex_index
            return self.update_indexed_vertex(_id,data,index_name,keys=None)
        path = self._build_vertex_path(_id,"properties")
        params = self._remove_null_values(data)
        return self.request.put(path, params)

    def delete_vertex(self, _id):
        """
        Deletes a vertex with the _id and returns the Response.

        :param _id: Vertex ID.
        :type _id: dict

        :rtype: Neo4jResponse

        """
        script = self.scripts.get("delete_vertex")
        params = dict(_id=_id)
        return self.gremlin(script,params)
        
    # Edge Proxy

    def create_edge(self, outV, label, inV, data=None): 
        """
        Creates a edge and returns the Response.
        
        :param outV: Outgoing vertex ID.
        :type outV: int

        :param label: Edge label.
        :type label: str

        :param inV: Incoming vertex ID.
        :type inV: int

        :param data: Property data.
        :type data: dict or None

        :rtype: Neo4jResponse

        """
        if self.config.autoindex is True:
            index_name = self.config.edge_index
            return self.create_indexed_edge(outV,label,inV,data,index_name,keys=None)
        data = self._remove_null_values(data)
        inV_uri = self._build_vertex_uri(inV)
        path = build_path(vertex_path, outV, "relationships")
        params = {'to':inV_uri, 'type':label, 'data':data}
        return self.request.post(path, params)

    def get_edge(self, _id):
        """
        Gets the edge with the _id and returns the Response.

        :param data: Edge ID.
        :type data: int

        :rtype: Neo4jResponse

        """
        path = build_path(edge_path,_id)
        params = None
        return self.request.get(path, params)
        
    def get_all_edges(self):
        """
        Returns a Response containing all the edges in the Graph.

        :rtype: Neo4jResponse

        """
        script = self.scripts.get("get_edges")
        params = None
        return self.gremlin(script, params)

    def update_edge(self, _id, data):
        """
        Updates the edge with the _id and returns the Response.

        :param _id: Edge ID.
        :type _id: dict

        :param data: Property data.
        :type data: dict

        :rtype: Neo4jResponse

        """
        if self.config.autoindex is True:
            index_name = self.config.edge_index
            return self.update_indexed_edge(_id,data,index_name,keys=None)
        path = build_path(edge_path,_id,"properties")
        params = self._remove_null_values(data)
        return self.request.put(path, params)

    def delete_edge(self, _id):
        """
        Deletes a edge with the _id and returns the Response.

        :param _id: Edge ID.
        :type _id: dict

        :rtype: Neo4jResponse

        """
        path = build_path(edge_path,_id)
        params = None
        return self.request.delete(path, params)

    # Vertex Container

    def outE(self, _id, label=None, start=None, limit=None):
        """
        Returns the outgoing edges of the vertex.

        :param _id: Vertex ID.
        :type _id: dict

        :param label: Optional edge label. Defaults to None.
        :type label: str

        :rtype: Neo4jResponse
        
        """
        script = self.scripts.get('outE')
        params = dict(_id=_id,label=label,start=start,limit=limit)
        return self.gremlin(script,params)

    def inE(self, _id, label=None, start=None, limit=None):
        """
        Returns the incoming edges of the vertex.

        :param _id: Vertex ID.
        :type _id: dict

        :param label: Optional edge label. Defaults to None.
        :type label: str

        :rtype: Neo4jResponse

        """
        script = self.scripts.get('inE')
        params = dict(_id=_id,label=label,start=start,limit=limit)
        return self.gremlin(script,params)

    def bothE(self, _id, label=None, start=None, limit=None):
        """
        Returns the incoming and outgoing edges of the vertex.

        :param _id: Vertex ID.
        :type _id: dict

        :param label: Optional edge label. Defaults to None.
        :type label: str

        :rtype: Neo4jResponse
        
        """
        script = self.scripts.get('bothE')
        params = dict(_id=_id,label=label,start=start,limit=limit)
        return self.gremlin(script,params)

    def outV(self, _id, label=None, start=None, limit=None):
        """
        Returns the out-adjacent vertices of the vertex.

        :param _id: Vertex ID.
        :type _id: dict

        :param label: Optional edge label. Defaults to None.
        :type label: str

        :rtype: Neo4jResponse

        """
        script = self.scripts.get('outV')
        params = dict(_id=_id,label=label,start=start,limit=limit)
        return self.gremlin(script,params)
        
    def inV(self, _id, label=None, start=None, limit=None):
        """
        Returns the in-adjacent vertices of the vertex.

        :param _id: Vertex ID.
        :type _id: dict

        :param label: Optional edge label. Defaults to None.
        :type label: str

        :rtype: Neo4jResponse

        """
        script = self.scripts.get('inV')
        params = dict(_id=_id,label=label,start=start,limit=limit)
        return self.gremlin(script,params)
        
    def bothV(self, _id, label=None, start=None, limit=None):
        """
        Returns the incoming- and outgoing-adjacent vertices of the vertex.

        :param _id: Vertex ID.
        :type _id: dict

        :param label: Optional edge label. Defaults to None.
        :type label: str

        :rtype: Neo4jResponse

        """
        script = self.scripts.get('bothV')
        params = dict(_id=_id,label=label,start=start,limit=limit)
        return self.gremlin(script,params)

    #: Index Proxy - Vertex

    def create_vertex_index(self, index_name, *args, **kwds):
        """
        Creates a vertex index with the specified params.

        :param index_name: Name of the index to create.
        :type index_name: str

        :rtype: Neo4jResponse

        """
        default_config = {'type': "exact", 'provider': "lucene"}
        index_config = kwds.pop("index_config", default_config) 
        path = build_path(index_path, vertex_path)
        params = dict(name=index_name, config=index_config)
        resp = self.request.post(path, params)
        resp._set_index_name(index_name)        
        return resp

    def get_vertex_indices(self):
        """
        Returns all the vertex indices.

        :rtype: Neo4jResponse

        """
        path = build_path(index_path,vertex_path)
        params = None
        return self.request.get(path, params)

    def get_vertex_index(self, index_name):
        """
        Returns the vertex index with the index_name.

        :param index_name: Name of the index.
        :type index_name: str

        :rtype: Neo4jResponse

        """
        resp = self.get_vertex_indices()
        resp.results = self._get_index_results(index_name,resp)
        if resp.results:
            resp._set_index_name(index_name)
        return resp

    def get_or_create_vertex_index(self, index_name, *args, **kwds):
        """
        Get a Vertex Index or create it if it doesn't exist.

        :param index_name: Index name.
        :type index_name: str

        :param index_config: Index configuration.
        :type index_config: dict

        :rtype: bulbs.neo4jserver.index.Index

        """ 
        # Neo4j's create index endpoint returns the index if it already exists
        return self.create_vertex_index(index_name, *args, **kwds)

    def delete_vertex_index(self, index_name): 
        """
        Deletes the vertex index with the index_name.

        :param index_name: Name of the index.
        :type index_name: str

        :rtype: Neo4jResponse

        """
        path = build_path(index_path, vertex_path, index_name)
        params = None
        return self.request.delete(path, params)

    # Index Proxy - Edge

    def create_edge_index(self, index_name, *args, **kwds):
        """
        Creates a edge index with the specified params.

        :param index_name: Name of the index.
        :type index_name: str

        :rtype: Neo4jResponse

        """
        default_config = {'type': "exact", 'provider': "lucene"}
        index_config = kwds.pop("index_config", default_config) 
        path = build_path(index_path, edge_path)
        params = dict(name=index_name, config=index_config)
        resp = self.request.post(path, params)
        resp._set_index_name(index_name)
        return resp

    def get_edge_indices(self):
        """
        Returns a dict of all the vertex indices.

        :rtype: Neo4jResponse

        """
        path = build_path(index_path,edge_path)
        params = None
        return self.request.get(path, params)

    def get_edge_index(self, index_name):
        """
        Returns the edge index with the index_name.

        :param index_name: Name of the index.
        :type index_name: str

        :rtype: Neo4jResponse

        """
        resp = self.get_edge_indices()
        resp.results = self._get_index_results(index_name, resp)
        if resp.results:
            resp._set_index_name(index_name)
        return resp

    def get_or_create_edge_index(self, index_name, *args, **kwds):
        """
        Get a Edge Index or create it if it doesn't exist.

        :param index_name: Index name.
        :type index_name: str

        :param index_config: Index configuration.
        :type index_config: dict

        :rtype: bulbs.neo4jserver.index.Index

        """ 
        # Neo4j's create index endpoint returns the index if it already exists
        return self.create_edge_index(index_name, *args, **kwds)

    def delete_edge_index(self, index_name):
        """
        Deletes the edge index with the index_name.

        :param index_name: Name of the index.
        :type index_name: str

        :rtype: Neo4jResponse

        """
        path = build_path(index_path, edge_path, index_name)
        params = None
        return self.request.delete(path, params)

    # Index Container - Vertex

    def put_vertex(self, index_name, key, value, _id):
        """
        Adds a vertex to the index with the index_name.

        :param index_name: Name of the index.
        :type index_name: str

        :param key: Name of the key.
        :type key: str

        :param value: Value of the key.
        :type value: str

        :param _id: Vertex ID
        :type _id: int
        
        :rtype: Neo4jResponse

        """
        uri = "%s/%s/%d" % (self.config.root_uri, vertex_path, _id)
        path = build_path(index_path, vertex_path, index_name)
        params = dict(key=key, value=value, uri=uri)
        return self.request.post(path, params)

    def lookup_vertex(self, index_name, key, value):
        """
        Returns the vertices indexed with the key and value.

        :param index_name: Name of the index.
        :type index_name: str

        :param key: Name of the key.
        :type key: str

        :param value: Value of the key.
        :type value: str

        :rtype: Neo4jResponse

        """
        # converting all values to strings because that's how they're stored
        path = build_path(index_path, vertex_path, index_name, key, value)
        params = None
        return self.request.get(path, params)

    def query_vertex(self, index_name, query):
        """
        Queries the index and returns the Response.

        :param index_name: Name of the index.
        :type index_name: str

        :param query: Lucene query string
        :type query: str

        :rtype: Neo4jResponse

        """
        path = build_path(index_path, vertex_path, index_name)
        params = dict(query=query)
        return self.request.get(path, params)

    def remove_vertex(self, index_name, _id, key=None, value=None):
        """
        Removes a vertex from the index and returns the Response.

        :param index_name: Name of the index.
        :type index_name: str

        :param key: Optional. Name of the key.
        :type key: str

        :param value: Optional. Value of the key.
        :type value: str        

        :rtype: Neo4jResponse

        """
        path = build_path(index_path, vertex_path, index_name ,key, value, _id)
        params = None
        return self.request.delete(path, params)
        
    # Index Container - Edge

    def put_edge(self, index_name, key, value, _id):
        """
        Adds an edge to the index and returns the Response.

        :param index_name: Name of the index.
        :type index_name: str

        :param key: Name of the key.
        :type key: str

        :param value: Value of the key.
        :type value: str

        :param _id: Edge ID
        :type _id: int
        
        :rtype: Neo4jResponse

        """
        uri = "%s/%s/%d" % (self.config.root_uri,edge_path,_id)
        path = build_path(index_path, edge_path, index_name)
        params = dict(key=key,value=value,uri=uri)
        return self.request.post(path, params)

    def lookup_edge(self, index_name, key, value):
        """
        Looks up an edge in the index and returns the Response.

        :param index_name: Name of the index.
        :type index_name: str

        :param key: Name of the key.
        :type key: str

        :param value: Value of the key.
        :type value: str

        :rtype: Neo4jResponse

        """
        # converting all values to strings because that's how they're stored
        path = build_path(index_path, edge_path, index_name, key, value)
        params = None
        return self.request.get(path, params)

    def query_edge(self, index_name, query):
        """
        Queries the index and returns the Response.

        :param index_name: Name of the index.
        :type index_name: str

        :param query: Lucene query string
        :type query: str

        :rtype: Neo4jResponse

        """
        path = build_path(index_path, edge_path, index_name)
        params = dict(query=query)
        return self.request.get(path, params)

    def remove_edge(self, index_name, _id, key=None, value=None):
        """
        Removes an edge from the index and returns the Response.
        
        :param index_name: Name of the index.
        :type index_name: str

        :param _id: Edge ID
        :type _id: int

        :param key: Optional. Name of the key.
        :type key: str

        :param value: Optional. Value of the key.
        :type value: str        

        :rtype: Neo4jResponse

        """
        path = build_path(index_path, edge_path, index_name, key, value, _id)
        params = None
        return self.request.delete(path, params)

    # Model Proxy - Vertex

    def create_indexed_vertex(self, data, index_name, keys=None):
        """
        Creates a vertex, indexes it, and returns the Response.

        :param data: Property data.
        :type data: dict

        :param index_name: Name of the index.
        :type index_name: str

        :param keys: Property keys to index.
        :type keys: list

        :rtype: Neo4jResponse

        """
        data = self._remove_null_values(data)
        params = dict(data=data,index_name=index_name,keys=keys)
        script = self.scripts.get("create_indexed_vertex")
        return self.gremlin(script,params)
    
    def update_indexed_vertex(self, _id, data, index_name, keys=None):
        """
        Updates an indexed vertex and returns the Response.

        :param index_name: Name of the index.
        :type index_name: str

        :param data: Property data.
        :type data: dict

        :param index_name: Name of the index.
        :type index_name: str

        :param keys: Property keys to index.
        :type keys: list

        :rtype: Neo4jResponse

        """
        data = self._remove_null_values(data)
        params = dict(_id=_id,data=data,index_name=index_name,keys=keys)
        script = self.scripts.get("update_indexed_vertex")
        return self.gremlin(script,params)

    # Model Proxy - Edge

    def create_indexed_edge(self, outV, label, inV, data, index_name, keys=None):
        """
        Creates a edge, indexes it, and returns the Response.

        :param outV: Outgoing vertex ID.
        :type outV: int

        :param label: Edge label.
        :type label: str

        :param inV: Incoming vertex ID.
        :type inV: int

        :param data: Property data.
        :type data: dict

        :param index_name: Name of the index.
        :type index_name: str

        :param keys: Property keys to index. Defaults to None (indexes all properties).
        :type keys: list

        :rtype: Neo4jResponse

        """
        data = self._remove_null_values(data)
        edge_params = dict(outV=outV,label=label,inV=inV,label_var=self.config.label_var)
        params = dict(data=data,index_name=index_name,keys=keys)
        params.update(edge_params)
        script = self.scripts.get("create_indexed_edge")
        return self.gremlin(script,params)


    def update_indexed_edge(self, _id, data, index_name, keys=None):
        """
        Updates an indexed edge and returns the Response.

        :param _id: Edge ID.
        :type _id: int

        :param data: Property data.
        :type data: dict

        :param index_name: Name of the index.
        :type index_name: str

        :param keys: Property keys to index. Defaults to None (indexes all properties).
        :type keys: list

        :rtype: Neo4jResponse

        """
        data = self._remove_null_values(data)
        params = dict(_id=_id,data=data,index_name=index_name,keys=keys)
        script = self.scripts.get("update_indexed_edge")
        return self.gremlin(script,params)


    # Metadata

    def set_metadata(self, key, value):
        """
        Sets the metadata key to the supplied value.

        :param key: Metadata key
        :type key: str

        :param value: Metadata value.
        :type value: str, int, or list

        :rtype: Neo4jResponse
        
        """
        script = self.scripts.get("set_metadata")
        params = dict(key=key, value=value)
        return self.gremlin(script, params)

    def get_metadata(self, key, default_value=None):
        """
        Returns the value of metadata for the key.

        :param key: Metadata key
        :type key: str

        :param default_value: Default value to return if the key is not found.
        :type default_value: str, int, or list

        :rtype: Neo4jResponse
        
        """
        script = self.scripts.get("get_metadata")
        params = dict(key=key, default_value=default_value)
        return self.gremlin(script, params)

    def remove_metadata(self, key):
        """
        Removes the metadata key and value.

        :param key: Metadata key
        :type key: str

        :rtype: Neo4jResponse
        
        """
        script = self.scripts.get("remove_metadata")
        params = dict(key=key)
        return self.gremlin(script, params)


    # Private 

    def _remove_null_values(self,data):
        """Removes null property values because they aren't valid in Neo4j."""
        # Neo4j Server uses PUTs to overwrite all properties so no need
        # to worry about deleting props that are being set to null.
        data = data or {}
        clean_data = [(k, data[k]) for k in data if data[k] is not None] # Python 3
        return dict(clean_data)

    def _get_index_results(self, index_name, resp):
        """
        Returns the index from a dict of indicies.

        """
        if resp.content and index_name in resp.content:
            result = resp.content[index_name]
            return Neo4jResult(result, self.config)


    # Batch related
    def _placeholder(self,_id):
        pattern = "^{.*}$"
        match = re.search(pattern,str(_id))
        if match:
            placeholder = match.group()
            return placeholder

    def _build_vertex_path(self,_id,*args):
        # if the _id is a placeholder, return the placeholder;
        # othewise, return a normal vertex path
        placeholder = self._placeholder(_id) 
        if placeholder:
            segments = [placeholder]
        else:
            segments = [vertex_path,_id]
        segments = segments + list(args)
        return build_path(*segments)
        
    def _build_vertex_uri(self,_id,*args):
        placeholder = self._placeholder(_id) 
        if placeholder:
            return placeholder
        root_uri = self.config.root_uri.rstrip("/")
        segments = [vertex_path, _id] + list(args)
        path = build_path(*segments)
        uri = "%s/%s" % (root_uri, path)
        return uri

    def _build_edge_path(self,_id):
        # if the _id is a placeholder, return the placeholder;
        # othewise, return a normal edge path
        return self._placeholder(_id) or build_path(edge_path,_id)

    def _build_edge_uri(self,_id):
        pass

