import socket, time, warnings
###
from . import rtypes
from .rexceptions import RConnectionRefused, REvalError, PyRserveClosed
from .rserializer import rEval, rAssign
from .rparser import rparse

RSERVEPORT = 6311
DEBUG = False


def connect(host='', port=RSERVEPORT, atomicArray=False, arrayOrder='C'):
    """Open a connection to an Rserve instance
    Params:
    - host: provide hostname where Rserve runs, or leave as empty string to connect to localhost
    - port: Rserve port number, defaults to 6311
    - atomicArray: If True: when a result from an Rserve call is an array with a single element that single element
                   is returned. Otherwise the array is returned unmodified. Default: True
    - arrayOrder:  The order in which data in multi-dimensional arrays is returned.
                   Provide 'C' for c-order, F for fortran. Default: 'C'
    """
#    if host in (None, ''):
#        # On Win32 it seems that passing an empty string as 'localhost' does not work
#        # So just to be sure provide the full local hostname if None or '' were passed.
#        host = socket.gethostname()
    assert port is not None, 'port number must be given'
    assert arrayOrder in ('C', 'F'), 'array order must be either "C" for c-order or "F" for fortran order'
    return RConnector(host, port, atomicArray, arrayOrder)



def checkIfClosed(func):
    def decoCheckIfClosed(self, *args, **kw):
        if self.isClosed:
            raise PyRserveClosed('Connection to Rserve already closed')
        return func(self, *args, **kw)
    return decoCheckIfClosed
    


class RConnector(object):
    """Provide a network connector to an Rserve process"""
    def __init__(self, host, port, atomicArray, arrayOrder):
        self.host = host
        self.port = port
        self.atomicArray = atomicArray
        self.arrayOrder  = arrayOrder   # must be either 'C' or 'F' (for fortran)
        self.connect()
        self.r = RNameSpace(self)
        self.ref = RNameSpaceReference(self)
        
    def __repr__(self):
        txt = 'Closed handle' if self.isClosed else 'Handle'
        return '<%s to Rserve on %s:%s>' % (txt, self.host or 'localhost', self.port)

    @property
    def isClosed(self):
        return self.__closed

    def connect(self):
        self.sock = socket.socket()
        try:
            self.sock.connect((self.host, self.port))
        except socket.error:
            raise RConnectionRefused('Connection denied, server not reachable or not accepting connections')
        time.sleep(0.2)
        hdr = self.sock.recv(1024)
        self.__closed = False
        if DEBUG:
            print('received hdr %s from rserve' % hdr)
        # make sure we are really connected with rserv
        assert hdr.startswith(b'Rsrv01'), 'Protocol error with Rserv, obtained invalid header string'
        # TODO: possibly also do version checking here to make sure we understand the protocol...

    @checkIfClosed
    def close(self):
        """Close network connection to rserve"""
        self.sock.close()
        self.__closed = True
        
    def _reval(self, aString):
        rEval(aString, fp=self.sock)
        
    @checkIfClosed
    def eval(self, aString, **kwargs):
        '@brief Evaluate a string expression through Rserve and return the result transformed into python objects'
        if not type(aString in rtypes.STRING_TYPES):
            raise TypeError('Only string evaluation is allowed')
        self._reval(aString)
        if DEBUG:
            # Read entire data into memory en bloque, it's easier to debug
            src = self._receive()
            print('Raw response:', repr(src))
        atomicArray = kwargs.get('atomicArray', self.atomicArray)
        arrayOrder = kwargs.get('arrayOrder', self.arrayOrder)
        try:
            return rparse(self.sock, atomicArray=atomicArray, arrayOrder=arrayOrder)
        except REvalError:
            # R has reported an evaulation error, so let's obtain a descriptive explanation
            # about why the error has occurred. R allows to retrieve the error message
            # of the last exception via a built-in function called 'geterrmessage()'.
            errorMsg = self.eval('geterrmessage()').strip()
            raise REvalError(errorMsg)
            
    @checkIfClosed
    def _receive(self):
        '@brief Receive the result from a previous call to rserve.'
        raw = self.sock.recv(rtypes.SOCKET_BLOCK_SIZE)
        d = [raw]
        while len(raw) == rtypes.SOCKET_BLOCK_SIZE:
            raw = self.sock.recv(rtypes.SOCKET_BLOCK_SIZE)
            d.append(raw)
        return ''.join(d)

