import sys
import struct
import logging
import uuid
from enum import IntEnum, Enum
import KL
from KL import hand_protocol, types
from common.utils import Buffer

if sys.version_info.major > 2:
    unicode = str

logger = logging.getLogger('__name__')


def test_bit(data, offset):
    mask = 1 << offset
    return data & mask


def set_bit(data, offset):
    mask = 1 << offset
    return data | mask


class _Primitive1(object):
    def __init__(self, fmt):
        self._fmt = fmt
        st = struct.Struct(fmt.format(1))
        self.size = st.size
        self.format = st.format

    def pack(self, data):
        return struct.pack(self.format, data)

    def unpack(self, data):
        return struct.unpack(self.format, data.read(self.size))[0]

    def pack_array(self, data):
        if data is None:
            return Primitives.Int32.pack(-1)
        if not isinstance(data, list):
            logger.warning(
                'binary.py > _Primitive1 > pack_array > data: {0} is not a instance of "list"!'.format(data))
            return Primitives.Int32.pack(-1)  # to prevent crashing while runtime
        sizedata = Primitives.Int32.pack(len(data))
        return sizedata + struct.pack(self._fmt.format(len(data)), *data)

    def unpack_array(self, data, length):
        if length == -1:
            return None
        if length == 0:
            return ()
        return struct.unpack(self._fmt.format(length), data.read(self.size * length))


class Primitives1(object):
    SByte = _Primitive1("<{:d}b")
    Int16 = _Primitive1("<{:d}h")
    Int32 = _Primitive1("<{:d}i")
    Int64 = _Primitive1("<{:d}q")
    Byte = _Primitive1("<{:d}B")
    Char = Byte
    UInt16 = _Primitive1("<{:d}H")
    UInt32 = _Primitive1("<{:d}I")
    UInt64 = _Primitive1("<{:d}Q")
    Boolean = _Primitive1("<{:d}?")
    Double = _Primitive1("<{:d}d")
    Float = _Primitive1("<{:d}f")


# class _DateTime(object):
#     @staticmethod
#     def pack(dt):
#         epch = KL.datetime_to_win_epoch(dt)
#         return Primitives.Int64.pack(epch)
#
#     @staticmethod
#     def unpack(data):
#         epch = Primitives.Int64.unpack(data)
#         return ua.win_epoch_to_datetime(epch)


class _Bytes(object):
    @staticmethod
    def pack(data):
        if data is None:
            return Primitives.Int32.pack(-1)
        length = len(data)
        return Primitives.Int32.pack(length) + data

    @staticmethod
    def unpack(data):
        length = Primitives.Int32.unpack(data)
        if length == -1:
            return None
        return data.read(length)


class _String(object):
    @staticmethod
    def pack(string):
        if string is not None:
            if sys.version_info.major > 2:
                string = string.encode('utf-8')
            else:
                # we do not want this test to happen with python3
                if isinstance(string, unicode):
                    string = string.encode('utf-8')
        return _Bytes.pack(string)

    @staticmethod
    def unpack(data):
        b = _Bytes.unpack(data)
        if sys.version_info.major < 3:
            # return unicode(b)  #might be correct for python2 but would complicate tests for python3
            return b
        else:
            if b is None:
                return b
            return b.decode("utf-8")


class _Null(object):
    @staticmethod
    def pack(data):
        return b""

    @staticmethod
    def unpack(data):
        return None


class _Guid(object):
    @staticmethod
    def pack(guid):
        # convert python UUID 6 field format to OPC UA 4 field format
        f1 = Primitives.UInt32.pack(guid.time_low)
        f2 = Primitives.UInt16.pack(guid.time_mid)
        f3 = Primitives.UInt16.pack(guid.time_hi_version)
        f4a = Primitives.Byte.pack(guid.clock_seq_hi_variant)
        f4b = Primitives.Byte.pack(guid.clock_seq_low)
        f4c = struct.pack('>Q', guid.node)[2:8]  # no primitive .pack available for 6 byte int
        f4 = f4a + f4b + f4c
        # concat byte fields
        b = f1 + f2 + f3 + f4

        return b

    @staticmethod
    def unpack(data):
        # convert OPC UA 4 field format to python UUID bytes
        f1 = struct.pack('>I', Primitives.UInt32.unpack(data))
        f2 = struct.pack('>H', Primitives.UInt16.unpack(data))
        f3 = struct.pack('>H', Primitives.UInt16.unpack(data))
        f4 = data.read(8)
        # concat byte fields
        b = f1 + f2 + f3 + f4

        return uuid.UUID(bytes=b)


