# Copyright (C) 2009 Kris Maglione

import operator
import os
import re
import sys
from threading import *
import traceback

import pyxp
from pyxp import fcall, fields
from pyxp.mux import Mux
from pyxp.types import *

if os.environ.get('NAMESPACE', None):
    namespace = os.environ['NAMESPACE']
else:
    try:
        namespace = '/tmp/ns.%s.%s' % (
            os.environ['USER'], 
            re.sub(r'\.0$', '', os.environ['DISPLAY']))
    except Exception:
        pass
NAMESPACE = namespace

OREAD = 0x00
OWRITE = 0x01
ORDWR = 0x02
OEXEC = 0x03
OEXCL = 0x04
OTRUNC = 0x10
OREXEC = 0x20
ORCLOSE = 0x40
OAPPEND = 0x80

ROOT_FID = 0

class ProtocolException(Exception):
    pass
class RPCError(Exception):
    pass

class Client(object):
    ROOT_FID = 0

    @staticmethod
    def respond(callback, data, exc=None, tb=None):
        if hasattr(callback, 'func_code'):
            callback(*(data, exc, tb)[0:callback.func_code.co_argcount])
        elif callable(callback):
            callback(data)

    def __enter__(self):
        return self
    def __exit__(self, *args):
        self._cleanup()

    def __init__(self, conn=None, namespace=None, root=None):
        if not conn and namespace:
            conn = 'unix!%s/%s' % (NAMESPACE, namespace)
        try:
            self.lastfid = ROOT_FID
            self.fids = set()
            self.lock = RLock()

            def process(data):
                return fcall.Fcall.unmarshall(data)[1]
            self.mux = Mux(conn, process, maxtag=256)

            resp = self._dorpc(fcall.Tversion(version=pyxp.VERSION, msize=65535))
            if resp.version != pyxp.VERSION:
                raise ProtocolException, "Can't speak 9P version '%s'" % resp.version
            self.msize = resp.msize

            self._dorpc(fcall.Tattach(fid=ROOT_FID, afid=fcall.NO_FID,
                       uname=os.environ['USER'], aname=''))

            if root:
                path = self._splitpath(root)
                resp = self._dorpc(fcall.Twalk(fid=ROOT_FID,
                                               newfid=ROOT_FID,
                                               wname=path))
        except Exception:
            traceback.print_exc(sys.stdout)
            if getattr(self, 'mux', None):
                self.mux.fd.close()
            raise

    def _cleanup(self):
        try:
            for f in self.files:
                f.close()
        finally:
            self.mux.fd.close()
            self.mux = None

    def _dorpc(self, req, callback=None, error=None):
        def doresp(resp):
            if isinstance(resp, fcall.Rerror):
                raise RPCError, "%s[%d] RPC returned error: %s" % (
                    req.__class__.__name__, resp.tag, resp.ename)
            if req.type != resp.type ^ 1:
                raise ProtocolException, "Missmatched RPC message types: %s => %s" % (
                    req.__class__.__name__, resp.__class__.__name__)
            return resp

        def next(mux, resp):
            try:
                res = doresp(resp)
            except Exception, e:
                self.respond(error or callback, None, e, None)
            else:
                self.respond(callback, res)

        if not callback:
            return doresp(self.mux.rpc(req))
        self.mux.rpc(req, next)

    def _splitpath(self, path):
        if isinstance(path, list):
            return path
        return [v for v in path.split('/') if v != '']

    def _getfid(self):
        with self.lock:
            if self.fids:
                return self.fids.pop()
            self.lastfid += 1
            return self.lastfid
    def _putfid(self, fid):
        with self.lock:
            self.fids.add(fid)

    def _aclunk(self, fid, callback=None):
        def next(resp, exc, tb):
            if resp:
                self._putfid(fid)
            self.respond(callback, resp, exc, tb)
        self._dorpc(fcall.Tclunk(fid=fid), next)

    def _clunk(self, fid):
        try:
            self._dorpc(fcall.Tclunk(fid=fid))
        finally:
            self._putfid(fid)

    def _walk(self, path):
        fid = self._getfid()
        ofid = ROOT_FID
        while True:
            self._dorpc(fcall.Twalk(fid=ofid, newfid=fid,
                                   wname=path[0:fcall.MAX_WELEM]))
            path = path[fcall.MAX_WELEM:]
            ofid = fid
            if len(path) == 0:
                break

        @apply
        class Res:
            def __enter__(res):
                return fid
            def __exit__(res, exc_type, exc_value, traceback):
                if exc_type:
                    self._clunk(fid)
        return Res

    _file = property(lambda self: File)
    def _open(self, path, mode, fcall, origpath=None):
        resp = None

        with self._walk(path) as nfid:
            fid = nfid
            fcall.fid = fid
            resp = self._dorpc(fcall)

        def cleanup():
            self._aclunk(fid)
        file = self._file(self, origpath or '/'.join(path), resp, fid, mode, cleanup)
        return file

    def open(self, path, mode=OREAD):
        path = self._splitpath(path)

        return self._open(path, mode, fcall.Topen(mode=mode))

    def create(self, path, mode=OREAD, perm=0):
        path = self._splitpath(path)
        name = path.pop()

        return self._open(path, mode, fcall.Tcreate(mode=mode, name=name, perm=perm),
                          origpath='/'.join(path + [name]))

    def remove(self, path):
        path = self._splitpath(path)

        with self._walk(path) as fid:
            self._dorpc(fcall.Tremove(fid=fid))

    def stat(self, path):
        path = self._splitpath(path)

        try:
            with self._walk(path) as fid:
                resp = self._dorpc(fcall.Tstat(fid= fid))
                st = resp.stat
                self._clunk(fid)
            return st
        except RPCError:
            return None

    def read(self, path, *args, **kwargs):
        with self.open(path) as f:
            return f.read(*args, **kwargs)
    def readlines(self, path, *args, **kwargs):
        with self.open(path) as f:
            for l in f.readlines(*args, **kwargs):
                yield l
    def readdir(self, path, *args, **kwargs):
        with self.open(path) as f:
            for s in f.readdir(*args, **kwargs):
                yield s
    def write(self, path, *args, **kwargs):
        with self.open(path, OWRITE) as f:
            return f.write(*args, **kwargs)

