#!/usr/bin/env python3
# coding=utf-8

#
#   Copyright (c) 2020 Project CHIP Authors
#   Copyright (c) 2019-2020 Google LLC.
#   All rights reserved.
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#

#
#   @file
#         This file contains definitions for working with data encoded in Chip TLV format
#


from __future__ import absolute_import, print_function

import struct
from collections import OrderedDict
from collections.abc import Mapping, Sequence
from enum import Enum

from .tlvlist import TLVList

TLV_TYPE_SIGNED_INTEGER = 0x00
TLV_TYPE_UNSIGNED_INTEGER = 0x04
TLV_TYPE_BOOLEAN = 0x08
TLV_TYPE_FLOATING_POINT_NUMBER = 0x0A
TLV_TYPE_UTF8_STRING = 0x0C
TLV_TYPE_BYTE_STRING = 0x10
TLV_TYPE_NULL = 0x14
TLV_TYPE_STRUCTURE = 0x15
TLV_TYPE_ARRAY = 0x16
TLV_TYPE_PATH = 0x17

TLV_TAG_CONTROL_ANONYMOUS = 0x00
TLV_TAG_CONTROL_CONTEXT_SPECIFIC = 0x20
TLV_TAG_CONTROL_COMMON_PROFILE_2Bytes = 0x40
TLV_TAG_CONTROL_COMMON_PROFILE_4Bytes = 0x60
TLV_TAG_CONTROL_IMPLICIT_PROFILE_2Bytes = 0x80
TLV_TAG_CONTROL_IMPLICIT_PROFILE_4Bytes = 0xA0
TLV_TAG_CONTROL_FULLY_QUALIFIED_6Bytes = 0xC0
TLV_TAG_CONTROL_FULLY_QUALIFIED_8Bytes = 0xE0

TLVBoolean_False = TLV_TYPE_BOOLEAN
TLVBoolean_True = TLV_TYPE_BOOLEAN + 1

TLVEndOfContainer = 0x18

INT8_MIN = -128
INT16_MIN = -32768
INT32_MIN = -2147483648
INT64_MIN = -9223372036854775808

INT8_MAX = 127
INT16_MAX = 32767
INT32_MAX = 2147483647
INT64_MAX = 9223372036854775807

UINT8_MAX = 255
UINT16_MAX = 65535
UINT32_MAX = 4294967295
UINT64_MAX = 18446744073709551615

ElementTypes = {
    0x00: "Signed Integer 1-byte value",
    0x01: "Signed Integer 2-byte value",
    0x02: "Signed Integer 4-byte value",
    0x03: "Signed Integer 8-byte value",
    0x04: "Unsigned Integer 1-byte value",
    0x05: "Unsigned Integer 2-byte value",
    0x06: "Unsigned Integer 4-byte value",
    0x07: "Unsigned Integer 8-byte value",
    0x08: "Boolean False",
    0x09: "Boolean True",
    0x0A: "Floating Point 4-byte value",
    0x0B: "Floating Point 8-byte value",
    0x0C: "UTF-8 String 1-byte length",
    0x0D: "UTF-8 String 2-byte length",
    0x0E: "UTF-8 String 4-byte length",
    0x0F: "UTF-8 String 8-byte length",
    0x10: "Byte String 1-byte length",
    0x11: "Byte String 2-byte length",
    0x12: "Byte String 4-byte length",
    0x13: "Byte String 8-byte length",
    0x14: "Null",
    0x15: "Structure",
    0x16: "Array",
    0x17: "Path",
    0x18: "End of Collection",
}

TagControls = {
    0x00: "Anonymous",
    0x20: "Context 1-byte",
    0x40: "Common Profile 2-byte",
    0x60: "Common Profile 4-byte",
    0x80: "Implicit Profile 2-byte",
    0xA0: "Implicit Profile 4-byte",
    0xC0: "Fully Qualified 6-byte",
    0xE0: "Fully Qualified 8-byte",
}


class uint(int):
    '''
    NewType will not return a class until Python 3.10, as Python 3.10 is not widely used,
    we still need to construct a class so it can work as a type.
    '''

    def __init__(self, val: int):
        if (val < 0):
            raise TypeError(
                'expecting positive value, got negative value of %d instead' % val)