class Primitives(Primitives1):
    Null = _Null()
    String = _String()
    Bytes = _Bytes()
    ByteString = _Bytes()
    CharArray = _String()
    # DateTime = _DateTime()
    Guid = _Guid()


def header_to_binary(hdr):
    b = []
    b.append(struct.pack("<3ss", hdr.MessageType, hdr.ChunkType))
    size = hdr.body_size + 8
    if hdr.MessageType in (hand_protocol.MessageType.SecureOpen, hand_protocol.MessageType.SecureClose,
                           hand_protocol.MessageType.SecureMessage):
        size += 4
    b.append(Primitives.UInt32.pack(size))
    if hdr.MessageType in (hand_protocol.MessageType.SecureOpen, hand_protocol.MessageType.SecureClose,
                           hand_protocol.MessageType.SecureMessage):
        b.append(Primitives.UInt32.pack(hdr.ChannelId))
    return b"".join(b)


def struct_to_binary(obj):
    packet = []
    has_switch = hasattr(obj, "switches")
    if has_switch:
        for name, switch in obj.switches.items():
            member = getattr(obj, name)
            container_name, idx = switch
            if member is not None:
                container_val = getattr(obj, container_name)
                container_val = container_val | 1 << idx
                setattr(obj, container_name, container_val)
    for name, utype in obj.types:
        val = getattr(obj, name)
        if utype.startswith("ListOf"):
            packet.append(list_to_binary(utype[6:], val))
        else:
            if has_switch and val is None and name in obj.switches:
                pass
            else:
                packet.append(to_binary(utype, val))
    return b''.join(packet)


def to_binary(utype, val):
    if utype.startswith("ListOf"):
        # if isinstance(val, (list, tuple)):
        return list_to_binary(utype[6:], val)
    elif isinstance(utype, (str, unicode)) and hasattr(types.VariantType, utype):
        vtype = getattr(types.VariantType, utype)
        return pack_utype(vtype, val)
    elif isinstance(utype, (str, unicode)) and hasattr(Primitives, utype):
        return getattr(Primitives, utype).pack(val)
    elif isinstance(val, (IntEnum, Enum)):
        return Primitives.UInt32.pack(val.value)
    elif isinstance(val, types.NodeId):
        return nodeid_to_binary(val)
    elif isinstance(val, types.Variant):
        return variant_to_binary(val)
    elif hasattr(val, "ua_types"):
        return struct_to_binary(val)
    else:
        raise Exception("No known way to pack {} of type {} to ua binary".format(val, utype))


def nodeid_to_binary(nodeid):
    data = None
    if nodeid.NodeIdType == types.NodeIdType.TwoByte:
        return struct.pack("<BB", nodeid.NodeIdType.value, nodeid.Identifier)
    elif nodeid.NodeIdType == types.NodeIdType.FourByte:
        return struct.pack("<BBH", nodeid.NodeIdType.value, nodeid.NamespaceIndex, nodeid.Identifier)
    elif nodeid.NodeIdType == types.NodeIdType.Numeric:
        data = struct.pack("<BHI", nodeid.NodeIdType.value, nodeid.NamespaceIndex, nodeid.Identifier)
    elif nodeid.NodeIdType == types.NodeIdType.String:
        data = struct.pack("<BH", nodeid.NodeIdType.value, nodeid.NamespaceIndex) + \
               Primitives.String.pack(nodeid.Identifier)
    elif nodeid.NodeIdType == types.NodeIdType.ByteString:
        data = struct.pack("<BH", nodeid.NodeIdType.value, nodeid.NamespaceIndex) + \
               Primitives.Bytes.pack(nodeid.Identifier)
    elif nodeid.NodeIdType == types.NodeIdType.Guid:
        data = struct.pack("<BH", nodeid.NodeIdType.value, nodeid.NamespaceIndex) + \
               Primitives.Guid.pack(nodeid.Identifier)
    else:
        raise Exception("Unknown NodeIdType: {} for NodeId: {}".format(nodeid.NodeIdType, nodeid))
    # Add NamespaceUri and ServerIndex in case we have an ExpandedNodeId
    if nodeid.NamespaceUri:
        data = bytearray(data)
        data[0] = set_bit(data[0], 7)
        data.extend(Primitives.String.pack(nodeid.NamespaceUri))
    if nodeid.ServerIndex:
        if not isinstance(data, bytearray):
            data = bytearray(data)
        data[0] = set_bit(data[0], 6)
        data.extend(Primitives.UInt32.pack(nodeid.ServerIndex))
    return data