class File(object):

    def __enter__(self):
        return self
    def __exit__(self, *args):
        self.close()

    def __init__(self, client, path, fcall, fid, mode, cleanup):
        self.lock = RLock()
        self.client = client
        self.path = path
        self.fid = fid
        self._cleanup = cleanup
        self.mode = mode
        self.iounit = fcall.iounit
        self.qid = fcall.qid
        self.closed = False

        self.offset = 0
    def __del__(self):
        if not self.closed:
            self._cleanup()

    def _dorpc(self, fcall, async=None, error=None):
        if hasattr(fcall, 'fid'):
            fcall.fid = self.fid
        return self.client._dorpc(fcall, async, error)

    def stat(self):
        resp = self._dorpc(fcall.Tstat())
        return resp.stat

    def read(self, count=None, offset=None, buf=''):
        if count is None:
            count = self.iounit
        res = []
        with self.lock:
            offs = self.offset
            if offset is not None:
                offs = offset
            while count > 0:
                n = min(count, self.iounit)
                count -= n

                resp = self._dorpc(fcall.Tread(offset=offs, count=n))
                data = resp.data

                offs += len(data)
                res.append(data)

                if len(data) < n:
                    break
            if offset is None:
                self.offset = offs
        return ''.join(res)

    def readlines(self):
        last = None
        while True:
            data = self.read()
            if not data:
                break
            lines = data.split('\n')
            if last:
                lines[0] = last + lines[0]
                last = None
            for i in range(0, len(lines) - 1):
                yield lines[i]
            last = lines[-1]
        if last:
            yield last

    def write(self, data, offset=None):
        if offset is None:
            offset = self.offset
        off = 0
        with self.lock:
            offs = self.offset
            if offset is not None:
                offs = offset
            while off < len(data):
                n = min(len(data), self.iounit)

                resp = self._dorpc(fcall.Twrite(offset=offs,
                                               data=data[off:off+n]))
                off += resp.count
                offs += resp.count
                if resp.count < n:
                    break
            if offset is None:
                self.offset = offs
        return off
    def readdir(self):
        if not self.qid.type & Qid.QTDIR:
            raise Exception, "Can only call readdir on a directory"
        off = 0
        while True:
            data = self.read(self.iounit, off)
            if not data:
                break
            off += len(data)
            for s in Stat.unmarshall_list(data):
                yield s

    def close(self):
        assert not self.closed
        self.closed = True
        try:
            self._cleanup()
        except:
            pass
        self.tg = None
        self.fid = None
        self.client = None
        self.qid = None

    def remove(self):
        try:
            self._dorpc(fcall.Tremove())
        finally:
            self.close()

# vim:se sts=4 sw=4 et:
