# SPDX-License-Identifier: MIT

from __future__ import annotations  # noqa:F407

import functools
import struct
import sys
import textwrap
import typing
import warnings

from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, TextIO, Tuple, Union


if sys.version_info >= (3, 8):
    from typing import Literal
else:  # pragma: no cover
    from typing_extensions import Literal

import hid_parser.data


__version__ = '0.0.3'


class HIDWarning(Warning):
    pass


class HIDComplianceWarning(HIDWarning):
    pass


class HIDReportWarning(HIDWarning):
    pass


class HIDUnsupportedWarning(HIDWarning):
    pass


class Type():
    MAIN = 0
    GLOBAL = 1
    LOCAL = 2


class TagMain():
    INPUT = 0b1000
    OUTPUT = 0b1001
    FEATURE = 0b1011
    COLLECTION = 0b1010
    END_COLLECTION = 0b1100


class TagGlobal():
    USAGE_PAGE = 0b0000
    LOGICAL_MINIMUM = 0b0001
    LOGICAL_MAXIMUM = 0b0010
    PHYSICAL_MINIMUM = 0b0011
    PHYSICAL_MAXIMUM = 0b0100
    UNIT_EXPONENT = 0b0101
    UNIT = 0b0110
    REPORT_SIZE = 0b0111
    REPORT_ID = 0b1000
    REPORT_COUNT = 0b1001
    PUSH = 0b1010
    POP = 0b1011


class TagLocal():
    USAGE = 0b0000
    USAGE_MINIMUM = 0b0001
    USAGE_MAXIMUM = 0b0010
    DESIGNATOR_INDEX = 0b0011
    DESIGNATOR_MINIMUM = 0b0100
    DESIGNATOR_MAXIMUM = 0b0101
    STRING_INDEX = 0b0111
    STRING_MINIMUM = 0b1000
    STRING_MAXIMUM = 0b1001
    DELIMITER = 0b1010


def _data_bit_shift(data: Sequence[int], offset: int, length: int) -> Sequence[int]:
    if not length > 0:
        raise ValueError(f'Invalid specified length: {length}')

    left_extra = offset % 8
    right_extra = 8 - (offset + length) % 8
    start_offset = offset // 8
    end_offset = (offset + length - 1) // 8
    byte_length = (length - 1) // 8 + 1

    if not end_offset < len(data):
        raise ValueError(f'Invalid data length: {len(data)} (expecting {end_offset + 1})')

    shifted = [0] * byte_length

    if right_extra == 8:
        right_extra = 0

    i = end_offset
    shifted_offset = byte_length - 1
    while shifted_offset >= 0:
        shifted[shifted_offset] = data[i] >> right_extra

        if i - start_offset >= 0:
            shifted[shifted_offset] |= (data[i - 1] & (0xff >> (8 - right_extra))) << (8 - right_extra)

        shifted_offset -= 1
        i -= 1

    shifted[0] &= 0xff >> ((left_extra + right_extra) % 8)

    if not len(shifted) == byte_length:
        raise ValueError('Invalid data')

    return shifted


class BitNumber(int):
    def __init__(self, value: int):
        self._value = value

    def __int__(self) -> int:
        return self._value

    def __eq__(self, other: Any) -> bool:
        try:
            return self._value == int(other)
        except:  # noqa: E722
            return False

    @property
    def byte(self) -> int:
        '''
        Number of bytes
        '''
        return self._value // 8

    @property
    def bit(self) -> int:
        '''
        Number of unaligned bits

        n.byte * 8 + n.bits = n
        '''
        if self.byte == 0:
            return self._value

        return self._value % (self.byte * 8)

    @staticmethod
    def _param_repr(value: int, unit: str) -> str:
        if value != 1:
            unit += 's'
        return f'{value}{unit}'

    def __repr__(self) -> str:
        byte_str = self._param_repr(self.byte, 'byte')
        bit_str = self._param_repr(self.bit, 'bit')

        if self.byte == 0 and self.bit == 0:
            return bit_str

        parts = []
        if self.byte != 0:
            parts.append(byte_str)
        if self.bit != 0:
            parts.append(bit_str)

        return ' '.join(parts)


class Usage():
    def __init__(
        self,
        page: Optional[int] = None,
        usage: Optional[int] = None,
        *,
        extended_usage: Optional[int] = None
    ) -> None:
        if extended_usage and page and usage:
            raise ValueError('You need to specify either the usage page and usage or the extended usage')
        if extended_usage is not None:
            self.page = extended_usage >> (2 * 8)
            self.usage = extended_usage & 0xffff
        elif page is not None and usage is not None:
            self.page = page
            self.usage = usage
        else:
            raise ValueError('No usage specified')

    def __int__(self) -> int:
        return self.page << (2 * 8) | self.usage

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, self.__class__):
            return False
        return self.page == other.page and self.usage == other.usage

    def __hash__(self) -> int:
        return self.usage << (2 * 8) + self.page

    def __repr__(self) -> str:
        try:
            page_str = hid_parser.data.UsagePages.get_description(self.page)
        except KeyError:
            page_str = f'0x{self.page:04x}'
            usage_str = f'0x{self.usage:04x}'
        else:
            try:
                page = hid_parser.data.UsagePages.get_subdata(self.page)
                usage_str = page.get_description(self.usage)
            except (KeyError, ValueError):
                usage_str = f'0x{self.usage:04x}'
        return f'Usage(page={page_str}, usage={usage_str})'

    @property
    def usage_types(self) -> Tuple[hid_parser.data.UsageTypes]:
        subdata = hid_parser.data.UsagePages.get_subdata(self.page).get_subdata(self.usage)

        if isinstance(subdata, tuple):
            types = subdata
        else:
            types = (subdata,)

        for typ in types:
            if not isinstance(typ, hid_parser.data.UsageTypes):
                raise ValueError(f"Expecting usage type but got '{type(typ)}'")

        return typing.cast(Tuple[hid_parser.data.UsageTypes], types)


class UsageValue():
    def __init__(self, item: MainItem, value: int):
        self._item = item
        self._value = value

    def __int__(self) -> int:
        return self.value

    def __repr__(self) -> str:
        return repr(self.value)

    @property
    def value(self) -> Union[int, bool]:
        return self._value

    @property
    def constant(self) -> bool:
        return self._item.constant

    @property
    def data(self) -> bool:
        return self._item.data

    @property
    def relative(self) -> bool:
        return self._item.relative

    @property
    def absolute(self) -> bool:
        return self._item.absolute


class VendorUsageValue(UsageValue):
    def __init__(
        self,
        item: MainItem,
        *,
        value: Optional[int] = None,
        value_list: Optional[List[int]] = None,
    ):
        self._item = item
        if value:
            self._list = [value]
        elif value_list:
            self._list = value_list
        else:
            self._list = []

    def __int__(self) -> int:
        return self.value

    def __iter__(self) -> Iterator[int]:
        return iter(self.list)

    @property
    def value(self) -> Union[int, bool]:
        return int.from_bytes(self._list, byteorder='little')

    @property
    def list(self) -> List[int]:
        return self._list


class BaseItem():
    def __init__(self, offset: int, size: int):
        self._offset = BitNumber(offset)
        self._size = BitNumber(size)

    @property
    def offset(self) -> BitNumber:
        return self._offset

    @property
    def size(self) -> BitNumber:
        return self._size

    def __repr__(self) -> str:
        return f'{self.__class__.__name__}(offset={self.offset}, size={self.size})'


class PaddingItem(BaseItem):
    pass


