import sys
import os
import inspect
import pdb
import pickle
import rpyc
from rpyc import SlaveService
from rpyc.utils import factory


SERVER_FILE = os.path.join(os.path.dirname(rpyc.__file__), "servers", "classic_server.py")
DEFAULT_SERVER_PORT = 18812


#===============================================================================
# connecting
#===============================================================================
def connect_channel(channel):
    return factory.connect_channel(channel, SlaveService)

def connect_stream(stream):
    return factory.connect_stream(stream, SlaveService)

def connect_stdpipes():
    return factory.connect_stdpipes(SlaveService)

def connect_pipes(input, output):
    return factory.connect_pipes(input, output, SlaveService)

def connect(host, port = DEFAULT_SERVER_PORT):
    """creates a socket connection to the given host and port"""
    return factory.connect(host, port, SlaveService)

def tls_connect(host, username, password, port = DEFAULT_SERVER_PORT):
    """creates a secure (TLS) socket connection to the given host and port,
    authenticating with the given username and password"""
    return factory.tls_connect(host, port, username, password, SlaveService)

def connect_subproc():
    """runs an rpyc classic server as a subprocess and return an rpyc
    connection to it"""
    return factory.connect_subproc(["python", "-u", SERVER_FILE, "-q", "-m", "stdio"], 
        SlaveService)

def connect_thread():
    """starts a SlaveService on a thread and connects to it"""
    return factory.connect_thread(SlaveService, remote_service = SlaveService)


#===============================================================================
# remoting utilities
#===============================================================================
def upload(conn, localpath, remotepath, filter = None, ignore_invalid = False):
    """uploads a file or a directory to the given remote path
    localpath - the local file or directory
    remotepath - the remote path
    filter - a predicate that accepts the filename and determines whether 
    it should be uploaded; None means any file"""
    if os.path.isdir(localpath):
        upload_dir(conn, localpath, remotepath, filter)
    elif os.path.isfile(localpath):
        upload_file(conn, localpath, remotepath)
    else:
        if not ignore_invalid:
            raise ValueError("cannot upload %r" % (localpath,))

def upload_file(conn, localpath, remotepath):
    lf = open(localpath, "rb")
    rf = conn.modules.builtins.open(remotepath, "wb")
    while True:
        buf = lf.read(16000)
        if not buf:
            break
        rf.write(buf)
    lf.close()
    rf.close()

def upload_dir(conn, localpath, remotepath, filter = None):
    if not conn.modules.os.path.isdir(remotepath):
        conn.modules.os.makedirs(remotepath)
    for fn in os.listdir(localpath):
        if not filter or filter(fn):
            lfn = os.path.join(localpath, fn)
            rfn = conn.modules.os.path.join(remotepath, fn)
            upload(conn, lfn, rfn, filter = filter, ignore_invalid = True)

def download(conn, remotepath, localpath, filter = None, ignore_invalid = False):
    """download a file or a directory to the given remote path
    localpath - the local file or directory
    remotepath - the remote path
    filter - a predicate that accepts the filename and determines whether 
    it should be downloaded; None means any file"""
    if conn.modules.os.path.isdir(remotepath):
        download_dir(conn, remotepath, localpath, filter)
    elif conn.modules.os.path.isfile(remotepath):
        download_file(conn, remotepath, localpath)
    else:
        if not ignore_invalid:
            raise ValueError("cannot download %r" % (remotepath,))

def download_file(conn, remotepath, localpath):
    rf = conn.modules.builtins.open(remotepath, "rb")
    lf = open(localpath, "wb")
    while True:
        buf = rf.read(16000)
        if not buf:
            break
        lf.write(buf)
    lf.close()
    rf.close()

def download_dir(conn, remotepath, localpath, filter = None):
    if not os.path.isdir(localpath):
        os.makedirs(localpath)
    for fn in conn.modules.os.listdir(remotepath):
        if not filter or filter(fn):
            rfn = conn.modules.os.path.join(remotepath, fn)
            lfn = os.path.join(localpath, fn)
            download(conn, rfn, lfn, filter = filter, ignore_invalid = True)

def upload_package(conn, module, remotepath = None):
    """uploads a module or a package to the remote party"""
    if remotepath is None:
        site = conn.modules["distutils.sysconfig"].get_python_lib()
        remotepath = conn.modules.os.path.join(site, module.__name__)
    localpath = os.path.dirname(inspect.getsourcefile(module))
    upload(conn, localpath, remotepath)

upload_module = upload_package

def update_module(conn, module):
    """replaces a module on the remote party"""
    rmodule = conn.modules[module.__name__]
    lf = inspect.getsourcefile(module)
    rf = conn.modules.inspect.getsourcefile(rmodule)
    upload_file(conn, lf, rf)
    c.modules.imp.reload(rmodule)

def obtain(proxy):
    """obtains (recreates) a remote object proxy from the other party. 
    the object is moved by *value*, so changes made to it will not reflect 
    on the remote object"""
    return pickle.loads(pickle.dumps(proxy))

def deliver(conn, localobj):
    """delivers (recreates) a local object on the other party. the object is
    moved by *value*, so changes made to it will not reflect on the local 
    object. returns a proxy to the remote object"""
    return conn.modules.pickle.loads(pickle.dumps(localobj))

class redirected_stdio(object):
    """redirects the other party's stdin, stdout and stderr to those of the 
    local party, so remote STDIO will occur locally"""
    def __init__(self, conn):
        self._restored = True
        self.conn = conn
        self.orig_stdin = self.conn.modules.sys.stdin
        self.orig_stdout = self.conn.modules.sys.stdout
        self.orig_stderr = self.conn.modules.sys.stderr
        self.conn.modules.sys.stdin = sys.stdin
        self.conn.modules.sys.stdout = sys.stdout
        self.conn.modules.sys.stderr = sys.stderr
        self._restored = False
    def __del__(self):
        self.restore()
    def restore(self):
        if self._restored:
            return
        self._restored = True
        self.conn.modules.sys.stdin = self.orig_stdin
        self.conn.modules.sys.stdout = self.orig_stdout
        self.conn.modules.sys.stderr = self.orig_stderr
    def __enter__(self):
        return self
    def __exit__(self, t, v, tb):
        self.restore()

#== compatibility with python 2.4 ==
#@contextmanager
#def redirected_stdio(conn):
#    orig_stdin = conn.modules.sys.stdin
#    orig_stdout = conn.modules.sys.stdout
#    orig_stderr = conn.modules.sys.stderr
#    try:
#        conn.modules.sys.stdin = sys.stdin
#        conn.modules.sys.stdout = sys.stdout
#        conn.modules.sys.stderr = sys.stderr
#        yield
#    finally:
#        conn.modules.sys.stdin = orig_stdin
#        conn.modules.sys.stdout = orig_stdout
#        conn.modules.sys.stderr = orig_stderr

def pm(conn):
    """pdb.pm on a remote exception"""
    #pdb.post_mortem(conn.root.getconn()._last_traceback)
    redir = redirected_stdio(conn)
    try:
        conn.modules.pdb.post_mortem(conn.root.getconn()._last_traceback)
    finally:
        redir.restore()

def interact(conn, namespace = None):
    """remote interactive interpreter"""
    if namespace is None:
        namespace = {}
    namespace["conn"] = conn
    redir = redirected_stdio(conn)
    try:
        conn.execute("""def _rinteract(ns):
            import code
            code.interact(local = dict(ns))""")
        conn.namespace["_rinteract"](namespace)
    finally:
        redir.restore()