class float32(float):
    ''' A type for single precision floats distinct from the double precision 'float'
        type offered by default in Python. This type distinction is present in the Matter
        data model types so we need it here as well.

        It is backed by an ordinary float, which means there will be precision loss at the time
        the value is converted to TLV.
    '''
    pass


class TLVWriter(object):
    def __init__(self, encoding=None, implicitProfile=None):
        self._encoding = encoding if encoding is not None else bytearray()
        self._implicitProfile = implicitProfile
        self._containerStack = []

    @property
    def encoding(self):
        """The object into which encoded TLV data is written.

        By default this is a bytearray object.
        """
        return self._encoding

    @encoding.setter
    def encoding(self, val):
        self._encoding = val

    @property
    def implicitProfile(self):
        """The Chip profile id used when encoding implicit profile tags.

        Setting this value will result in an implicit profile tag being encoded
        whenever the profile of the tag to be encoded matches the specified implicit
        profile id.

        Setting this value to None (the default) disabled encoding of implicit
        profile tags.
        """
        return self._implicitProfile

    @implicitProfile.setter
    def implicitProfile(self, val):
        self._implicitProfile = val

    def put(self, tag, val):
        """Write a value in TLV format with the specified TLV tag.

        val can be a Python object which will be encoded as follows:
        - Python bools, floats and strings are encoded as their respective TLV types.
        - Python ints are encoded as unsigned TLV integers if zero or positive; signed TLV
          integers if negative.
        - None is encoded as a TLV Null.
        - bytes and bytearray objects are encoded as TVL byte strings.
        - Mapping-like objects (e.g. dict) are encoded as TLV structures.  The keys of the
          map object are expected to be tag values, as described below for the tag argument.
          Map values are encoded recursively, using the same rules as defined for the val
          argument. The encoding order of elements depends on the type of the map object.
          Elements within a dict are automatically encoded tag numerical order. Elements
          within other forms of mapping object (e.g. OrderedDict) are encoded in the
          object's natural iteration order.
        - Sequence-like objects (e.g. arrays) are written as TLV arrays. Elements within
          the array are encoded recursively, using the same rules as defined for the val
          argument.

        tag can be a small int (0-255), a tuple of two integers, or None.
        If tag is an integer, it is encoded as a TLV context-specific tag.
        If tag is a two-integer tuple, it is encoded as a TLV profile-specific tag, with
          the first integer encoded as the profile id and the second as the tag number.
        If tag is None, it is encoded as a TLV anonymous tag.
        """
        if val is None:
            self.putNull(tag)
        elif isinstance(val, Enum):
            self.putUnsignedInt(tag, val)
        elif isinstance(val, bool):
            self.putBool(tag, val)
        elif isinstance(val, uint):
            self.putUnsignedInt(tag, val)
        elif isinstance(val, int):
            self.putSignedInt(tag, val)
        elif isinstance(val, float32):
            self.putFloat(tag, val)
        elif isinstance(val, float):
            self.putDouble(tag, val)
        elif isinstance(val, str):
            self.putString(tag, val)
        elif isinstance(val, bytes) or isinstance(val, bytearray):
            self.putBytes(tag, val)
        elif isinstance(val, Mapping):
            self.startStructure(tag)
            if type(val) == dict:
                val = OrderedDict(
                    sorted(val.items(),
                           key=lambda item: tlvTagToSortKey(item[0]))
                )
            for containedTag, containedVal in val.items():
                self.put(containedTag, containedVal)
            self.endContainer()
        elif isinstance(val, TLVList):
            self.startPath(tag)
            for containedTag, containedVal in val:
                self.put(containedTag, containedVal)
            self.endContainer()
        elif isinstance(val, Sequence):
            self.startArray(tag)
            for containedVal in val:
                self.put(None, containedVal)
            self.endContainer()
        else:
            raise ValueError("Attempt to TLV encode unsupported value")

    def putSignedInt(self, tag, val):
        """Write a value as a TLV signed integer with the specified TLV tag."""
        if val >= INT8_MIN and val <= INT8_MAX:
            format = "<b"
        elif val >= INT16_MIN and val <= INT16_MAX:
            format = "<h"
        elif val >= INT32_MIN and val <= INT32_MAX:
            format = "<l"
        elif val >= INT64_MIN and val <= INT64_MAX:
            format = "<q"
        else:
            raise ValueError("Integer value out of range")
        val = struct.pack(format, val)
        controlAndTag = self._encodeControlAndTag(
            TLV_TYPE_SIGNED_INTEGER, tag, lenOfLenOrVal=len(val)
        )
        self._encoding.extend(controlAndTag)
        self._encoding.extend(val)

    def putUnsignedInt(self, tag, val):
        """Write a value as a TLV unsigned integer with the specified TLV tag."""
        val = self._encodeUnsignedInt(val)
        controlAndTag = self._encodeControlAndTag(
            TLV_TYPE_UNSIGNED_INTEGER, tag, lenOfLenOrVal=len(val)
        )
        self._encoding.extend(controlAndTag)
        self._encoding.extend(val)

    def putFloat(self, tag, val):
        """Write a value as a TLV float with the specified TLV tag."""
        val = struct.pack("f", val)
        controlAndTag = self._encodeControlAndTag(
            TLV_TYPE_FLOATING_POINT_NUMBER, tag, lenOfLenOrVal=len(val)
        )
        self._encoding.extend(controlAndTag)
        self._encoding.extend(val)

    def putDouble(self, tag, val):
        """Write a value as a TLV double with the specified TLV tag."""
        val = struct.pack("d", val)
        controlAndTag = self._encodeControlAndTag(
            TLV_TYPE_FLOATING_POINT_NUMBER, tag, lenOfLenOrVal=len(val)
        )
        self._encoding.extend(controlAndTag)
        self._encoding.extend(val)

    def putString(self, tag, val):
        """Write a value as a TLV string with the specified TLV tag."""
        val = val.encode("utf-8")
        valLen = self._encodeUnsignedInt(len(val))
        controlAndTag = self._encodeControlAndTag(
            TLV_TYPE_UTF8_STRING, tag, lenOfLenOrVal=len(valLen)
        )
        self._encoding.extend(controlAndTag)
        self._encoding.extend(valLen)
        self._encoding.extend(val)

    def putBytes(self, tag, val):
        """Write a value as a TLV byte string with the specified TLV tag."""
        valLen = self._encodeUnsignedInt(len(val))
        controlAndTag = self._encodeControlAndTag(
            TLV_TYPE_BYTE_STRING, tag, lenOfLenOrVal=len(valLen)
        )
        self._encoding.extend(controlAndTag)
        self._encoding.extend(valLen)
        self._encoding.extend(val)

    def putBool(self, tag, val):
        """Write a value as a TLV boolean with the specified TLV tag."""
        if val:
            type = TLVBoolean_True
        else:
            type = TLVBoolean_False
        controlAndTag = self._encodeControlAndTag(type, tag)
        self._encoding.extend(controlAndTag)

    def putNull(self, tag):
        """Write a TLV null with the specified TLV tag."""
        controlAndTag = self._encodeControlAndTag(TLV_TYPE_NULL, tag)
        self._encoding.extend(controlAndTag)

    def startContainer(self, tag, containerType):
        """Start writing a TLV container with the specified TLV tag.

        containerType can be one of TLV_TYPE_STRUCTURE, TLV_TYPE_ARRAY or
        TLV_TYPE_PATH.
        """
        self._verifyValidContainerType(containerType)
        controlAndTag = self._encodeControlAndTag(containerType, tag)
        self._encoding.extend(controlAndTag)
        self._containerStack.insert(0, containerType)

    def startStructure(self, tag):
        """Start writing a TLV structure with the specified TLV tag."""
        self.startContainer(tag, containerType=TLV_TYPE_STRUCTURE)

    def startArray(self, tag):
        """Start writing a TLV array with the specified TLV tag."""
        self.startContainer(tag, containerType=TLV_TYPE_ARRAY)

    def startPath(self, tag):
        """Start writing a TLV path with the specified TLV tag."""
        self.startContainer(tag, containerType=TLV_TYPE_PATH)

    def endContainer(self):
        """End writing the current TLV container."""
        self._containerStack.pop(0)
        controlAndTag = self._encodeControlAndTag(TLVEndOfContainer, None)
        self._encoding.extend(controlAndTag)

    def _encodeControlAndTag(self, type, tag, lenOfLenOrVal=0):
        controlByte = type
        if lenOfLenOrVal == 2:
            controlByte |= 1
        elif lenOfLenOrVal == 4:
            controlByte |= 2
        elif lenOfLenOrVal == 8:
            controlByte |= 3
        if tag is None:
            if (
                type != TLVEndOfContainer
                and len(self._containerStack) != 0
                and self._containerStack[0] == TLV_TYPE_STRUCTURE
            ):
                raise ValueError(
                    "Attempt to encode anonymous tag within TLV structure")
            controlByte |= TLV_TAG_CONTROL_ANONYMOUS
            return struct.pack("<B", controlByte)
        if isinstance(tag, int):
            if tag < 0 or tag > UINT8_MAX:
                raise ValueError(
                    "Context-specific TLV tag number out of range")
            if len(self._containerStack) == 0:
                raise ValueError(
                    "Attempt to encode context-specific TLV tag at top level"
                )
            if self._containerStack[0] == TLV_TYPE_ARRAY:
                raise ValueError(
                    "Attempt to encode context-specific tag within TLV array"
                )
            controlByte |= TLV_TAG_CONTROL_CONTEXT_SPECIFIC
            return struct.pack("<BB", controlByte, tag)
        if isinstance(tag, tuple):
            (profile, tagNum) = tag
            if not isinstance(tagNum, int):
                raise ValueError("Invalid object given for TLV tag")
            if tagNum < 0 or tagNum > UINT32_MAX:
                raise ValueError("TLV tag number out of range")
            if profile is not None:
                if not isinstance(profile, int):
                    raise ValueError("Invalid object given for TLV profile id")
                if profile < 0 or profile > UINT32_MAX:
                    raise ValueError("TLV profile id value out of range")
            if (
                len(self._containerStack) != 0
                and self._containerStack[0] == TLV_TYPE_ARRAY
            ):
                raise ValueError(
                    "Attempt to encode profile-specific tag within TLV array"
                )
            if profile is None or profile == self._implicitProfile:
                if tagNum <= UINT16_MAX:
                    controlByte |= TLV_TAG_CONTROL_IMPLICIT_PROFILE_2Bytes
                    return struct.pack("<BH", controlByte, tagNum)
                else:
                    controlByte |= TLV_TAG_CONTROL_IMPLICIT_PROFILE_4Bytes
                    return struct.pack("<BL", controlByte, tagNum)
            elif profile == 0:
                if tagNum <= UINT16_MAX:
                    controlByte |= TLV_TAG_CONTROL_COMMON_PROFILE_2Bytes
                    return struct.pack("<BH", controlByte, tagNum)
                else:
                    controlByte |= TLV_TAG_CONTROL_COMMON_PROFILE_4Bytes
                    return struct.pack("<BL", controlByte, tagNum)
            else:
                vendorId = (profile >> 16) & 0xFFFF
                profileNum = (profile >> 0) & 0xFFFF
                if tagNum <= UINT16_MAX:
                    controlByte |= TLV_TAG_CONTROL_FULLY_QUALIFIED_6Bytes
                    return struct.pack("<BHHH", controlByte, vendorId, profileNum, tagNum)
                else:
                    controlByte |= TLV_TAG_CONTROL_FULLY_QUALIFIED_8Bytes
                    return struct.pack("<BHHL", controlByte, vendorId, profileNum, profile, tagNum)
        raise ValueError("Invalid object given for TLV tag")

    @staticmethod
    def _encodeUnsignedInt(val):
        if val < 0:
            raise ValueError("Integer value out of range")
        if val <= UINT8_MAX:
            format = "<B"
        elif val <= UINT16_MAX:
            format = "<H"
        elif val <= UINT32_MAX:
            format = "<L"
        elif val <= UINT64_MAX:
            format = "<Q"
        else:
            raise ValueError("Integer value out of range")
        return struct.pack(format, val)

    @staticmethod
    def _verifyValidContainerType(containerType):
        if (
            containerType != TLV_TYPE_STRUCTURE
            and containerType != TLV_TYPE_ARRAY
            and containerType != TLV_TYPE_PATH
        ):
            raise ValueError("Invalid TLV container type")