def variant_to_binary(var):
    b = []
    encoding = var.VariantType.value & 0b111111
    if var.is_array or isinstance(var.Value, (list, tuple)):
        var.is_array = True
        encoding = set_bit(encoding, 7)
        if var.Dimensions is not None:
            encoding = set_bit(encoding, 6)
        b.append(Primitives.Byte.pack(encoding))
        b.append(pack_utype_array(var.VariantType, types.flatten(var.Value)))
        if var.Dimensions is not None:
            b.append(pack_utype_array(types.VariantType.Int32, var.Dimensions))
    else:
        b.append(Primitives.Byte.pack(encoding))
        b.append(pack_utype_array(var.VariantType, var.Value))

    return b"".join(b)


def pack_utype_array(vtype, array):
    if hasattr(Primitives1, vtype.name):
        dataType = getattr(Primitives1, vtype.name)
        return dataType.pack_array(array)
    if array is None:
        return b'\xff\xff\xff\xff'
    length = len(array)
    b = [pack_utype(vtype, val) for val in array]
    b.insert(0, Primitives.Int32.pack(length))
    return b"".join(b)


def pack_utype(vtype, value):
    if hasattr(Primitives, vtype.name):
        return getattr(Primitives, vtype.name).pack(value)
    elif vtype.value > 25:
        return Primitives.Bytes.pack(value)
    elif vtype == types.VariantType.ExtensionObject:
        return extensionobject_to_binary(value)
    elif vtype in (types.VariantType.NodeId, types.VariantType.ExpandedNodeId):
        return nodeid_to_binary(value)
    elif vtype == types.VariantType.Variant:
        return variant_to_binary(value)
    else:
        return struct_to_binary(value)


def extensionobject_to_binary(obj):
    """
    Convert Python object to binary-coded ExtensionObject.
    If obj is None, convert to empty ExtensionObject (TypeId=0, no Body).
    Returns a binary string
    """
    if isinstance(obj, types.ExtensionObject):
        return struct_to_binary(obj)
    if obj is None:
        TypeId = types.NodeId()
        Encoding = 0
        Body = None
    else:
        TypeId = types.extension_object_ids[obj.__class__.__name__]
        Encoding = 0x01
        Body = struct_to_binary(obj)
    packet = []
    packet.append(nodeid_to_binary(TypeId))
    packet.append(Primitives.Byte.pack(Encoding))
    if Body:
        packet.append(Primitives.Bytes.pack(Body))
    return b''.join(packet)


def list_to_binary(utype, val):
    if val is None:
        return Primitives.Int32.pack(-1)
    if hasattr(Primitives1, utype):
        dataType = getattr(Primitives1, utype)
        return dataType.pack_array(val)
    datasize = Primitives.Int32.pack(len(val))
    pack = [to_binary(utype, el) for el in val]
    pack.insert(0, datasize)
    return b''.join(pack)


def struct_from_binary(objtype, data):
    """
    unpack an ua struct. Arguments are an objtype as Python class or string
    """
    if isinstance(objtype, (unicode, str)):
        objtype = getattr(KL, objtype)
    if issubclass(objtype, Enum):
        return objtype(Primitives.UInt32.unpack(data))
    obj = objtype()
    for name, utype in obj.types:
        # if our member has a swtich and it is not set we skip it
        if hasattr(obj, "ua_switches") and name in obj.switches:
            container_name, idx = obj.switches[name]
            val = getattr(obj, container_name)
            if not test_bit(val, idx):
                continue
        val = from_binary(utype, data)
        setattr(obj, name, val)
    return obj


def from_binary(utype, data):
    """
    unpack data given an uatype as a string or a python class having a ua_types memeber
    """
    if isinstance(utype, (str, unicode)) and utype.startswith("ListOf"):
        type = utype[6:]
        if hasattr(types.VariantType, type):
            vtype = getattr(types.VariantType, utype)
            return unpack_utype_array(vtype, data)
        size = Primitives.Int32.unpack(data)
        return [from_binary(utype, data) for _ in range(size)]
    elif isinstance(utype, (str, unicode)) and hasattr(types.VariantType, utype):
        vtype = getattr(types.VariantType, utype)
        return unpack_utype(vtype, data)
    elif isinstance(utype, (str, unicode)) and hasattr(Primitives, utype):
        return getattr(Primitives, utype).unpack(data)
    else:
        return struct_from_binary(utype, data)


