# Get the original here: https://gitorious.org/python-storable/python-storable/
# Embed the library until a public version on PyPI is available

#
# License
#
# python storable is distributed under the zlib/libpng license, which is OSS
# (Open Source Software) compliant.
#
# Copyright (C) 2009 Tim Aerts
#
# This software is provided 'as-is', without any express or implied
# warranty.  In no event will the authors be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
#    claim that you wrote the original software. If you use this software
#    in a product, an acknowledgment in the product documentation would be
#    appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
#    misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
#
# Tim Aerts <aardbeiplantje@gmail.com>
#

from struct import unpack

try:
    from io import BytesIO
except ImportError:
    from cStringIO import StringIO as BytesIO


def _read_size(fh, cache):
    return unpack(cache["size_unpack_fmt"], fh.read(4))[0]


def SX_OBJECT(fh, cache):
    # idx's are always big-endian dumped by storable's freeze/nfreeze I think
    i = unpack(">I", fh.read(4))[0]
    cache["has_sx_object"] = True
    return (0, i)


def SX_LSCALAR(fh, cache):
    return fh.read(_read_size(fh, cache))


def SX_LUTF8STR(fh, cache):
    return SX_LSCALAR(fh, cache)


def SX_ARRAY(fh, cache):
    data = []
    for i in range(0, _read_size(fh, cache)):
        data.append(process_item(fh, cache))

    return data


def SX_HASH(fh, cache):
    data = {}
    for i in range(0, _read_size(fh, cache)):
        value = process_item(fh, cache)
        key = fh.read(_read_size(fh, cache))
        data[key] = value

    return data


def SX_REF(fh, cache):
    return process_item(fh, cache)


def SX_UNDEF(fh, cache):
    return None


def SX_DOUBLE(fh, cache):
    return unpack(cache["double_unpack_fmt"], fh.read(8))[0]


def SX_BYTE(fh, cache):
    return unpack("B", fh.read(1))[0] - 128


def SX_NETINT(fh, cache):
    return unpack(">I", fh.read(4))[0]


def SX_SCALAR(fh, cache):
    size = unpack("B", fh.read(1))[0]
    return fh.read(size)


def SX_UTF8STR(fh, cache):
    return SX_SCALAR(fh, cache)


def SX_TIED_ARRAY(fh, cache):
    return process_item(fh, cache)


def SX_TIED_HASH(fh, cache):
    return SX_TIED_ARRAY(fh, cache)


def SX_TIED_SCALAR(fh, cache):
    return SX_TIED_ARRAY(fh, cache)


def SX_SV_UNDEF(fh, cache):
    return None


def SX_BLESS(fh, cache):
    size = unpack("B", fh.read(1))[0]
    package_name = fh.read(size)
    cache["classes"].append(package_name)
    return process_item(fh, cache)


def SX_IX_BLESS(fh, cache):
    indx = unpack("B", fh.read(1))[0]
    package_name = cache["classes"][indx]
    return process_item(fh, cache)


def SX_OVERLOAD(fh, cache):
    return process_item(fh, cache)


def SX_TIED_KEY(fh, cache):
    data = process_item(fh, cache)
    key = process_item(fh, cache)
    return data


def SX_TIED_IDX(fh, cache):
    data = process_item(fh, cache)
    # idx's are always big-endian dumped by storable's freeze/nfreeze I think
    indx_in_array = unpack(">I", fh.read(4))[0]
    return data