class TLVReader(object):
    def __init__(self, tlv):
        self._tlv = tlv
        self._bytesRead = 0
        self._decodings = []

    @property
    def decoding(self):
        return self._decodings

    def get(self):
        """Get the dictionary representation of tlv data"""
        out = {}
        self._get(self._tlv, self._decodings, out)
        return out

    def _decodeControlByte(self, tlv, decoding):
        (controlByte,) = struct.unpack(
            "<B", tlv[self._bytesRead: self._bytesRead + 1])
        controlTypeIndex = controlByte & 0xE0
        decoding["tagControl"] = TagControls[controlTypeIndex]
        elementtypeIndex = controlByte & 0x1F
        decoding["type"] = ElementTypes[elementtypeIndex]
        self._bytesRead += 1

    def _decodeControlAndTag(self, tlv, decoding):
        """The control byte specifies the type of a TLV element and how its tag, length and value fields are encoded.
        The control byte consists of two subfields: an element type field which occupies the lower 5 bits,
        and a tag control field which occupies the upper 3 bits. The element type field encodes the element’s type
        as well as how the corresponding length and value fields are encoded.  In the case of Booleans and the
        null value, the element type field also encodes the value itself."""

        self._decodeControlByte(tlv, decoding)

        if decoding["tagControl"] == "Anonymous":
            decoding["tag"] = None
            decoding["tagLen"] = 0
        elif decoding["tagControl"] == "Context 1-byte":
            (decoding["tag"],) = struct.unpack(
                "<B", tlv[self._bytesRead: self._bytesRead + 1]
            )
            decoding["tagLen"] = 1
            self._bytesRead += 1
        elif decoding["tagControl"] == "Common Profile 2-byte":
            profile = 0
            (tag,) = struct.unpack(
                "<H", tlv[self._bytesRead: self._bytesRead + 2])
            decoding["profileTag"] = (profile, tag)
            decoding["tagLen"] = 2
            self._bytesRead += 2
        elif decoding["tagControl"] == "Common Profile 4-byte":
            profile = 0
            (tag,) = struct.unpack(
                "<L", tlv[self._bytesRead: self._bytesRead + 4])
            decoding["profileTag"] = (profile, tag)
            decoding["tagLen"] = 4
            self._bytesRead += 4
        elif decoding["tagControl"] == "Implicit Profile 2-byte":
            profile = None
            (tag,) = struct.unpack(
                "<H", tlv[self._bytesRead: self._bytesRead + 2])
            decoding["profileTag"] = (profile, tag)
            decoding["tagLen"] = 2
            self._bytesRead += 2
        elif decoding["tagControl"] == "Implicit Profile 4-byte":
            profile = None
            (tag,) = struct.unpack(
                "<L", tlv[self._bytesRead: self._bytesRead + 4])
            decoding["profileTag"] = (profile, tag)
            decoding["tagLen"] = 4
            self._bytesRead += 4
        elif decoding["tagControl"] == "Fully Qualified 6-byte":
            (vendorId, profileNum) = struct.unpack(
                "<HH", tlv[self._bytesRead: self._bytesRead + 4])
            profile = (vendorId << 16) | profileNum
            (tag,) = struct.unpack(
                "<H", tlv[self._bytesRead + 4: self._bytesRead + 6])
            decoding["profileTag"] = (profile, tag)
            decoding["tagLen"] = 2
            self._bytesRead += 6
        elif decoding["tagControl"] == "Fully Qualified 8-byte":
            (vendorId, profileNum) = struct.unpack(
                "<HH", tlv[self._bytesRead: self._bytesRead + 4])
            profile = (vendorId << 16) | profileNum
            (tag,) = struct.unpack(
                "<L", tlv[self._bytesRead + 4: self._bytesRead + 8])
            decoding["profileTag"] = (profile, tag)
            decoding["tagLen"] = 4
            self._bytesRead += 8

    def _decodeStrLength(self, tlv, decoding):
        """UTF-8 or Byte StringLength fields are encoded in 0, 1, 2 or 4 byte widths, as specified by
        the element type field. If the element type needs a length field grab the next bytes as length"""
        if "length" in decoding["type"]:
            if "1-byte" in decoding["type"]:
                (decoding["strDataLen"],) = struct.unpack(
                    "<B", tlv[self._bytesRead: self._bytesRead + 1]
                )
                decoding["strDataLenLen"] = 1
                self._bytesRead += 1
            elif "2-byte" in decoding["type"]:
                (decoding["strDataLen"],) = struct.unpack(
                    "<H", tlv[self._bytesRead: self._bytesRead + 2]
                )
                decoding["strDataLenLen"] = 2
                self._bytesRead += 2
            elif "4-byte" in decoding["type"]:
                (decoding["strDataLen"],) = struct.unpack(
                    "<L", tlv[self._bytesRead: self._bytesRead + 4]
                )
                decoding["strDataLenLen"] = 4
                self._bytesRead += 4
            elif "8-byte" in decoding["type"]:
                (decoding["strDataLen"],) = struct.unpack(
                    "<Q", tlv[self._bytesRead: self._bytesRead + 8]
                )
                decoding["strDataLenLen"] = 8
                self._bytesRead += 8
        else:
            decoding["strDataLen"] = 0
            decoding["strDataLenLen"] = 0

    def _decodeVal(self, tlv, decoding):
        """decode primitive tlv value to the corresponding python value, tlv array and path are decoded as
        python list, tlv structure is decoded as python dictionary"""
        if decoding["type"] == "Structure":
            decoding["value"] = {}
            decoding["Structure"] = []
            self._get(tlv, decoding["Structure"], decoding["value"])
        elif decoding["type"] == "Array":
            decoding["value"] = []
            decoding["Array"] = []
            self._get(tlv, decoding["Array"], decoding["value"])
        elif decoding["type"] == "Path":
            decoding["value"] = TLVList()
            decoding["Path"] = []
            self._get(tlv, decoding["Path"], decoding["value"])
        elif decoding["type"] == "Null":
            decoding["value"] = None
        elif decoding["type"] == "End of Collection":
            decoding["value"] = None
        elif decoding["type"] == "Boolean True":
            decoding["value"] = True
        elif decoding["type"] == "Boolean False":
            decoding["value"] = False
        elif decoding["type"] == "Unsigned Integer 1-byte value":
            (decoding["value"],) = struct.unpack(
                "<B", tlv[self._bytesRead: self._bytesRead + 1]
            )
            decoding["value"] = uint(decoding["value"])
            self._bytesRead += 1
        elif decoding["type"] == "Signed Integer 1-byte value":
            (decoding["value"],) = struct.unpack(
                "<b", tlv[self._bytesRead: self._bytesRead + 1]
            )
            self._bytesRead += 1
        elif decoding["type"] == "Unsigned Integer 2-byte value":
            (decoding["value"],) = struct.unpack(
                "<H", tlv[self._bytesRead: self._bytesRead + 2]
            )
            decoding["value"] = uint(decoding["value"])
            self._bytesRead += 2
        elif decoding["type"] == "Signed Integer 2-byte value":
            (decoding["value"],) = struct.unpack(
                "<h", tlv[self._bytesRead: self._bytesRead + 2]
            )
            self._bytesRead += 2
        elif decoding["type"] == "Unsigned Integer 4-byte value":
            (decoding["value"],) = struct.unpack(
                "<L", tlv[self._bytesRead: self._bytesRead + 4]
            )
            decoding["value"] = uint(decoding["value"])
            self._bytesRead += 4
        elif decoding["type"] == "Signed Integer 4-byte value":
            (decoding["value"],) = struct.unpack(
                "<l", tlv[self._bytesRead: self._bytesRead + 4]
            )
            self._bytesRead += 4
        elif decoding["type"] == "Unsigned Integer 8-byte value":
            (decoding["value"],) = struct.unpack(
                "<Q", tlv[self._bytesRead: self._bytesRead + 8]
            )
            decoding["value"] = uint(decoding["value"])
            self._bytesRead += 8
        elif decoding["type"] == "Signed Integer 8-byte value":
            (decoding["value"],) = struct.unpack(
                "<q", tlv[self._bytesRead: self._bytesRead + 8]
            )
            self._bytesRead += 8
        elif decoding["type"] == "Floating Point 4-byte value":
            (decoding["value"],) = struct.unpack(
                "<f", tlv[self._bytesRead: self._bytesRead + 4]
            )
            decoding["value"] = float32(decoding["value"])
            self._bytesRead += 4
        elif decoding["type"] == "Floating Point 8-byte value":
            (decoding["value"],) = struct.unpack(
                "<d", tlv[self._bytesRead: self._bytesRead + 8]
            )
            self._bytesRead += 8
        elif "UTF-8 String" in decoding["type"]:
            (val,) = struct.unpack(
                "<%ds" % decoding["strDataLen"],
                tlv[self._bytesRead: self._bytesRead + decoding["strDataLen"]],
            )
            try:
                decoding["value"] = str(val, "utf-8")
            except Exception:
                decoding["value"] = val
            self._bytesRead += decoding["strDataLen"]
        elif "Byte String" in decoding["type"]:
            (val,) = struct.unpack(
                "<%ds" % decoding["strDataLen"],
                tlv[self._bytesRead: self._bytesRead + decoding["strDataLen"]],
            )

            decoding["value"] = val
            self._bytesRead += decoding["strDataLen"]
        else:
            raise ValueError("Attempt to decode unsupported TLV type")

    def _get(self, tlv, decodings, out):
        endOfEncoding = False

        while len(tlv[self._bytesRead:]) > 0 and endOfEncoding is False:
            decoding = {}
            self._decodeControlAndTag(tlv, decoding)
            self._decodeStrLength(tlv, decoding)
            self._decodeVal(tlv, decoding)
            decodings.append(decoding)

            if decoding["type"] == "End of Collection":
                endOfEncoding = True
            else:
                if "profileTag" in list(decoding.keys()):
                    out[decoding["profileTag"]] = decoding["value"]
                elif "tag" in list(decoding.keys()):
                    if isinstance(out, Mapping):
                        tag = decoding["tag"] if decoding["tag"] is not None else "Any"
                        out[tag] = decoding["value"]
                    elif isinstance(out, TLVList):
                        tag = decoding["tag"] if decoding["tag"] is not None else None
                        out.append(tag, decoding["value"])
                    else:
                        out.append(decoding["value"])
                else:
                    raise ValueError("Attempt to decode unsupported TLV tag")


def tlvTagToSortKey(tag):
    if tag is None:
        return -1
    if isinstance(tag, int):
        majorOrder = 0
    elif isinstance(tag, tuple):
        (profileId, tag) = tag
        if profileId is None:
            majorOrder = 1
        else:
            majorOrder = profileId + 2
    else:
        raise ValueError("Invalid TLV tag")
    return (majorOrder << 32) + tag


if __name__ == "__main__":
    val = dict(
        [
            (1, 0),
            (2, 65536),
            (3, True),
            (4, None),
            (5, "Hello!"),
            (6, bytearray([0xDE, 0xAD, 0xBE, 0xEF])),
            (7, ["Goodbye!", 71024724507, False]),
            ((0x235A0000, 42), "FOO"),
            ((None, 42), "BAR"),
        ]
    )

    writer = TLVWriter()
    encodedVal = writer.put(None, val)

    reader = TLVReader(writer.encoding)
    out = reader.get()

    print("TLVReader input: " + str(val))
    print("TLVReader output: " + str(out["Any"]))

    if val == out["Any"]:
        print("Test Success")
    else:
        print("Test Failure")