#    @checkIfClosed
#    def _raw(self, *args, **kw):
#        self.send(*args)
#        return self.receive()

    @checkIfClosed
    def setRexp(self, name, o):
        '@brief Convert a python object into an RExp and bind it to a variable called "name" in the R namespace'
        rAssign(name, o, self.sock)
        # Rserv sends an emtpy confirmation message, or error message in case of an error.
        # rparse() will raise an Exception in the latter case.
        rparse(self.sock, atomicArray=self.atomicArray, arrayOrder=self.arrayOrder)

    @checkIfClosed
    def getRexp(self, name):
        '@brief Retrieve a Rexp stored in a variable called "name"'
        return self.eval(name)
        
    @checkIfClosed
    def callFunc(self, name, *args, **kw):
        '''
        @brief  make a call to a function "name" through Rserve
        @detail positional and keyword arguments are first stored as local variables in 
                the R namespace and then delivered to the function.
        @result Whatever the result of the called function is.
        '''
        if name == 'rm':
            # SPECIAL HANDLING FOR "rm()":
            # Calling "rm" with real values instead of reference to values works, however
            # it doesn't produce the desired effect (it only removes our temporaily created
            # variables). To avoid confusion for the users a check is applied here to make
            # sure that "args" only contains variable or function references (proxies) and
            # NOT values!
            assert [x for x in args if not isinstance(x, RBaseProxy)] == (), \
                   'Only references to variables or functions allowed for "rm()"'
        
        argNames = []
        for idx, arg in enumerate(args):
            if isinstance(arg, RBaseProxy):
                argName = arg.name
            else:
                argName = 'arg_%d_' % idx
                self.setRexp(argName, arg)
            argNames.append(argName)
        for key, value in kw.items():
            if isinstance(value, RBaseProxy):
                argName = value.name
            else:
                argName = 'kwarg_%s_' % key
                self.setRexp(argName, value)
            argNames.append('%s=%s' % (key, argName))
        return self.eval(name+'(%s)' % ', '.join(argNames))

    @checkIfClosed
    def assign(self, aDict):
        '@brief Assign all items of the dictionary to the default R namespace'
        for k, v in aDict.items():
            self.setRexp(k, v)

    @checkIfClosed
    def isFunction(self, name):
        '@Checks whether given name references an existing function in R'
        return self.eval('is.function(%s)' % name)


class RNameSpace(object):
    'An instance of this class serves as access point to the default namesspace of an Rserve connection'
    def __init__(self, rconn):
        self.__dict__['_rconn'] = rconn
        
    def __setattr__(self, name, o):
        '@brief Assign an rExpr to a variable called "name"'
        self._rconn.setRexp(name, o)

    def __getattr__(self, name):
        '@brief Either retrieve Rexp stored in a variable called "name" or make call to function called "name"'
        realname = name[1:] if name.startswith('_') else name
        try:
            isFunction = self._rconn.isFunction(realname)
        except:
            # an error is only raised if neither such a function or variable exists at all!
            raise NameError('no such variable or function "%s" defined in Rserve' % realname)
        if isFunction:
            return RFuncProxy(realname, self._rconn)
        elif name.startswith('_'):
            return RVarProxy(realname, self._rconn)
        else:
            return self._rconn.getRexp(name)

    def __call__(self, aString, **kwargs):
        return self._rconn.eval(aString, **kwargs)


class RNameSpaceReference(object):
    'Provides references to R objects (a proxy), NOT directly to their values'
    def __init__(self, rconn):
        self.__dict__['_rconn'] = rconn
        
    def __getattr__(self, name):
        '@brief Returns either a reference proxy to a variable to to a function'
        try:
            isFunction = self._rconn.isFunction(name)
        except:
            # an error is only raised if neither such a function or variable exists at all!
            raise NameError('no such variable or function "%s" defined in Rserve' % name)
        if isFunction:
            return RFuncProxy(name, self._rconn)
        else:
            return RVarProxy(name, self._rconn)



class RBaseProxy(object):
    'Proxy for a reference to a variable or function in R. Do not use this directly, only its subclasses'
    def __init__(self, name, rconn):
        self._name = name
        self._rconn = rconn

    

class RVarProxy(RBaseProxy):
    'Proxy for a reference to a variable in R'
    def __repr__(self):
        return '<RVarProxy to variable "%s">' % self._name

    def value(self):
        return self._rconn.getRexp(self._name)
        


class RFuncProxy(RBaseProxy):
    'Proxy for function calls to Rserve'
    def __repr__(self):
        return '<RFuncProxy to function "%s">' % self._name
        
    def __call__(self, *args, **kw):
        return self._rconn.callFunc(self._name, *args, **kw)

    @property
    def __doc__(self):
        try:
            d = self._rconn.eval('readLines(as.character(help(%s)))' % self._name)
        except REvalError:
            # probably no help available, unfortunately there is no specific code for this...
            return None
        helpstring = '\n'.join(d)
        helpstring = helpstring.replace('_\x08', '')
        return helpstring

    def help(self):
        print(self.__doc__)

    def __getattr__(self, name):
        """Allow for nested name space calls, e.g. 't.test' """
        if name == '__name__':
            # this is useful for py.test which does some code inspection during runtime
            return self._name

        concatName = "%s.%s" % (self._name, name)
        try:
            isFunction = self._rconn.isFunction(concatName)
        except:
            # an error is only raised if neither such a function or variable exists at all!
            raise NameError('no such variable or function "%s" defined in Rserve' % concatName)
        return RFuncProxy(concatName, self._rconn)


if __name__ == '__main__':
    import os, readline, atexit
    # Setup history and readline facility for remote q:
    histfile = os.path.join(os.environ['HOME'], '.pyhistory')
    try:
        readline.read_history_file(histfile)
    except IOError:
        pass
    atexit.register(readline.write_history_file, histfile)

    conn = connect()
    print('''"conn" is your handle to rserve. Type e.g. "conn('1')" for string evaluation.''')
    #r('x<-1:20; y<-x*2; lm(y~x)')
    sc = open('../testData/test-script.R').read()
    v = conn.r(sc)
    open('r-test-png.png', 'w').write(v[3])
    conn.r.v = 'abc'
    conn.r('func0 <- function() { 3 }')
    conn.r('func1 <- function(a1) { a1 }')
    conn.r('func2 <- function(a1, a2) { list(a1, a2) }')
    conn.r('funcKW <- function(a1=1, a2=4) { list(a1, a2) }')
    conn.r('squared<-function(t) t^2')

