import struct
import zlib

OP_CALLBACK = 0x19

PY3 = (bytes != str)
if PY3:
    long_type = int
    text_type = str
    byte_type = bytes
    def b_ord(x):
        return x if isinstance(x, int) else ord(x)
else:
    long_type = long
    text_type = unicode
    byte_type = str
    def b_ord(x):
        return ord(x)

OP_NONE          = b"\x01"
OP_GLOBAL        = b"\x02"
OP_INT64         = b"\x03"
OP_INT32         = b"\x04"
OP_INT16         = b"\x05"
OP_INT8          = b"\x06"
OP_MINUS_ONE     = b"\x07"
OP_ZERO          = b"\x08"
OP_ONE           = b"\x09"
OP_FLOAT         = b"\x0a"
OP_FLOAT_ZERO    = b"\x0b"
OP_EMPTY_STR     = b"\x0e"
OP_STR_CHAR      = b"\x0f"
OP_UNICODE       = b"\x12"
OP_STR_SHORT     = b"\x13"
OP_TUPLE_N       = b"\x14"
OP_LIST_N        = b"\x15"
OP_DICT          = b"\x16"
OP_INSTANCE      = b"\x17"
OP_TRUE          = b"\x1f"
OP_FALSE         = b"\x20"
OP_OBJECT        = b"\x22"
OP_TUPLE_EMPTY   = b"\x24"
OP_TUPLE_1       = b"\x25"
OP_LIST_1        = b"\x27"
OP_SUBSTREAM     = b"\x2b"
OP_TUPLE_2       = b"\x2c"
OP_LIST_EMPTY    = b"\x15\x00"
OP_UNICODE_UTF8  = b"\x2e"
OP_LONG          = b"\x2f"
OP_UNICODE_1     = b"\x30"
OP_STR_LONG      = b"\x32"

# Standard EVE Carbon String Table (1-indexed)
STRING_TABLE = (
    None,
    "*corpid", "*locationid", "age", "Asteroid", "authentication", "ballpark", "beyonce", "bloodlineID",
    "capacity", "categoryID", "character", "charid", "charMgr", "chat", "clientID", "config", "contraband",
    "corporationID", "createDateTime", "customInfo", "description", "divisionID", "DoDestinyUpdate",
    "dogmaIM", "EVE System", "flag", "foo.SlimItem", "gangID", "Gemini", "gender", "graphicID", "groupID",
    "header", "idName", "invbroker", "itemID", "items", "jumps", "line", "lines", "locationID", "locationName",
    "macho.CallGPCS", "macho.CallRsp", "macho.MachoAddress", "macho.Notification", "macho.SessionChangeNotification",
    "modules", "name", "objectID", "ownerID", "owners", "paperdoll", "password", "portionSize", "position",
    "quantity", "raceID", "row", "rowset", "security", "securityStatus", "sessionchange", "singleton",
    "skillMgr", "solarSystemID", "solarsystemid", "solarsystemid2", "source", "splash", "station", "stationID",
    "stationid", "stationSvc", "structural", "structure", "target", "typeID", "user", "userID", "userid",
    "util.CachedObject", "util.IndexRowset", "util.Moniker", "util.Row", "util.Rowset", "*multicastID",
    "AddBalls", "alert", "allianceID", "allianceid", "allianceshortname", "attributeID", "attributeValue",
    "bookmark", "bounty", "channel", "charid", "constellationid", "corpid", "corprole", "damage", "duration",
    "effects.Laser", "gangid", "gangrole", "hqID", "implants", "languageID", "locationid", "machoNet",
    "mass", "maxVelocity", "modules", "name", "nodeID", "online", "orbitID", "ownerID", "penalty",
    "radius", "regionid", "role", "rolesAtAll", "rolesAtBase", "rolesAtHQ", "rolesAtOther", "sc",
    "security", "service", "sessionID", "shipid", "Skills", "slimItem", "solarSystemID", "solarsystemid",
    "solarsystemid2", "standing", "stationid", "stationid2", "targetID", "time", "title", "total",
    "typeID", "userType", "val", "volume", "warFactionID", "x", "y", "z",
)

