## Copyright (C) 2012-2013  Daniel Pavel
## Copyright (C) 2014-2024  Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import annotations

from enum import Flag
from enum import IntEnum
from typing import List

from .common import NamedInts

"""HID constants for HID++ 1.0.

Most of them as defined by the official Logitech HID++ 1.0
documentation, some of them guessed.
"""

DEVICE_KIND = NamedInts(
    unknown=0x00,
    keyboard=0x01,
    mouse=0x02,
    numpad=0x03,
    presenter=0x04,
    remote=0x07,
    trackball=0x08,
    touchpad=0x09,
    tablet=0x0A,
    gamepad=0x0B,
    joystick=0x0C,
    headset=0x0D,  # not from Logitech documentation
    remote_control=0x0E,  # for compatibility with HID++ 2.0
    receiver=0x0F,  # for compatibility with HID++ 2.0
)


class PowerSwitchLocation(IntEnum):
    UNKNOWN = 0x00
    BASE = 0x01
    TOP_CASE = 0x02
    EDGE_OF_TOP_RIGHT_CORNER = 0x03
    TOP_LEFT_CORNER = 0x05
    BOTTOM_LEFT_CORNER = 0x06
    TOP_RIGHT_CORNER = 0x07
    BOTTOM_RIGHT_CORNER = 0x08
    TOP_EDGE = 0x09
    RIGHT_EDGE = 0x0A
    LEFT_EDGE = 0x0B
    BOTTOM_EDGE = 0x0C

    @classmethod
    def location(cls, loc: int) -> PowerSwitchLocation:
        try:
            return cls(loc)
        except ValueError:
            return cls.UNKNOWN


class NotificationFlag(Flag):
    """Some flags are used both by devices and receivers.

    The Logitech documentation mentions that the first and last (third)
    byte are used for devices while the second is used for the receiver.
    In practise, the second byte is also used for some device-specific
    notifications (keyboard illumination level). Do not simply set all
    notification bits if the software does not support it. For example,
    enabling keyboard_sleep_raw makes the Sleep key a no-operation
    unless the software is updated to handle that event.

    Observations:
    - wireless and software present seen on receivers,
    reserved_r1b4 as well
    - the rest work only on devices as far as we can tell right now
    In the future would be useful to have separate enums for receiver
    and device notification flags, but right now we don't know enough.
    Additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
    """

    @classmethod
    def flag_names(cls, flag_bits: int) -> List[str]:
        """Extract the names of the flags from the integer."""
        indexed = {item.value: item.name for item in cls}

        flag_names = []
        unknown_bits = flag_bits
        for k in indexed:
            # Ensure that the key (flag value) is a power of 2 (a single bit flag)
            assert bin(k).count("1") == 1
            if k & flag_bits == k:
                unknown_bits &= ~k
                flag_names.append(indexed[k].replace("_", " ").lower())

        # Yield any remaining unknown bits
        if unknown_bits != 0:
            flag_names.append(f"unknown:{unknown_bits:06X}")
        return flag_names

    NUMPAD_NUMERICAL_KEYS = 0x800000
    F_LOCK_STATUS = 0x400000
    ROLLER_H = 0x200000
    BATTERY_STATUS = 0x100000  # send battery charge notifications (0x07 or 0x0D)
    MOUSE_EXTRA_BUTTONS = 0x080000
    ROLLER_V = 0x040000
    POWER_KEYS = 0x020000  # system control keys such as Sleep
    KEYBOARD_MULTIMEDIA_RAW = 0x010000  # consumer controls such as Mute and Calculator
    MULTI_TOUCH = 0x001000  # notify on multi-touch changes
    SOFTWARE_PRESENT = 0x000800  # software is controlling part of device behaviour
    LINK_QUALITY = 0x000400  # notify on link quality changes
    UI = 0x000200  # notify on UI changes
    WIRELESS = 0x000100  # notify when the device wireless goes on/off-line
    CONFIGURATION_COMPLETE = 0x000004
    VOIP_TELEPHONY = 0x000002
    THREED_GESTURE = 0x000001


def flags_to_str(flag_bits: int | None, fallback: str) -> str:
    flag_names = []
    if flag_bits is not None:
        if flag_bits == 0:
            flag_names = (fallback,)
        else:
            flag_names = NotificationFlag.flag_names(flag_bits)
    return f"\n{' ':15}".join(sorted(flag_names))