def unpack_utype_array(vtype, data):
    length = Primitives.Int32.unpack(data)
    if length == -1:
        return None
    elif hasattr(Primitives1, vtype.name):
        dataType = getattr(Primitives1, vtype.name)
        # Remark: works without tuple conversion to list.
        return list(dataType.unpack_array(data, length))
    else:
        # Revert to slow serial unpacking.
        return [unpack_utype(vtype, data) for _ in range(length)]


def unpack_utype(vtype, data):
    if hasattr(Primitives, vtype.name):
        st = getattr(Primitives, vtype.name)
        return st.unpack(data)
    elif vtype.value > 25:
        return Primitives.Bytes.unpack(data)
    elif vtype == types.VariantType.ExtensionObject:
        return extensionobject_from_binary(data)
    elif vtype in (types.VariantType.NodeId, types.VariantType.ExpandedNodeId):
        return nodeid_from_binary(data)
    elif vtype == types.VariantType.Variant:
        return variant_from_binary(data)
    else:
        if hasattr(KL, vtype.name):
            klass = getattr(KL, vtype.name)
            return struct_from_binary(klass, data)
        else:
            raise Exception("Cannot unpack unknown variant type {0!s}".format(vtype))


def extensionobject_from_binary(data):
    typeid = nodeid_from_binary(data)
    Encoding = ord(data.read(1))
    body = None
    if Encoding & (1 << 0):
        length = Primitives.Int32.unpack(data)
        if length < 1:
            body = Buffer(b"")
        else:
            body = data.copy(length)
            data.skip(length)
    if typeid.Identifier == 0:
        return None
    elif typeid in types.extension_object_classes:
        klass = types.extension_object_classes[typeid]
        if body is None:
            raise Exception("parsing ExtensionObject {0} without data".format(klass.__name__))
        return from_binary(klass, body)
    else:
        e = types.ExtensionObject()
        e.TypeId = typeid
        e.Encoding = Encoding
        if body is not None:
            e.Body = body.read(len(body))
        return e


def nodeid_from_binary(data):
    nid = types.NodeId()
    encoding = ord(data.read(1))
    nid.NodeIdType = types.NodeIdType(encoding & 0b00111111)

    if nid.NodeIdType == types.NodeIdType.TwoByte:
        nid.Identifier = ord(data.read(1))
    elif nid.NodeIdType == types.NodeIdType.FourByte:
        nid.NamespaceIndex, nid.Identifier = struct.unpack("<BH", data.read(3))
    elif nid.NodeIdType == types.NodeIdType.Numeric:
        nid.NamespaceIndex, nid.Identifier = struct.unpack("<HI", data.read(6))
    elif nid.NodeIdType == types.NodeIdType.String:
        nid.NamespaceIndex = Primitives.UInt16.unpack(data)
        nid.Identifier = Primitives.String.unpack(data)
    elif nid.NodeIdType == types.NodeIdType.ByteString:
        nid.NamespaceIndex = Primitives.UInt16.unpack(data)
        nid.Identifier = Primitives.Bytes.unpack(data)
    elif nid.NodeIdType == types.NodeIdType.Guid:
        nid.NamespaceIndex = Primitives.UInt16.unpack(data)
        nid.Identifier = Primitives.Guid.unpack(data)
    else:
        raise Exception("Unknown NodeId encoding: " + str(nid.NodeIdType))

    if test_bit(encoding, 7):
        nid.NamespaceUri = Primitives.String.unpack(data)
    if test_bit(encoding, 6):
        nid.ServerIndex = Primitives.UInt32.unpack(data)

    return nid


def variant_from_binary(data):
    dimensions = None
    array = False
    encoding = ord(data.read(1))
    int_type = encoding & 0b00111111
    vtype = types.datatype_to_varianttype(int_type)
    if test_bit(encoding, 7):
        value = unpack_utype_array(vtype, data)
        array = True
    else:
        value = unpack_utype(vtype, data)
    if test_bit(encoding, 6):
        dimensions = unpack_utype_array(types.VariantType.Int32, data)
        value = _reshape(value, dimensions)
    return types.Variant(value, vtype, dimensions, is_array=array)


def _reshape(flat, dims):
    subdims = dims[1:]
    subsize = 1
    for i in subdims:
        if i == 0:
            i = 1
        subsize *= i
    while dims[0] * subsize > len(flat):
        flat.append([])
    if not subdims or subdims == [0]:
        return flat
    return [_reshape(flat[i:i + subsize], subdims) for i in range(0, len(flat), subsize)]