class UniStr(object):
    def __init__(self, val):
        if isinstance(val, bytes):
            self.val = val.decode("utf-8", "replace")
        else:
            self.val = text_type(val)
    def __repr__(self):
        return "u%r" % (self.val,)

class GlobalSymbol(object):
    def __init__(self, name):
        self.name = name

NEW_STYLE_CLASSES = (
    "carbon.common.script.sys.row.Row",
    "blue.DBRowDescriptor",
    "blue.DBRow",
)

class MachoObject(object):
    def __init__(self, class_name, state, args=(), new_style=None):
        self.class_name = class_name
        self.state = state
        self.args = args
        self.new_style = (class_name in NEW_STYLE_CLASSES) if new_style is None else new_style
    def __repr__(self):
        return "MachoObject(%r, %r)" % (self.class_name, self.state)

class BoundObject(object):
    """Represents a MachoNet pass-by-reference bound object (opcode 0x19 / OID+)."""
    def __init__(self, object_id, ref_id, public_attrs=None):
        self.object_id = object_id
        self.ref_id = ref_id
        self.public_attrs = public_attrs or {}
    def __repr__(self):
        return "BoundObject(%r, %r)" % (self.object_id, self.ref_id)

class SubStream(object):
    def __init__(self, value):
        self.value = value
    def __repr__(self):
        return "SubStream(%r)" % (self.value,)

class RawStream(object):
    def __init__(self, raw_bytes):
        self.raw_bytes = raw_bytes
    def __repr__(self):
        return "RawStream(%d bytes)" % len(self.raw_bytes)

def _pack_ex_len(n):
    if n < 255:
        return struct.pack("<B", n)
    return b"\xff" + struct.pack("<I", n)

def _unpack_ex_len(data, pos):
    b = b_ord(data[pos[0]])
    pos[0] += 1
    if b == 0xFF:
        val = struct.unpack_from("<I", data, pos[0])[0]
        pos[0] += 4
        return val
    return b

def dumps(obj):
    if isinstance(obj, RawStream):
        return obj.raw_bytes

    def _enc(v):
        if v is None:
            return OP_NONE
        elif v is True:
            return OP_TRUE
        elif v is False:
            return OP_FALSE
        elif isinstance(v, float):
            if v == 0.0:
                return OP_FLOAT_ZERO
            return OP_FLOAT + struct.pack("<d", v)
        elif isinstance(v, (int, long_type)):
            if v == 0:
                return OP_ZERO
            elif v == 1:
                return OP_ONE
            elif v == -1:
                return OP_MINUS_ONE
            elif -128 <= v <= 127:
                return OP_INT8 + struct.pack("<b", v)
            elif -32768 <= v <= 32767:
                return OP_INT16 + struct.pack("<h", v)
            elif -2147483648 <= v <= 2147483647:
                return OP_INT32 + struct.pack("<i", v)
            else:
                return OP_INT64 + struct.pack("<q", v)
        elif isinstance(v, UniStr):
            u8 = v.val.encode("utf-8")
            return OP_UNICODE_UTF8 + _pack_ex_len(len(u8)) + u8
        elif not PY3 and isinstance(v, text_type):
            u8 = v.encode("utf-8")
            return OP_UNICODE_UTF8 + _pack_ex_len(len(u8)) + u8
        elif isinstance(v, (bytes, str)):
            raw = v.encode("latin1") if (PY3 and isinstance(v, str)) else v
            n = len(raw)
            if n == 0:
                return OP_EMPTY_STR
            elif n == 1:
                return OP_STR_CHAR + raw
            elif n < 255:
                return OP_STR_SHORT + struct.pack("<B", n) + raw
            else:
                return OP_STR_LONG + _pack_ex_len(n) + raw
        elif isinstance(v, GlobalSymbol):
            raw = v.name.encode("latin1") if (PY3 and isinstance(v.name, str)) else v.name
            return OP_GLOBAL + _pack_ex_len(len(raw)) + raw
        elif isinstance(v, MachoObject):
            if v.new_style:
                args = tuple(v.args)
                if not args and isinstance(v.state, dict) and "header" in v.state and "line" in v.state:
                    args = (v.state["header"], v.state["line"])
                hdr = (GlobalSymbol(v.class_name), args)
                return OP_OBJECT + _enc(hdr) + b"\x2d\x2d"
            return OP_INSTANCE + _enc(v.class_name) + _enc(v.state)
        elif isinstance(v, BoundObject):
            inner_tuple = (v.object_id, v.public_attrs, v.ref_id) if v.public_attrs else (v.object_id, v.ref_id)
            inner_bytes = dumps(inner_tuple)
            return b"\x19" + _enc(inner_bytes)
        elif isinstance(v, SubStream):
            inner = dumps(v.value)
            return OP_SUBSTREAM + _pack_ex_len(len(inner)) + inner
        elif isinstance(v, RawStream):
            return v.raw_bytes
        elif isinstance(v, tuple):
            n = len(v)
            body = b"".join(_enc(x) for x in v)
            if n == 0:
                return OP_TUPLE_EMPTY
            elif n == 1:
                return OP_TUPLE_1 + body
            elif n == 2:
                return OP_TUPLE_2 + body
            else:
                return OP_TUPLE_N + _pack_ex_len(n) + body
        elif isinstance(v, list):
            n = len(v)
            if n == 0:
                return OP_LIST_EMPTY
            elif n == 1:
                return OP_LIST_1 + _enc(v[0])
            return OP_LIST_N + _pack_ex_len(n) + b"".join(_enc(x) for x in v)
        elif isinstance(v, dict):
            items = b"".join(_enc(val) + _enc(k) for k, val in v.items())
            return OP_DICT + _pack_ex_len(len(v)) + items
        raise TypeError("Unsupported type for macho_marshal: %r" % type(v))

    return b"\x7e\x00\x00\x00\x00" + _enc(obj)