def SX_HOOK(fh, cache):
    flags = unpack("B", fh.read(1))[0]

    while flags & int(0x40):  # SHF_NEED_RECURSE
        # print("SHF_NEED_RECURSE")
        dummy = process_item(fh, cache)
        # print(dummy)
        flags = unpack("B", fh.read(1))[0]
        # print("flags:"+str(flags))

    # print("recursive done")

    if flags & int(0x20):  # SHF_IDX_CLASSNAME
        # print("SHF_IDX_CLASSNAME")
        # print("where:"+str(fh.tell()))
        if flags & int(0x04):  # SHF_LARGE_CLASSLEN
            # print("SHF_LARGE_CLASSLEN")
            # TODO: test
            indx = unpack(">I", fh.read(4))[0]
        else:
            indx = unpack("B", fh.read(1))[0]
        # print("classindx:"+str(indx))
        package_name = cache["classes"][indx]
    else:
        # print("where:"+str(fh.tell()))
        if flags & int(0x04):  # SHF_LARGE_CLASSLEN
            # print("SHF_LARGE_CLASSLEN")
            # TODO: test
            # FIXME: is this actually possible?
            class_size = _read_size(fh, cache)
        else:
            class_size = unpack("B", fh.read(1))[0]
            # print("size:"+str(class_size))

        package_name = fh.read(class_size)
        cache["classes"].append(package_name)
        # print("size:"+str(class_size)+",package:"+str(package_name))

    arguments = {}

    str_size = 0
    if flags & int(0x08):  # SHF_LARGE_STRLEN
        # print("SHF_LARGE_STRLEN")
        str_size = _read_size(fh, cache)
    else:
        # print("where:"+str(fh.tell()))
        str_size = unpack("B", fh.read(1))[0]

    if str_size:
        frozen_str = fh.read(str_size)
        # print("size:"+str(str_size)+",frozen_str:"+str(frozen_str))
        arguments[0] = frozen_str

    list_size = 0
    if flags & int(0x80):  # SHF_HAS_LIST
        # print("SHF_HAS_LIST")
        if flags & int(0x10):  # SHF_LARGE_LISTLEN
            # print("SHF_LARGE_LISTLEN")
            # print("where:"+str(fh.tell()))
            list_size = _read_size(fh, cache)
        else:
            list_size = unpack("B", fh.read(1))[0]

    # print("list_size:"+str(list_size))
    for i in range(0, list_size):
        indx_in_array = unpack(">I", fh.read(4))[0]
        # print("indx:"+str(indx_in_array))
        if indx_in_array in cache["objects"]:
            arguments[i + 1] = cache["objects"][indx_in_array]
        else:
            arguments[i + 1] = None

    # FIXME: implement the real callback STORABLE_thaw() still, for now, just
    # return the dictionary 'arguments' as data
    type = flags & int(0x03)  # SHF_TYPE_MASK 0x03
    # print("flags:"+str(type))
    data = arguments
    if type == 3:  # SHT_EXTRA
        # TODO
        # print("SHT_EXTRA")
        pass
    if type == 0:  # SHT_SCALAR
        # TODO
        # print("SHT_SCALAR")
        pass
    if type == 1:  # SHT_ARRAY
        # TODO
        # print("SHT_ARRAY")
        pass
    if type == 2:  # SHT_HASH
        # TODO
        # print("SHT_HASH")
        pass

    return data


def SX_FLAG_HASH(fh, cache):
    # TODO: NOT YET IMPLEMENTED!!!!!!
    # print("SX_FLAG_HASH:where:"+str(fh.tell()))
    flags = unpack("B", fh.read(1))[0]
    size = _read_size(fh, cache)
    # print("size:"+str(size))
    # print("flags:"+str(flags))
    data = {}
    for i in range(0, size):
        value = process_item(fh, cache)
        flags = unpack("B", fh.read(1))[0]
        keysize = _read_size(fh, cache)
        key = None
        if keysize:
            key = fh.read(keysize)
        data[key] = value

    return data


