'''
Experimental MMTF (Macromolecular Transmission Format) I/O library
'''

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import sys
import itertools
import numpy
import struct

try:
    import msgpack
except ImportError:
    import umsgpack as msgpack

if True:
    from urllib.request import urlopen
    izip = zip
    izip_longest = itertools.zip_longest
    as_msgpack_key = lambda k: k if isinstance(k, str) else k.decode("utf-8")
    buffer = lambda s, i=0: memoryview(s)[i:]

# should be replaced with a more efficient numpy-array aware iterator
def simpleiter(iterable):
    if isinstance(iterable, numpy.ndarray):
        # Iterating over a numpy array is unreasonably slow. Iterating
        # over it's list copy is much faster!
        return iter(iterable.tolist())
    return iter(iterable)

def asarray(arr, dtype='i'):
    if hasattr(arr, '__len__'):
        return numpy.asarray(arr, dtype)
    return numpy.fromiter(arr, dtype)

MMTF_ENDIAN = '>' # big-endian

########### ENCODINGS ################

class RunLength:
    @staticmethod
    def encode(iterable):
        in_iter = simpleiter(iterable)
        curr = next(in_iter)
        counter = 1
        for item in in_iter:
            if item == curr:
                counter += 1
            else:
                yield curr
                yield counter
                curr = item
                counter = 1
        yield curr
        yield counter

    @staticmethod
    def decode(iterable):
        in_iter = simpleiter(iterable)
        out = []
        extend = out.extend
        for item in in_iter:
            extend([item] * next(in_iter))
        return out

class Delta:
    @staticmethod
    def encode(iterable):
        return numpy.diff(asarray(iterable, 'i4'))

    @staticmethod
    def decode(iterable):
        return asarray(iterable).cumsum(dtype='i4')

class RecursiveIndex:
    def __init__(self, min, max):
        self.limits = (min, max)

    def encode(self, iterable):
        min, max = self.limits
        for curr in simpleiter(iterable):
            while curr >= max:
                yield max
                curr -=  max
            while curr <= min:
                yield min
                curr -= min
            yield curr

    def decode(self, iterable):
        min, max = self.limits
        decoded_val = 0
        for item in simpleiter(iterable):
            decoded_val += item
            if item != max and item != min:
                yield decoded_val
                decoded_val = 0

class IntegerFloats:
    def __init__(self, factor):
        self.factor = factor

    def encode(self, in_floats):
        return (asarray(in_floats, 'f4') * self.factor).astype('i4')

    def decode(self, in_ints):
        return asarray(in_ints, 'f4') / self.factor

class IntegerChars:
    @staticmethod
    def encode(in_chars):
        return [ord(x) for x in in_chars]

    @staticmethod
    def decode(in_ints):
        return [(chr(x) if x else '') for x in simpleiter(in_ints)]

######## BUFFERS ###########

class NumbersBuffer:
    def __init__(self, basetype='i', dectype=''):
        self.enctype = numpy.dtype(MMTF_ENDIAN + basetype)
        self.dectype = numpy.dtype(dectype or basetype)

    def decode(self, in_bytes):
        return numpy.frombuffer(in_bytes, self.enctype).astype(self.dectype)

    def encode(self, in_ints):
        return asarray(in_ints, self.enctype).tostring()

class StringsBuffer:
    def __init__(self, nbytes, encoding='ascii'):
        self.enctype = numpy.dtype('S' + str(nbytes))
        self.encoding = encoding

    def decode(self, in_bytes):
        bstrings = numpy.frombuffer(in_bytes, self.enctype)
        return [b.decode(self.encoding) for b in bstrings]

    def encode(self, strings):
        bstrings = numpy.fromiter((s.encode(self.encoding) for s in strings),
                self.enctype, len(strings))
        return bstrings.tostring()

########## STRATEGIES #############

def _PackedIntBufStrategy(nbytes=1, dectype='i4'):
    m = 1 << (nbytes * 8 - 1)
    return [
        NumbersBuffer('i' + str(nbytes), dectype),
        RecursiveIndex(-m, m - 1),
    ]

strategies = {
    1: [NumbersBuffer('f4')],
    2: [NumbersBuffer('i1')],
    3: [NumbersBuffer('i2')],
    4: [NumbersBuffer('i4')],
    5: lambda length: [StringsBuffer(length)],
    6: [NumbersBuffer('i4'), RunLength, IntegerChars],
    7: [NumbersBuffer('i4'), RunLength],
    8: [NumbersBuffer('i4'), RunLength, Delta],
    9: lambda factor: [NumbersBuffer('i4'), RunLength, IntegerFloats(factor)],
   10: lambda factor: _PackedIntBufStrategy(2) + [Delta, IntegerFloats(factor)],
   11: lambda factor: [NumbersBuffer('i2'), IntegerFloats(factor)],
   12: lambda factor: _PackedIntBufStrategy(2) + [IntegerFloats(factor)],
   13: lambda factor: _PackedIntBufStrategy(1) + [IntegerFloats(factor)],
   14: _PackedIntBufStrategy(2),
   15: _PackedIntBufStrategy(1),
}

# optional parameters format (defaults to 'i' -> one int32 argument)
strategyparamsfmt = {
}

########## MEDIUM LEVEL ARRAY ENCODE/DECODE API ##############

def encode(arr, codec, param=0):
    strategy = strategies[codec]

    if not isinstance(strategy, list):
        strategy = strategy(param)

    buf = struct.pack(MMTF_ENDIAN + 'iii', codec, len(arr), param)

    for handler in reversed(strategy):
        arr = handler.encode(arr)

    buf += arr
    return buf

def encode_int(arr):
    '''Find the best compression for a 32bit int array'''
    return min((encode(arr, codec) for codec in (4, 7, 8)), key=len)

def decode(value):
    codec, length = struct.unpack(MMTF_ENDIAN + 'ii', value[:8])

    strategy = strategies[codec]
    if not isinstance(strategy, list):
        fmt = strategyparamsfmt.get(codec, 'i')
        params = struct.unpack(MMTF_ENDIAN + fmt, value[8:12])
        strategy = strategy(*params)

    value = buffer(value, 12)
    for handler in strategy:
        value = handler.decode(value)

    return value

############### HIGH LEVEL READER API ################

class MmtfReader:
    def __init__(self, data):
        if isinstance(data, bytes):
            if data[:2] != b'\x1f\x8b': # gzip magic number
                self._data = msgpack.unpackb(data, raw=False)
                return

            import io, gzip
            data = gzip.GzipFile(fileobj=io.BytesIO(data))

        self._data = msgpack.unpack(data, raw=False)

    @classmethod
    def from_url(cls, url):
        handle = open(url, 'rb') if os.path.isfile(url) else urlopen(url)
        return cls(handle.read())

    def get(self, key, default=None):
        key = as_msgpack_key(key)
        try:
            value = self._data[key]
        except KeyError:
            return default

        if not (key.endswith('List') and isinstance(value, bytes)):
            return value

        return decode(value)

    def get_iter(self, key, default=()):
        return simpleiter(self.get(key, default))

    def get_table_iter(self, keys, defaults=None):
        if defaults is None:
            return izip_longest(*[self.get_iter(k) for k in keys])
        return izip(*[self.get_iter(k, itertools.repeat(d))
            for (k, d) in zip(keys, defaults)])
