import select
import cPickle
import json

from ..util import app_logging

from .instantiate import instantiate, Instantiator

#
# Support for using JSON for serialization
#


log = app_logging.getLogger('Connection')


def _get_dict(obj):
    """
    Call __getstate__ or simply return __dict__.
    """
    if hasattr(obj, "__getstate__"):
        r = obj.__getstate__()
    else:
        r = obj.__dict__
    return r


class JSONPickler(object):
    """
    Class to dump/load our messages in JSON.
    Supports file-like objects as input for load.
    Dumps only to strings.
    """
    def __init__(self, hook=instantiate):
        self.hook = hook

    def dumps(self, obj):
        return json.dumps(obj, separators=(',', ':'), default=_get_dict)

    def load(self, f):
        j = json.JSONDecoder(object_hook=self.hook)
        pos = f.tell()
        s = f.read()
        x, i = j.raw_decode(s)
        f.seek(i + pos)
        return x


def _json_dumps(obj):
    return json.dumps(obj, separators=(',', ':'), default=_get_dict)


def _json_load(f):
    j = json.JSONDecoder(object_hook=instantiate)
    pos = f.tell()
    s = f.read()
    x, i = j.raw_decode(s)
    f.seek(i + pos)
    return x

#
# Support for using cPickle for serialization
#


def _cPickle_dumps(obj):
    return cPickle.dumps(obj, -1)


def _cPickle_loads(s):
    return cPickle.loads(s)


def _cPickle_load(f):
    return cPickle.load(f)

SEEK_START = 0
SEEK_CUR = 1
SEEK_END = 2
STRIP_LENGTH = 10000


class ReadableBuffer(object):
    """
    File-like interface for sequence.
    Supports state save/load, read and append.
    Can strip first part by appending.
    Set frozen=True before using seek/tell.
    """
    def __init__(self, s=None):
        if s is None:
            self.buf = ""
        else:
            self.buf = s
        self.pos = 0
        self.sbuf = self.buf
        self.spos = self.pos
        self._frozen = False

    def readtail(self):
        s = self.buf[self.pos:]
        self.pos = len(self.buf)
        return s

    def read(self, n=-1):
        if n < 0:
            return self.readtail()
        else:
            s = self.buf[self.pos:self.pos + n]
            self.pos += len(s)
            return s

    def readline(self):
        n = self.buf.find('\n', self.pos)
        if n == -1:
            return self.readtail()
        else:
            s = self.buf[self.pos:n + 1]
            self.pos = n + 1
            return s

    def append(self, s):
        self.buf += s
        if not self._frozen and self.pos > STRIP_LENGTH:
            self.buf = self.buf[self.pos:]
            self.pos = 0

    def savestate(self):
        self.spos = self.pos
        self.sbuf = self.buf

    def loadstate(self):
        self.pos = self.spos
        self.buf = self.sbuf

    def seek(self, pos, whence=SEEK_START):
        seek_pos = 0
        if whence == SEEK_CUR:
            seek_pos = self.pos
        elif whence == SEEK_END:
            seek_pos = len(self.buf)

        pos = pos + seek_pos
        if pos < 0:
            self.pos = 0
        elif pos > len(self.buf):
            self.pos = len(self.buf)
        else:
            self.pos = pos

    def tell(self):
        return self.pos

    @property
    def frozen(self):
        return self._frozen

    @frozen.setter
    def frozen(self, frozen):
        if isinstance(frozen, bool):
            self._frozen = frozen
            return
        raise TypeError('Frozen must be bool')

    def getvalue(self):
        return self.buf

    def __len__(self):
        return len(self.buf) - self.pos


class TimeoutException(Exception):
    pass


class ConnectionFactory(object):
    def __init__(self, pickler=None, instantiator=None):
        if pickler is None:
            if instantiator is None:
                pickler = JSONPickler(hook=Instantiator())
            else:
                pickler = JSONPickler(hook=instantiator)
        self.pickler = pickler

    def create_connection(self, socket):
        return SimpleConnection(socket, load=self.pickler.load,
                                dumps=self.pickler.dumps)


class SimpleConnection(object):
    """
    Wrapper for a socket.
    Supports asyncronious object send/recv with provided serializers.
    Reads all available data from socket and returns fully received objects.
    """
    def __init__(self, socket, load=_json_load, dumps=_json_dumps):
        self.socket = socket
        socket.setblocking(False)
        self.buffer = ReadableBuffer()
        self.dead = False
        self.load = load
        self.dumps = dumps

    def send(self, obj):
        if self.dead:
            raise EOFError('Socket is closed')
        d = None
        try:
            d = self.dumps(obj)
        except Exception as e:
            log.debug('%s on dump %s' % (e, obj))
            return
        try:
            r, w, e = select.select([], [self.socket], [], 1)
        except:
            self.dead = True
        if len(w) > 0:
            self.socket.sendall(d)
        else:
            log.info("Unable to send: select() timeout")
            raise TimeoutException()

    def recv(self):
        while True:
            r = []
            w = []
            e = []
            if not self.dead:
                try:
                    r, w, e = select.select([self.socket], [],
                                            [self.socket], 0.01)
                except Exception as e:
                    log.debug("%s" % e)
                    self.dead = True
                if len(r) > 0:
                    d = self.socket.recv(8192)
                    if len(d) == 0:
                        self.dead = True
                    self.buffer.append(d)
                if len(e) > 0:
                    self.dead = True
            self.buffer.savestate()
            obj = None
            try:
                obj = self.load(self.buffer)
            except:
                self.buffer.loadstate()
                if self.dead:
                    raise EOFError("Socket is closed")
                if len(r) == 0:
                    return None
            if obj is not None:
                return obj

    def recv_all(self):
        objs = []
        buffers = []
        while not self.dead:
            r = []
            w = []
            e = []
            try:
                r, w, e = select.select([self.socket], [], [self.socket], 0.0)
            except Exception as e:
                log.debug("%s" % e)
                self.dead = True
                break
            if len(e) > 0:
                self.dead = True
            if len(r) > 0:
                d = self.socket.recv(4096)
                if len(d) == 0:
                    self.dead = True
                buffers.append(d)
            else:
                break

        self.buffer.append("".join(buffers))
        while True:
            self.buffer.savestate()
            obj = None
            try:
                obj = self.load(self.buffer)
            except Exception as e:
                self.buffer.loadstate()
                if self.dead and len(objs) == 0:
                    raise EOFError("Socket is closed")
                else:
                    return objs
            if obj is not None:
                objs.append(obj)

    def close(self):
        """
        Disconnect.
        """
        self.socket.close()

    def readable(self, timeout):
        """
        Can we read?
        """
        self.buffer.append("")
        if len(self.buffer) > 0:
            return True
        if len(select.select([self.socket], [], [], timeout)[0]) > 0:
            return True
        return False

    def writeable(self, timeout):
        """
        Can we write?
        """
        if len(select.select([], [self.socket], [], timeout)[1]) > 0:
            return True
        return False

    def error(self, timeout):
        """
        Can we receive an error?
        """
        if len(select.select([], [], [self.socket], timeout)[2]) > 0:
            return True
        return False

    def fileno(self):
        """
        Underlying socket.
        """
        return self.socket.fileno()