# *AFTER* all the subroutines
engine = {
    "\x00": SX_OBJECT,  # ( 0): Already stored object
    "\x01": SX_LSCALAR,  # ( 1): Scalar (large binary) follows (length, data)
    "\x02": SX_ARRAY,  # ( 2): Array forthcoming (size, item list)
    "\x03": SX_HASH,  # ( 3): Hash forthcoming (size, key/value pair list)
    "\x04": SX_REF,  # ( 4): Reference to object forthcoming
    "\x05": SX_UNDEF,  # ( 5): Undefined scalar
    "\x07": SX_DOUBLE,  # ( 7): Double forthcoming
    "\x08": SX_BYTE,  # ( 8): (signed) byte forthcoming
    "\x09": SX_NETINT,  # ( 9): Integer in network order forthcoming
    "\x0a": SX_SCALAR,  # (10): Scalar (binary, small) follows (length, data)
    "\x0b": SX_TIED_ARRAY,  # (11): Tied array forthcoming
    "\x0c": SX_TIED_HASH,  # (12): Tied hash forthcoming
    "\x0d": SX_TIED_SCALAR,  # (13): Tied scalar forthcoming
    "\x0e": SX_SV_UNDEF,  # (14): Perl's immortal PL_sv_undef
    "\x11": SX_BLESS,  # (17): Object is blessed
    "\x12": SX_IX_BLESS,  # (18): Object is blessed, classname given by index
    "\x13": SX_HOOK,  # (19): Stored via hook, user-defined
    "\x14": SX_OVERLOAD,  # (20): Overloaded reference
    "\x15": SX_TIED_KEY,  # (21): Tied magic key forthcoming
    "\x16": SX_TIED_IDX,  # (22): Tied magic index forthcoming
    "\x17": SX_UTF8STR,  # (23): UTF-8 string forthcoming (small)
    "\x18": SX_LUTF8STR,  # (24): UTF-8 string forthcoming (large)
    "\x19": SX_FLAG_HASH,  # (25): Hash with flags forthcoming (size, flags, key/flags/value triplet list)
}

exclude_for_cache = dict(
    {"\x00": True, "\x0b": True, "\x0c": True, "\x0d": True, "\x11": True, "\x12": True}
)


def handle_sx_object_refs(cache, data):
    iterateelements = None
    if type(data) is list:
        iterateelements = enumerate(iter(data))
    elif type(data) is dict:
        iterateelements = data.iteritems()
    else:
        return

    for k, item in iterateelements:
        if type(item) is list or type(item) is dict:
            handle_sx_object_refs(cache, item)
        if type(item) is tuple:
            data[k] = cache["objects"][item[1]]
    return data


def process_item(fh, cache):
    magic_type = fh.read(1)
    # print('magic:'+str(unpack('B',magic_type)[0])+",where:"+str(fh.tell())+',will do:'+str(engine[magic_type]))
    if magic_type not in exclude_for_cache:
        i = cache["objectnr"]
        cache["objectnr"] = cache["objectnr"] + 1
        # print("set i:"+str(i))
        cache["objects"][i] = engine[magic_type](fh, cache)
        # print("set i:"+str(i)+",to:"+str(cache['objects'][i]))
        return cache["objects"][i]
    else:
        return engine[magic_type](fh, cache)


def thaw(frozen_data):
    fh = BytesIO(frozen_data)
    data = deserialize(fh)
    fh.close()
    return data


def retrieve(file):
    fh = open(file, "rb")
    ignore = fh.read(4)
    data = None
    if ignore == "pst0":
        data = deserialize(fh)
    fh.close()
    return data


def deserialize(fh):
    magic = fh.read(1)
    byteorder = ">"
    if magic == "\x05":
        version = fh.read(1)
        # print("OK:nfreeze")
        # pass
    if magic == "\x04":
        version = fh.read(1)
        size = unpack("B", fh.read(1))[0]
        byteorder = fh.read(size)
        # print("OK:freeze:" + str(byteorder))

        # 32-bit ppc:     4321
        # 32-bit x86:     1234
        # 64-bit x86_64:  12345678

        if byteorder == "1234" or byteorder == "12345678":
            byteorder = "<"
        else:
            byteorder = ">"

        somethingtobeinvestigated = fh.read(4)

    # print('version:'+str(unpack('B', version)[0]));
    cache = {
        "objects": {},
        "objectnr": 0,
        "classes": [],
        "has_sx_object": False,
        "size_unpack_fmt": byteorder + "I",
        "double_unpack_fmt": byteorder + "d",
    }
    data = process_item(fh, cache)

    if cache["has_sx_object"]:
        handle_sx_object_refs(cache, data)

    return data