def _bytes_to_str(raw):
    if not PY3:
        return raw
    return raw.decode("latin1")

def loads(data):
    if not data:
        return None
    if b_ord(data[0]) == 0x78:
        try:
            data = zlib.decompress(data)
        except Exception as e:
            return ("<ZlibDecompressError>", str(e))
    if b_ord(data[0]) != 0x7e:
        return ("<RawNonMarshalPacket>", data[:64])

    pos = [5]
    shared_refs = []

    def _dec():
        op = b_ord(data[pos[0]])
        pos[0] += 1
        save_ref = bool(op & 0x40)
        op_type = op & 0x3F
        ref_slot = None
        if save_ref:
            ref_slot = len(shared_refs)
            shared_refs.append(None)

        if op_type == 0x01:
            res = None
        elif op_type == 0x02:
            slen = _unpack_ex_len(data, pos)
            res = "<Global:" + _bytes_to_str(data[pos[0]:pos[0]+slen]) + ">"
            pos[0] += slen
        elif op_type == 0x03:
            res = struct.unpack_from("<q", data, pos[0])[0]
            pos[0] += 8
        elif op_type == 0x04:
            res = struct.unpack_from("<i", data, pos[0])[0]
            pos[0] += 4
        elif op_type == 0x05:
            res = struct.unpack_from("<h", data, pos[0])[0]
            pos[0] += 2
        elif op_type == 0x06:
            res = struct.unpack_from("<b", data, pos[0])[0]
            pos[0] += 1
        elif op_type == 0x07:
            res = -1
        elif op_type == 0x08:
            res = 0
        elif op_type == 0x09:
            res = 1
        elif op_type == 0x0a:
            res = struct.unpack_from("<d", data, pos[0])[0]
            pos[0] += 8
        elif op_type == 0x0b:
            res = 0.0
        elif op_type == 0x0e:
            res = ""
        elif op_type == 0x0f:
            res = _bytes_to_str(data[pos[0]:pos[0]+1])
            pos[0] += 1
        elif op_type in (0x10, 0x11):
            idx = b_ord(data[pos[0]])
            pos[0] += 1
            if False and STRING_TABLE[idx] is not None:
                res = STRING_TABLE[idx]
            else:
                res = "<StrTableRef:0x%02x>" % idx
        elif op_type in (0x13, 0x32):
            slen = _unpack_ex_len(data, pos)
            res = _bytes_to_str(data[pos[0]:pos[0]+slen])
            pos[0] += slen
        elif op_type == 0x12:
            ulen = _unpack_ex_len(data, pos)
            res = UniStr(data[pos[0]:pos[0]+ulen*2].decode("utf-16le", "replace"))
            pos[0] += ulen * 2
        elif op_type == 0x17:
            cls_name = _dec()
            state = _dec()
            res = MachoObject(cls_name, state)
        elif op_type == 0x19:
            res = BoundObject(_dec(), 0)
        elif op_type == 0x1f:
            res = True
        elif op_type == 0x20:
            res = False
        elif op_type == 0x22:
            res = ("<Object_0x22>", _dec())
        elif op_type in (0x23, 0x2b):
            slen = _unpack_ex_len(data, pos)
            sub = data[pos[0]:pos[0]+slen]
            pos[0] += slen
            res = SubStream(loads(sub))
        elif op_type == 0x2e:
            ulen = _unpack_ex_len(data, pos)
            res = UniStr(data[pos[0]:pos[0]+ulen].decode("utf-8", "replace"))
            pos[0] += ulen
        elif op_type == 0x2f:
            blen = _unpack_ex_len(data, pos)
            raw_bytes = data[pos[0]:pos[0]+blen]
            pos[0] += blen
            if blen == 8:
                res = struct.unpack("<q", raw_bytes)[0]
            else:
                res = int.from_bytes(raw_bytes, "little", signed=True) if PY3 else 0
        elif op_type == 0x30:
            res = UniStr(data[pos[0]:pos[0]+2].decode("utf-16le", "replace"))
            pos[0] += 2
        elif op_type == 0x14:
            tlen = _unpack_ex_len(data, pos)
            res = tuple(_dec() for _ in range(tlen))
        elif op_type in (0x24, 0x2a):
            res = ()
        elif op_type == 0x25:
            res = (_dec(),)
        elif op_type == 0x27:
            res = [_dec()]
        elif op_type == 0x2c:
            res = (_dec(), _dec())
        elif op_type == 0x15:
            llen = _unpack_ex_len(data, pos)
            res = [_dec() for _ in range(llen)]
        elif op_type in (0x26, 0x2d):
            res = []
        elif op_type == 0x28:
            res = [_dec(), _dec()]
        elif op_type == 0x16:
            dlen = _unpack_ex_len(data, pos)
            d = {}
            for _ in range(dlen):
                val = _dec()
                key = _dec()
                if isinstance(key, list):
                    key = tuple(key)
                elif not isinstance(key, (str, int, float, tuple, bool, type(None))):
                    key = str(key)
                d[key] = val
            res = d
        elif op_type == 0x1b:
            ref_idx = _unpack_ex_len(data, pos)
            res = shared_refs[ref_idx - 1] if 0 < ref_idx <= len(shared_refs) else ("<Ref:%d>" % ref_idx)
        else:
            res = "<UnparsedOpcode 0x%02x at offset %d>" % (op, pos[0]-1)

        if ref_slot is not None:
            shared_refs[ref_slot] = res
        return res

    return _dec()

def send_packet(sock, payload_bytes):
    frame = struct.pack("<I", len(payload_bytes)) + payload_bytes
    sock.sendall(frame)
    return frame

def recv_packet(sock):
    hdr = bytearray()
    while len(hdr) < 4:
        chunk = sock.recv(4 - len(hdr))
        if not chunk:
            return None
        hdr.extend(chunk)
    pkt_len = struct.unpack("<I", hdr)[0]
    data = bytearray()
    while len(data) < pkt_len:
        chunk = sock.recv(min(65536, pkt_len - len(data)))
        if not chunk:
            break
        data.extend(chunk)
    return bytes(data)