
# original version from https://github.com/MostAwesomeDude/bravo

from StringIO import StringIO

from collections import namedtuple
from codecs import register
from codecs import (BufferedIncrementalDecoder, CodecInfo, IncrementalEncoder,
                    StreamReader, StreamWriter, utf_16_be_encode,
                    utf_16_be_decode)


from construct import Struct, Container, Embed, MetaField, Value
from construct import MetaArray, If, IfThenElse, Switch, Const, Peek
from construct import OptionalGreedyRange, RepeatUntil
from construct import Flag, PascalString, Adapter
from construct import UBInt8, UBInt16, UBInt32
from construct import SBInt8, SBInt16, SBInt32, SBInt64
from construct import BFloat32, BFloat64
from construct import BitStruct, BitField
from construct import StringAdapter, LengthValueAdapter, Sequence

from pynbt import NBTFile

import logbot

# Strings.
# This one is a UCS2 string, which effectively decodes single writeChar()
# invocations. We need to import the encoding for it first, though.

log = logbot.getlogger("BOT_ENTITY")


def ucs2(name):
    if name.lower() not in ("ucs2", "ucs-2"):
        return None

    def ucs2_encode(data, errors="replace"):
        data = u"".join(i if ord(i) < 65536 else u"?" for i in data)
        return utf_16_be_encode(data, errors)

    ucs2_decode = utf_16_be_decode

    class UCS2IncrementalEncoder(IncrementalEncoder):
        def encode(self, input, final=False):
            return ucs2_encode(input, self.errors)[0]

    class UCS2IncrementalDecoder(BufferedIncrementalDecoder):
        _buffer_decode = ucs2_decode

    class UCS2StreamWriter(StreamWriter):
        encode = ucs2_encode

    class UCS2StreamReader(StreamReader):
        decode = ucs2_decode

    return CodecInfo(
        name="ucs2",
        encode=ucs2_encode,
        decode=ucs2_decode,
        incrementalencoder=UCS2IncrementalEncoder,
        incrementaldecoder=UCS2IncrementalDecoder,
        streamwriter=UCS2StreamWriter,
        streamreader=UCS2StreamReader,
    )

register(ucs2)


class DoubleAdapter(LengthValueAdapter):

    def _encode(self, obj, context):
        return len(obj) / 2, obj


def AlphaString(name):
    return StringAdapter(
        DoubleAdapter(
            Sequence(name,
                     UBInt16("length"),
                     MetaField("data", lambda ctx: ctx["length"] * 2),
                     )
        ),
        encoding="ucs2",
    )

# Boolean converter.


def Bool(*args, **kwargs):
    return Flag(*args, default=True, **kwargs)

# Flying, position, and orientation, reused in several places.
grounded = Struct("grounded", UBInt8("grounded"))
position = Struct("position",
                  BFloat64("x"),
                  BFloat64("y"),
                  BFloat64("stance"),
                  BFloat64("z")
                  )
orientation = Struct("orientation", BFloat32("yaw"), BFloat32("pitch"))


def ByteString(name, size_name, encoding=None):
    return StringAdapter(MetaField("data", lambda ctx: ctx[size_name]),
                         encoding=encoding)


class NBTAdapter(Adapter):

    def _decode(self, obj, context):
        return NBTFile(StringIO(obj), compression=NBTFile.Compression.GZIP)


def NBTdata(name, size_name):
    return NBTAdapter(MetaField(name, lambda ctx: ctx[size_name]))


# item packing
slotdata = Struct("slotdata",
                  SBInt16("id"),
                  If(lambda context: context["id"] >= 0,
                     Embed(Struct("item_information",
                                  UBInt8("count"),
                                  UBInt16("damage"),
                                  SBInt16("size"),
                                  If(lambda context: context["size"] >= 0,
                                     NBTdata("data", size_name="size")
                                     )
                                  ))
                     )
                  )


def itemstack_as_slotdata(itemstack):
    if itemstack is None:
        data = {'id': -1}
    else:
        data = {'id': itemstack.number,
                'count': itemstack.count,
                'damage': itemstack.meta}
        if itemstack.nbt is None:
            data['size'] = -1
            data['data'] = None
        else:
            nbt = itemstack.nbt
            f = StringIO()
            nbt.save(f)
            nbt_data = f.getvalue()
            data['size'] = len(nbt_data)
            data['data'] = nbt_data
    return Container(**data)


Metadata = namedtuple("Metadata", "type value")
metadata_types = ["byte", "short", "int", "float", "string16", "slotdata",
                  "int_tup"]

# Metadata adaptor.


class MetadataAdapter(Adapter):

    def _decode(self, obj, context):
        d = {}
        for m in obj.data:
            d[m.id.identifier] = Metadata(metadata_types[m.id.data_type], m.value)
        return d

    def _encode(self, obj, context):
        c = Container(data=[], terminator=None)
        for k, v in obj.iteritems():
            t, value = v
            d = Container(
                id=Container(data_type=metadata_types.index(t), identifier=k),
                value=value,
                peeked=None)
            c.data.append(d)
        c.data[-1].peeked = 127
        return c

# Metadata inner container.
metadata_switch = {