class ErrorCode(IntEnum):
    INVALID_SUB_ID_COMMAND = 0x01
    INVALID_ADDRESS = 0x02
    INVALID_VALUE = 0x03
    CONNECTION_REQUEST_FAILED = 0x04
    TOO_MANY_DEVICES = 0x05
    ALREADY_EXISTS = 0x06
    BUSY = 0x07
    UNKNOWN_DEVICE = 0x08
    RESOURCE_ERROR = 0x09
    REQUEST_UNAVAILABLE = 0x0A
    UNSUPPORTED_PARAMETER_VALUE = 0x0B
    WRONG_PIN_CODE = 0x0C


class PairingError(IntEnum):
    DEVICE_TIMEOUT = 0x01
    DEVICE_NOT_SUPPORTED = 0x02
    TOO_MANY_DEVICES = 0x03
    SEQUENCE_TIMEOUT = 0x06


class BoltPairingError(IntEnum):
    DEVICE_TIMEOUT = 0x01
    FAILED = 0x02


class Registers(IntEnum):
    """Known HID registers.

    Devices usually have a (small) sub-set of these. Some registers are only
    applicable to certain device kinds (e.g. smooth_scroll only applies to mice).
    """

    # Generally applicable
    NOTIFICATIONS = 0x00
    FIRMWARE = 0xF1

    # only apply to receivers
    RECEIVER_CONNECTION = 0x02
    RECEIVER_PAIRING = 0xB2
    DEVICES_ACTIVITY = 0x2B3
    RECEIVER_INFO = 0x2B5
    BOLT_DEVICE_DISCOVERY = 0xC0
    BOLT_PAIRING = 0x2C1
    BOLT_UNIQUE_ID = 0x02FB

    # only apply to devices
    MOUSE_BUTTON_FLAGS = 0x01
    KEYBOARD_HAND_DETECTION = 0x01
    DEVICES_CONFIGURATION = 0x03
    BATTERY_STATUS = 0x07
    KEYBOARD_FN_SWAP = 0x09
    BATTERY_CHARGE = 0x0D
    KEYBOARD_ILLUMINATION = 0x17
    THREE_LEDS = 0x51
    MOUSE_DPI = 0x63

    # notifications
    PASSKEY_REQUEST_NOTIFICATION = 0x4D
    PASSKEY_PRESSED_NOTIFICATION = 0x4E
    DEVICE_DISCOVERY_NOTIFICATION = 0x4F
    DISCOVERY_STATUS_NOTIFICATION = 0x53
    PAIRING_STATUS_NOTIFICATION = 0x54


# Subregisters for receiver_info register
class InfoSubRegisters(IntEnum):
    SERIAL_NUMBER = 0x01  # not found on many receivers
    FW_VERSION = 0x02
    RECEIVER_INFORMATION = 0x03
    PAIRING_INFORMATION = 0x20  # 0x2N, by connected device
    EXTENDED_PAIRING_INFORMATION = 0x30  # 0x3N, by connected device
    DEVICE_NAME = 0x40  # 0x4N, by connected device
    BOLT_PAIRING_INFORMATION = 0x50  # 0x5N, by connected device
    BOLT_DEVICE_NAME = 0x60  # 0x6N01, by connected device


class DeviceFeature(Flag):
    """Features for devices.

    Flags taken from
    https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
    """

    @classmethod
    def flag_names(cls, flag_bits: int) -> List[str]:
        """Extract the names of the flags from the integer."""
        indexed = {item.value: item.name for item in cls}

        flag_names = []
        unknown_bits = flag_bits
        for k in indexed:
            # Ensure that the key (flag value) is a power of 2 (a single bit flag)
            assert bin(k).count("1") == 1
            if k & flag_bits == k:
                unknown_bits &= ~k
                flag_names.append(indexed[k].replace("_", " ").lower())

        # Yield any remaining unknown bits
        if unknown_bits != 0:
            flag_names.append(f"unknown:{unknown_bits:06X}")
        return flag_names

    RESERVED1 = 0x010000
    SPECIAL_BUTTONS = 0x020000
    ENHANCED_KEY_USAGE = 0x040000
    FAST_FW_REV = 0x080000
    RESERVED2 = 0x100000
    RESERVED3 = 0x200000
    SCROLL_ACCEL = 0x400000
    BUTTONS_CONTROL_RESOLUTION = 0x800000
    INHIBIT_LOCK_KEY_SOUND = 0x000001
    RESERVED4 = 0x000002
    MX_AIR_3D_ENGINE = 0x000004
    HOST_CONTROL_LEDS = 0x000008
    RESERVED5 = 0x000010
    RESERVED6 = 0x000020
    RESERVED7 = 0x000040
    RESERVED8 = 0x000080