class MainItem(BaseItem):
    def __init__(
        self,
        offset: int,
        size: int,
        flags: int,
        logical_min: int,
        logical_max: int,
        physical_min: Optional[int] = None,
        physical_max: Optional[int] = None,
    ):
        super().__init__(offset, size)
        self._flags = flags
        self._logical_min = logical_min
        self._logical_max = logical_max
        self._physical_min = physical_min
        self._physical_max = physical_max
        # TODO: unit

    @property
    def offset(self) -> BitNumber:
        return self._offset

    @property
    def size(self) -> BitNumber:
        return self._size

    @property
    def logical_min(self) -> int:
        return self._logical_min

    @property
    def logical_max(self) -> int:
        return self._logical_max

    @property
    def physical_min(self) -> Optional[int]:
        return self._physical_min

    @property
    def physical_max(self) -> Optional[int]:
        return self._physical_max

    # flags

    @property
    def constant(self) -> bool:
        return self._flags & (1 << 0) != 0

    @property
    def data(self) -> bool:
        return self._flags & (1 << 0) == 0

    @property
    def relative(self) -> bool:
        return self._flags & (1 << 2) != 0

    @property
    def absolute(self) -> bool:
        return self._flags & (1 << 2) == 0


class VariableItem(MainItem):
    _INCOMPATIBLE_TYPES = (
        # array types
        hid_parser.data.UsageTypes.SELECTOR,
        # collection types
        hid_parser.data.UsageTypes.NAMED_ARRAY,
        hid_parser.data.UsageTypes.COLLECTION_APPLICATION,
        hid_parser.data.UsageTypes.COLLECTION_LOGICAL,
        hid_parser.data.UsageTypes.COLLECTION_PHYSICAL,
        hid_parser.data.UsageTypes.USAGE_SWITCH,
        hid_parser.data.UsageTypes.USAGE_MODIFIER,
    )

    def __init__(
        self,
        offset: int,
        size: int,
        flags: int,
        usage: Usage,
        logical_min: int,
        logical_max: int,
        physical_min: Optional[int] = None,
        physical_max: Optional[int] = None,
    ):
        super().__init__(offset, size, flags, logical_min, logical_max, physical_min, physical_max)
        self._usage = usage

        try:
            if all(usage_type in self._INCOMPATIBLE_TYPES for usage_type in usage.usage_types):
                warnings.warn(HIDComplianceWarning(
                    f'{usage} has no compatible usage types with a variable item'
                ))
        except (KeyError, ValueError):
            pass

    def __repr__(self) -> str:
        return f'VariableItem(offset={self.offset}, size={self.size}, usage={self.usage})'

    def parse(self, data: Sequence[int]) -> UsageValue:
        data = _data_bit_shift(data, self.offset, self.size)

        if (
            hid_parser.data.UsageTypes.LINEAR_CONTROL in self.usage.usage_types
            or any(
                usage_type in hid_parser.data.UsageTypesData
                and usage_type != hid_parser.data.UsageTypes.SELECTOR
                for usage_type in self.usage.usage_types
            )
        ):  # int
            value = int.from_bytes(data, byteorder='little')
        elif (
            hid_parser.data.UsageTypes.ON_OFF_CONTROL in self.usage.usage_types
            and not self.preferred_state
            and self.logical_min == -1
            and self.logical_max == 1
        ):  # bool - -1 is false
            value = int.from_bytes(data, byteorder='little') == 1
        else:  # bool
            value = bool.from_bytes(data, byteorder='little')

        return UsageValue(self, value)

    @property
    def usage(self) -> Usage:
        return self._usage

    # flags (variable only, see HID spec 1.11 page 32)

    @property
    def wrap(self) -> bool:
        return self._flags & (1 << 3) != 0

    @property
    def linear(self) -> bool:
        return self._flags & (1 << 4) != 0

    @property
    def preferred_state(self) -> bool:
        return self._flags & (1 << 5) != 0

    @property
    def null_state(self) -> bool:
        return self._flags & (1 << 6) != 0

    @property
    def buffered_bytes(self) -> bool:
        return self._flags & (1 << 7) != 0

    @property
    def bitfield(self) -> bool:
        return self._flags & (1 << 7) == 0


