
import json

from neo4j.cursor import Cursor

try:
    from http import client as http
    from urllib.parse import urlparse
    StandardError = Exception
except:
    import httplib as http
    from urlparse import urlparse
    from exceptions import StandardError

TX_ENDPOINT = "/db/data/transaction"

def neo_code_to_error_class(code):
    if code.startswith('Neo.ClientError.Schema'):
        return Connection.IntegrityError
    elif code.startswith('Neo.ClientError'):
        return Connection.ProgrammingError
    return Connection.InternalError

def default_error_handler(connection, cursor, errorclass, errorvalue):
    if errorclass != Connection.Warning:
        raise errorclass(errorvalue)

class Connection(object):

    class Error(StandardError):
        pass

    class Warning(StandardError):
        pass

    class InterfaceError(Error):
        pass

    class DatabaseError(Error):
        pass

    class InternalError(DatabaseError):
        pass

    class OperationalError(DatabaseError):
        pass

    class ProgrammingError(DatabaseError):
        pass

    class IntegrityError(DatabaseError):
        pass

    class DataError(DatabaseError):
        pass

    class NotSupportedError(DatabaseError):
        pass    

    def __init__(self, dbUri):
        self.errorhandler = default_error_handler
        self._http = http.HTTPConnection(urlparse(dbUri).netloc)
        self._tx  = TX_ENDPOINT
        self._messages = []
        self._cursors = set()
        self._cursor_ids = 0

    def commit(self):
        self._messages = []
        pending = self._gather_pending()
        
        if self._tx != TX_ENDPOINT or len(pending) > 0:
            payload = None
            if len(pending) > 0:
                payload = json.dumps( {'statements':[ { 'statement':s, 'parameters':p } for (s, p) in pending ]} )
            self._http.request("POST", self._tx + "/commit", payload)
            self._tx = TX_ENDPOINT
            response = self._deserialize( self._http.getresponse() )
            self._handle_errors(response, self, None)

    def rollback(self):
        self._messages = []
        self._gather_pending() # Just used to clear all pending requests
        if self._tx != TX_ENDPOINT:
            self._http.request("DELETE", self._tx )
            self._tx = TX_ENDPOINT
            response = self._deserialize( self._http.getresponse() )
            self._handle_errors(response, self, None)

    def cursor(self):
        self._messages = []
        cursor = Cursor( self._next_cursor_id(), self, self._execute )
        self._cursors.add(cursor)
        return cursor

    def close(self):
        self._messages = []
        if hasattr(self, '_http') and self._http != None:
            self._http.close()
            self._http = None

    def __del__(self):
        self.close()

    @property
    def messages(self):
        return self._messages

    def _next_cursor_id(self):
        self._cursor_ids += 1
        return self._cursor_ids

    def _gather_pending(self):
        pending = []
        for cursor in self._cursors:
            if len(cursor._pending) > 0:
                pending.extend(cursor._pending)
                cursor._pending = []
        return pending

    def _handle_errors(self, response, owner, cursor):
        for error in response['errors']:
            ErrorClass = neo_code_to_error_class(error['code'])
            error_value = error['code'] + ": " + str(error['message'])
            owner._messages.append( ( ErrorClass, error_value))
            owner.errorhandler(self, cursor, ErrorClass, error_value)

    def _execute( self, cursor, statements ):
        '''
        Executes a list of statements, returning an iterator of results sets. Each 
        statement should be a tuple of (statement, params).
        '''
        payload = [ { 'statement':s, 'parameters':p } for (s, p) in statements ]
        self._http.request("POST", self._tx, json.dumps( {'statements':payload} ) )
        
        http_response = self._http.getresponse()

        if self._tx == TX_ENDPOINT:
            self._tx = http_response.getheader('Location')

        response = self._deserialize( http_response )
        self._handle_errors(response, cursor, cursor)

        return response['results'][-1]

    def _deserialize(self, response):
        return json.loads(response.read().decode('utf-8'))