# encoding: utf-8

__all__ = ['render', 'CodecError', 'EncodeError', 'DecodeError', 'Encoding', 'ChunkedEncoder', 'Bencode', 'EnhancedBencode']


def render(data, template=None, kind='enhanced', i18n=None, **kw):
    """A bencoding serializer templating language.
    
    Accepts the same extended arguments as the JSON dumps() function, see:
    
        http://docs.python.org/library/json.html#json.dump
    
    Data may be of any datatype supported by the json standard library or simplejson.
    
    Sample usage:
    
        >>> from alacarte.core import Engines
        >>> render = Engines()
        >>> render.bencode(dict(hello="world"))
        ('application/x-bencode', 'd5:hello5:worlde')
        
    """
    
    codecs = dict(basic=Bencode, enhanced=EnhancedBencode)
    
    return 'application/x-bencode', codecs[kind]().encode(data)


class CodecError(Exception):
    """Useful superclass to group all codec-related errors."""


class EncodeError(CodecError):
    """Raised by C{Encoding} implementations if encode fails."""


class DecodeError(CodecError):
    """Raised by C{Encoding} implementations if decode fails."""


class Encoding(object):
    """Interface for RPC message encoders/decoders.
    
    All encoding implementations used with this library should inherit and implement this.
    """
    
    def encode(self, data): # pragma: no cover
        """Encode data.
        
        @param data: The data to encode.  Must, at a minimum, implement encoding of C{str}, C{int}, and C{long} values.
        
        @return: The encoded data.
        @rtype: str
        """
        raise NotImplementedError
    
    def decode(self, data): # pragma: no cover
        """Decode data.
        
        @param data: The data (byte string) to decode.
        @type data: str
        
        @return: The decoded data (in its correct type).
        """
        raise NotImplementedError


class ChunkedEncoder(Encoding):
    """A mix-in class to easily support chunked encoders."""
    
    def encode(self, data):
        try:
            return getattr(self, 'encode_' + type(data).__name__)(data)
        
        except AttributeError:
            raise EncodeError("Unable to encode a chunk of type '%s'." % (type(data), ))


class Bencode(ChunkedEncoder):
    """Implementation of the bencode algorithm used by Bittorrent.
    
    See: http://en.wikipedia.org/wiki/Bencode
    
    Suported Values: C{str}, C{int}, C{long}, C{dict}, C{list}
    """
    
    def decode(self, data):
        length = len(data)
        
        if length == 0: raise DecodeError("Can not decode an empty string.")
        
        data, processed = self._decode(data)
        
        if processed != length: raise DecodeError("Did not fully decode input. %d of %d processed, %d bytes remaining." % (processed, length, length - processed))
        
        return data
    
    def _decode(self, data, offset=0):
        signature = data[offset : offset + 1]
        
        if hasattr(self, 'decode_' + signature):
            return getattr(self, 'decode_' + signature)(data, offset + 1)
        
        if signature.isdigit():
            return self.decode_str(data, offset)
        
        raise DecodeError("Unable to decode unknown signature '%s'." % (signature, ))
    
    def encode_int(self, data):
        return 'i%de' % (data, )
    
    encode_long = encode_int
    encode_bool = encode_int
    
    def decode_i(self, data, o):
        index = data.index('e', o)
        return int(data[o:index]), index + 1
    
    def encode_str(self, data):
        return '%d:%s' % (len(data), data)
    
    def decode_str(self, data, o):
        index = data.index(':', o)
        length = int(data[o:index])
        offset = index + 1
        return data[offset : offset + length], offset + length
    
    def encode_list(self, data):
        return 'l%se' % (''.join([self.encode(item) for item in data]), )
    
    encode_tuple = encode_list
    
    def decode_l(self, data, o):
        offset = o
        values = []
        
        while data[offset] != 'e':
            value, offset = self._decode(data, offset)
            values.append(value)
        
        return values, offset + 1
    
    def encode_dict(self, data):    return 'd%se' % (''.join([(self.encode(key) + self.encode(data[key])) for key in sorted(data.keys())]), )
    def decode_d(self, data, o):
        offset = o
        values = {}
        
        while data[offset] != 'e':
            key, offset = self._decode(data, offset)
            value, offset = self._decode(data, offset)
            values[key] = value

        return values, offset + 1


class EnhancedBencode(Bencode):
    """Implementation of a Bencode-based algorithm.
    
    Suported Values: C{str}, C{int}, C{long}, C{dict}, C{list}, C{float}, C{None}, C{tuple}, C{set}, C{unicode}
    
    @note: This algorithm differs from the "official" Bencode algorithm in that it can encode/decode additional data types.
    """
    
    def encode_float(self, data):
        return 'f%fe' % (data, )
    
    def decode_f(self, data, o):
        index = data.index('e', o)
        return float(data[o:index]), index + 1
    
    def encode_NoneType(self, data):
        return 'n'
    
    def decode_n(self, data, o):
        return None, o
    
    def encode_tuple(self, data):
        return 't%se' % (''.join([self.encode(item) for item in data]), )
    
    def decode_t(self, data, o):
        offset = o
        values = []
        
        while data[offset] != 'e':
            value, offset = self._decode(data, offset)
            values.append(value)
        
        return tuple(values), offset + 1
    
    def encode_set(self, data):
        return 's%se' % (''.join([self.encode(item) for item in data]), )
    
    def decode_s(self, data, o):
        offset = o
        values = []
        
        while data[offset] != 'e':
            value, offset = self._decode(data, offset)
            values.append(value)
        
        return set(values), offset + 1
    
    def encode_unicode(self, data):
        encoded = data.encode('utf-8')
        return 'u%d:%s' % (len(encoded), encoded)
    
    def decode_u(self, data, o):
        index = data.index(':', o)
        length = int(data[o:index])
        offset = index + 1
        return data[offset : offset + length].decode('utf-8'), offset + length