class ArrayItem(MainItem):
    _INCOMPATIBLE_TYPES = (
        # variable types
        hid_parser.data.UsageTypes.LINEAR_CONTROL,
        hid_parser.data.UsageTypes.ON_OFF_CONTROL,
        hid_parser.data.UsageTypes.MOMENTARY_CONTROL,
        hid_parser.data.UsageTypes.ONE_SHOT_CONTROL,
        hid_parser.data.UsageTypes.RE_TRIGGER_CONTROL,
        hid_parser.data.UsageTypes.STATIC_VALUE,
        hid_parser.data.UsageTypes.STATIC_FLAG,
        hid_parser.data.UsageTypes.DYNAMIC_VALUE,
        hid_parser.data.UsageTypes.DYNAMIC_FLAG,
        # collection types
        hid_parser.data.UsageTypes.NAMED_ARRAY,
        hid_parser.data.UsageTypes.COLLECTION_APPLICATION,
        hid_parser.data.UsageTypes.COLLECTION_LOGICAL,
        hid_parser.data.UsageTypes.COLLECTION_PHYSICAL,
        hid_parser.data.UsageTypes.USAGE_SWITCH,
        hid_parser.data.UsageTypes.USAGE_MODIFIER,
    )
    _IGNORE_USAGE_VALUES = (
        (hid_parser.data.UsagePages.KEYBOARD_KEYPAD_PAGE, hid_parser.data.KeyboardKeypad.NO_EVENT),
    )

    def __init__(
        self,
        offset: int,
        size: int,
        count: int,
        flags: int,
        usages: List[Usage],
        logical_min: int,
        logical_max: int,
        physical_min: Optional[int] = None,
        physical_max: Optional[int] = None,
    ):
        super().__init__(offset, size, flags, logical_min, logical_max, physical_min, physical_max)
        self._count = count
        self._usages = usages
        self._page = self._usages[0].page if usages else None

        for usage in self._usages:
            if usage.page != self._page:
                raise ValueError(f'Mismatching usage page in usage: {usage} (expecting {self._usages[0]})')
            try:
                if all(usage_type in self._INCOMPATIBLE_TYPES for usage_type in usage.usage_types):
                    warnings.warn(HIDComplianceWarning(
                        f'{usage} has no compatible usage types with an array item'
                    ))
            except (KeyError, ValueError):
                pass

        self._ignore_usages: List[Usage] = []
        for page, usage_id in self._IGNORE_USAGE_VALUES:
            assert isinstance(page, int) and isinstance(usage_id, int)
            self._ignore_usages.append(Usage(page, usage_id))

    def __repr__(self) -> str:
        return textwrap.dedent('''
            ArrayItem(
                offset={}, size={}, count={},
                usages=[
                    {},
                ],
            )
        ''').strip().format(
            self.offset,
            self.size,
            self.count,
            ',\n        '.join(repr(usage) for usage in self.usages),
        )

    def parse(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
        usage_values: Dict[Usage, UsageValue] = {}

        for i in range(self.count):
            aligned_data = _data_bit_shift(data, self.offset + i*8, self.size)
            usage = Usage(self._page, int.from_bytes(aligned_data, byteorder='little'))

            if usage in self._ignore_usages:
                continue

            # vendor usages don't have usage any standard type - just save the raw data
            if usage.page in hid_parser.data.UsagePages.VENDOR_PAGE:
                if usage not in usage_values:
                    usage_values[usage] = VendorUsageValue(
                        self,
                        value=int.from_bytes(aligned_data, byteorder='little'),
                    )
                typing.cast(VendorUsageValue, usage_values[usage]).list.append(
                    int.from_bytes(aligned_data, byteorder='little')
                )
                continue

            if usage in self._usages and all(
                usage_type not in self._INCOMPATIBLE_TYPES
                for usage_type in usage.usage_types
            ):
                usage_values[usage] = UsageValue(self, True)

        return usage_values

    @property
    def count(self) -> int:
        return self._count

    @property
    def usages(self) -> List[Usage]:
        return self._usages


class InvalidReportDescriptor(Exception):
    pass


# report ID (None for no report ID), item list
_ITEM_POOL = Dict[Optional[int], List[BaseItem]]


class ReportDescriptor():
    def __init__(self, data: Sequence[int]) -> None:
        self._data = data

        for byte in data:
            if byte < 0 or byte > 255:
                raise InvalidReportDescriptor(
                    f'A report descriptor should be represented by a list of bytes: found value {byte}'
                )

        self._input: _ITEM_POOL = {}
        self._output: _ITEM_POOL = {}
        self._feature: _ITEM_POOL = {}

        self._parse()

    @property
    def data(self) -> Sequence[int]:
        return self._data

    @property
    def input_report_ids(self) -> List[Optional[int]]:
        return list(self._input.keys())

    @property
    def output_report_ids(self) -> List[Optional[int]]:
        return list(self._output.keys())

    @property
    def feature_report_ids(self) -> List[Optional[int]]:
        return list(self._feature.keys())

    def _get_report_size(self, items: List[BaseItem]) -> BitNumber:
        size = 0
        for item in items:
            if isinstance(item, ArrayItem):
                size += item.size * item.count
            else:
                size += item.size
        return BitNumber(size)

    def get_input_items(self, report_id: Optional[int] = None) -> List[BaseItem]:
        return self._input[report_id]

    @functools.lru_cache(maxsize=16)
    def get_input_report_size(self, report_id: Optional[int] = None) -> BitNumber:
        return self._get_report_size(self.get_input_items(report_id))

    def get_output_items(self, report_id: Optional[int] = None) -> List[BaseItem]:
        return self._output[report_id]

    @functools.lru_cache(maxsize=16)
    def get_output_report_size(self, report_id: Optional[int] = None) -> BitNumber:
        return self._get_report_size(self.get_output_items(report_id))

    def get_feature_items(self, report_id: Optional[int] = None) -> List[BaseItem]:
        return self._feature[report_id]

    @functools.lru_cache(maxsize=16)
    def get_feature_report_size(self, report_id: Optional[int] = None) -> BitNumber:
        return self._get_report_size(self.get_feature_items(report_id))

    def _parse_report_items(self, items: List[BaseItem], data: Sequence[int]) -> Dict[Usage, UsageValue]:
        parsed: Dict[Usage, UsageValue] = {}
        for item in items:
            if isinstance(item, VariableItem):
                parsed[item.usage] = item.parse(data)
            elif isinstance(item, ArrayItem):
                usage_values = item.parse(data)
                for usage in usage_values:
                    if usage in parsed:
                        warnings.warn(HIDReportWarning(f'Overriding usage: {usage}'))
                parsed.update(usage_values)
            elif isinstance(item, PaddingItem):
                pass
            else:
                raise TypeError(f'Unknown item: {item}')
        return parsed

    def _parse_report(self, item_poll: _ITEM_POOL, data: Sequence[int]) -> Dict[Usage, UsageValue]:
        if None in item_poll:  # unnumbered reports
            return self._parse_report_items(item_poll[None], data)
        else:  # numbered reports
            return self._parse_report_items(item_poll[data[0]], data[1:])

    def parse_input_report(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
        return self._parse_report(self._input, data)

    def parse_output_report(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
        return self._parse_report(self._output, data)

    def parse_feature_report(self, data: Sequence[int]) -> Dict[Usage, UsageValue]:
        return self._parse_report(self._feature, data)

    def _iterate_raw(self) -> Iterable[Tuple[int, int, Optional[int]]]:
        i = 0
        while i < len(self.data):
            prefix = self.data[i]
            tag = (prefix & 0b11110000) >> 4
            typ = (prefix & 0b00001100) >> 2
            size = prefix & 0b00000011

            if size == 3:  # 6.2.2.2
                size = 4

            if size == 0:
                data = None
            elif size == 1:
                if i + 1 >= len(self.data):
                    raise InvalidReportDescriptor(f'Invalid size: expecting >={i + 1}, got {len(self.data)}')
                data = self.data[i+1]
            else:
                if i + 1 + size >= len(self.data):
                    raise InvalidReportDescriptor(f'Invalid size: expecting >={i + 1 + size}, got {len(self.data)}')
                if size == 2:
                    pack_type = 'H'
                elif size == 4:
                    pack_type = 'L'
                else:
                    raise ValueError(f'Invalid item size: {size}')
                data = struct.unpack(f'<{pack_type}', bytes(self.data[i+1:i+1+size]))[0]

            yield typ, tag, data

            i += size + 1

    def _append_item(
        self,
        offset_list: Dict[Optional[int], int],
        pool: _ITEM_POOL,
        report_id: Optional[int],
        item: BaseItem,
    ) -> None:
        offset_list[report_id] += item.size
        if report_id in pool:
            pool[report_id].append(item)
        else:
            pool[report_id] = [item]

    def _append_items(
        self,
        offset_list: Dict[Optional[int], int],
        pool: _ITEM_POOL,
        report_id: Optional[int],
        report_count: int,
        report_size: int,
        usages: List[Usage],
        flags: int,
        data: Dict[str, Any],
    ) -> None:
        item: BaseItem
        is_array = flags & (1 << 1) == 0  # otherwise variable

        '''
        HID 1.11, 6.2.2.9 says reports can be byte aligned by declaring a
        main item without usage. A main item can have multiple usages, as I
        interpret it, items are only considered padding when they have NO
        usages.
        '''
        if len(usages) == 0 or not usages:
            for _ in range(report_count):
                item = PaddingItem(offset_list[report_id], report_size)
                self._append_item(offset_list, pool, report_id, item)
            return

        if is_array:
            item = ArrayItem(
                offset=offset_list[report_id],
                size=report_size,
                usages=usages,
                count=report_count,
                flags=flags,
                **data,
            )
            self._append_item(offset_list, pool, report_id, item)
        else:
            if len(usages) != report_count:
                error_str = f'Expecting {report_count} usages but got {len(usages)}'
                if len(usages) == 1:
                    warnings.warn(HIDComplianceWarning(error_str))
                    usages *= report_count
                else:
                    raise InvalidReportDescriptor(error_str)

            for usage in usages:
                item = VariableItem(
                    offset=offset_list[report_id],
                    size=report_size,
                    usage=usage,
                    flags=flags,
                    **data,
                )
                self._append_item(offset_list, pool, report_id, item)

    def _parse(self, level: int = 0, file: TextIO = sys.stdout) -> None:  # noqa: C901
        offset_input: Dict[Optional[int], int] = {
            None: 0,
        }
        offset_output: Dict[Optional[int], int] = {
            None: 0,
        }
        offset_feature: Dict[Optional[int], int] = {
            None: 0,
        }
        report_id: Optional[int] = None
        report_count: Optional[int] = None
        report_size: Optional[int] = None
        usage_page: Optional[int] = None
        usages: List[Usage] = []
        usage_min: Optional[int] = None
        glob: Dict[str, Any] = {}
        local: Dict[str, Any] = {}

        for typ, tag, data in self._iterate_raw():

            if typ == Type.MAIN:

                if tag in (TagMain.COLLECTION, TagMain.END_COLLECTION):
                    usages = []

                # we only care about input, output and features for now
                if tag not in (TagMain.INPUT, TagMain.OUTPUT, TagMain.FEATURE):
                    continue

                if report_count is None:
                    raise InvalidReportDescriptor('Trying to append an item but no report count given')
                if report_size is None:
                    raise InvalidReportDescriptor('Trying to append an item but no report size given')

                if tag == TagMain.INPUT:
                    if data is None:
                        raise InvalidReportDescriptor('Invalid input item')
                    self._append_items(
                        offset_input,
                        self._input,
                        report_id,
                        report_count,
                        report_size,
                        usages,
                        data,
                        {**glob, **local}
                    )

                elif tag == TagMain.OUTPUT:
                    if data is None:
                        raise InvalidReportDescriptor('Invalid output item')
                    self._append_items(
                        offset_output,
                        self._output,
                        report_id,
                        report_count,
                        report_size,
                        usages,
                        data,
                        {**glob, **local}
                    )

                elif tag == TagMain.FEATURE:
                    if data is None:
                        raise InvalidReportDescriptor('Invalid feature item')
                    self._append_items(
                        offset_feature,
                        self._feature,
                        report_id,
                        report_count,
                        report_size,
                        usages,
                        data,
                        {**glob, **local}
                    )

                # clear local
                usages = []
                usage_min = None
                local = {}

                # we don't care about collections for now, maybe in the future...

            elif typ == Type.GLOBAL:

                if tag == TagGlobal.USAGE_PAGE:
                    usage_page = data

                elif tag == TagGlobal.LOGICAL_MINIMUM:
                    glob['logical_min'] = data

                elif tag == TagGlobal.LOGICAL_MAXIMUM:
                    glob['logical_max'] = data

                elif tag == TagGlobal.PHYSICAL_MINIMUM:
                    glob['physical_min'] = data

                elif tag == TagGlobal.PHYSICAL_MAXIMUM:
                    glob['physical_max'] = data

                elif tag == TagGlobal.REPORT_SIZE:
                    report_size = data

                elif tag == TagGlobal.REPORT_ID:
                    if not report_id and (self._input or self._output or self._feature):
                        raise InvalidReportDescriptor('Tried to set a report ID in a report that does not use them')
                    report_id = data
                    # initialize the item offset for this report ID
                    for offset_list in (offset_input, offset_output, offset_feature):
                        if report_id not in offset_list:
                            offset_list[report_id] = 0

                elif tag in (TagGlobal.UNIT, TagGlobal.UNIT_EXPONENT):
                    warnings.warn(HIDUnsupportedWarning(
                        "Data specifies a unit or unit exponent, but we don't support those yet"
                    ))

                elif tag == TagGlobal.REPORT_COUNT:
                    report_count = data

                else:
                    raise NotImplementedError(f'Unsupported global tag: {bin(tag)}')

            elif typ == Type.LOCAL:

                if tag == TagLocal.USAGE:
                    if usage_page is None:
                        raise InvalidReportDescriptor('Usage field found but no usage page')
                    usages.append(Usage(usage_page, data))

                elif tag == TagLocal.USAGE_MINIMUM:
                    usage_min = data

                elif tag == TagLocal.USAGE_MAXIMUM:
                    if usage_min is None:
                        raise InvalidReportDescriptor('Usage maximum set but no usage minimum')
                    if data is None:
                        raise InvalidReportDescriptor('Invalid usage maximum value')
                    for i in range(usage_min, data + 1):
                        usages.append(Usage(usage_page, i))
                    usage_min = None

                elif tag in (TagLocal.STRING_INDEX, TagLocal.STRING_MINIMUM, TagLocal.STRING_MAXIMUM):
                    pass  # we don't care about this information to parse the reports

                else:
                    raise NotImplementedError(f'Unsupported local tag: {bin(tag)}')

    @staticmethod
    def _get_main_item_desc(value: int) -> str:
        fields = [
            'Constant' if value & (1 << 0) else 'Data',
            'Variable' if value & (1 << 1) else 'Array',
            'Relative' if value & (1 << 2) else 'Absolute',
        ]
        if value & (1 << 1):
            # variable only
            fields += [
                'Wrap' if value & (1 << 3) else 'No Wrap',
                'Non Linear' if value & (1 << 4) else 'Linear',
                'No Preferred State' if value & (1 << 5) else 'Preferred State',
                'Null State' if value & (1 << 6) else 'No Null position',
                'Buffered Bytes' if value & (1 << 8) else 'Bit Field',
            ]
        return ', '.join(fields)

    def print(self, level: int = 0, file: TextIO = sys.stdout) -> None:  # noqa: C901
        def printl(string: str) -> None:
            print(' ' * level + string, file=file)

        usage_data: Union[Literal[False], Optional[hid_parser.data._Data]] = False

        for typ, tag, data in self._iterate_raw():
            if typ == Type.MAIN:

                if tag == TagMain.INPUT:
                    if data is None:
                        raise InvalidReportDescriptor('Invalid input item')
                    printl(f'Input ({self._get_main_item_desc(data)})')

                elif tag == TagMain.OUTPUT:
                    if data is None:
                        raise InvalidReportDescriptor('Invalid output item')
                    printl(f'Output ({self._get_main_item_desc(data)})')

                elif tag == TagMain.FEATURE:
                    if data is None:
                        raise InvalidReportDescriptor('Invalid feature item')
                    printl(f'Feature ({self._get_main_item_desc(data)})')

                elif tag == TagMain.COLLECTION:
                    printl(f'Collection ({hid_parser.data.Collections.get_description(data)})')
                    level += 1

                elif tag == TagMain.END_COLLECTION:
                    level -= 1
                    printl('End Collection')

            elif typ == Type.GLOBAL:

                if tag == TagGlobal.USAGE_PAGE:
                    try:
                        printl(f'Usage Page ({hid_parser.data.UsagePages.get_description(data)})')
                        try:
                            usage_data = hid_parser.data.UsagePages.get_subdata(data)
                        except ValueError:
                            usage_data = None
                    except KeyError:
                        printl(f'Usage Page (Unknown 0x{data:04x})')

                elif tag == TagGlobal.LOGICAL_MINIMUM:
                    printl(f'Logical Minimum ({data})')

                elif tag == TagGlobal.LOGICAL_MAXIMUM:
                    printl(f'Logical Maximum ({data})')

                elif tag == TagGlobal.PHYSICAL_MINIMUM:
                    printl(f'Physical Minimum ({data})')

                elif tag == TagGlobal.PHYSICAL_MAXIMUM:
                    printl(f'Physical Maximum ({data})')

                elif tag == TagGlobal.UNIT_EXPONENT:
                    printl(f'Unit Exponent (0x{data:04x})')

                elif tag == TagGlobal.UNIT:
                    printl(f'Unit (0x{data:04x})')

                elif tag == TagGlobal.REPORT_SIZE:
                    printl(f'Report Size ({data})')

                elif tag == TagGlobal.REPORT_ID:
                    printl(f'Report ID (0x{data:02x})')

                elif tag == TagGlobal.REPORT_COUNT:
                    printl(f'Report Count ({data})')

                elif tag == TagGlobal.PUSH:
                    printl(f'Push ({data})')

                elif tag == TagGlobal.POP:
                    printl(f'Pop ({data})')

            elif typ == Type.LOCAL:

                if tag == TagLocal.USAGE:
                    if usage_data is False:
                        raise InvalidReportDescriptor('Usage field found but no usage page')

                    if usage_data:
                        try:
                            printl(f'Usage ({usage_data.get_description(data)})')
                        except KeyError:
                            printl(f'Usage (Unknown, 0x{data:04x})')
                    else:
                        printl(f'Usage (0x{data:04x})')

                elif tag == TagLocal.USAGE_MINIMUM:
                    printl(f'Usage Minimum ({data})')

                elif tag == TagLocal.USAGE_MAXIMUM:
                    printl(f'Usage Maximum ({data})')

                elif tag == TagLocal.DESIGNATOR_INDEX:
                    printl(f'Designator Index ({data})')

                elif tag == TagLocal.DESIGNATOR_MINIMUM:
                    printl(f'Designator Minimum ({data})')

                elif tag == TagLocal.DESIGNATOR_MAXIMUM:
                    printl(f'Designator Maximum ({data})')

                elif tag == TagLocal.STRING_INDEX:
                    printl(f'String Index ({data})')

                elif tag == TagLocal.STRING_MINIMUM:
                    printl(f'String Minimum ({data})')

                elif tag == TagLocal.STRING_MAXIMUM:
                    printl(f'String Maximum ({data})')

                elif tag == TagLocal.DELIMITER:
                    printl(f'Delemiter ({data})')
