# Copyright © 2017 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

import os
from dataclasses import dataclass, field
from functools import total_ordering

try:
    from typing import Self
except ImportError:
    from typing_extensions import Self

from ._clib import Libevdev
import libevdev


@total_ordering
@dataclass(unsafe_hash=True)
class EventCode:
    """
    .. warning ::

        Do not instantiate an object of this class, all objects you'll ever need
        are already present in the libevdev namespace. Use :func:`evbit()`
        to get an :class:`EventCode` from numerical or string values.

    A class representing an evdev event code, e.g. libevdev.EV_ABS.ABS_X or,
    shorter, simply libevdev.ABS_X.
    To use a :class:`EventCode`, use the namespaced name directly::

        >>> print(libevdev.EV_ABS.ABS_X)
        ABS_X:0
        >>> print(libevdev.ABS_Y)
        ABS_Y:1
        >>> code = libevdev.REL_Y
        >>> print(code.type)
        EV_REL:2
        >>> int(code)
        1

    .. attribute:: value

        The numeric value of the event code. This value is also returned when
        the object is converted to ``int``.

    .. attribute:: name

        The string name of this event code. Where the name is not defined, a
        fake name is generated by this module, consisting of the prefix and
        the uppercase hexadecimal value, e.g. ``REL_0B``. These generated
        names (see :func:`EventCode.is_defined()`) should never be used as
        input.

        Note that even defined names are not a stable API. From time to time
        the kernel introduces a new name and aliases the old one. The same
        numeric event code may then return a different name than before.
        This does not affect the :func:`evbit()` function which continues to
        return the correct event code.

    .. attribute:: type

        The :class:`EventType` for this event code

    .. attribute:: is_defined

        ``True`` if this event bit has a ``#define`` in the kernel header.
        :class:`EventCode` objects not defined in the header have a name
        composed of the type name plus the hex code, e.g. ``KEY_2E8``.

        Usually you will not need to check this property unless you need to
        filter for known codes only.
    """

    value: int = field(compare=False)
    name: str = field(compare=False)
    type: "EventType" = field(compare=True)
    is_defined: bool = field(compare=False)

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, EventCode):
            return NotImplemented
        return self.value < other.value

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

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, EventCode):
            return NotImplemented

        return self.value == other.value and self.type == other.type

    def __repr__(self) -> str:
        return f"{self.name}:{self.value}"

    def __str__(self) -> str:
        return repr(self)

    @classmethod
    def from_type_and_code_value(
        cls, evtype: "EventType | int", code: int
    ) -> Self | None:
        """
        Return the **known** EventCode with the given value. The type
        must be known to libevdev at libevdev compilation time:

            >>> EventCode.from_type_and_code_value(libevdev.EV_ABS, 0)
            ABS_X:0
            >>> EventCode.from_type_and_code_value(3, 0)
            ABS_X:0
            >>> EventCode.from_type_and_code_value(99, 999)
            None
        """
        if isinstance(evtype, int):
            t = EventType.from_value(evtype)
            if t is None:
                return None
        else:
            t = evtype

        return next((c for c in t.codes if c.value == code), None)  # type: ignore

    @classmethod
    def from_name(cls, name: str) -> Self | None:
        """
        Return the **known** EventCode with the given name. The type
        must be known to libevdev at libevdev compilation time::

            >>> EventCode.from_name("ABS_X")
            ABS_X:0
            >>> EventCode.from_name("KEY_ESC")
            KEY_ESC:1
            >>> EventCode.from_name("DOES_NOT_EXIST")
            None
        """
        if name.startswith("INPUT_PROP_") or name.startswith("EV_"):
            return None
        return getattr(libevdev, name, None)


@total_ordering
@dataclass(unsafe_hash=True)
class EventType:
    """
    .. warning ::

        Do not instantiate an object of this class, all objects you'll ever need
        are already present in the libevdev namespace. Use :func:`evbit()`
        to get an :class:`EventType` from numerical or string values.

    A class represending an evdev event type (e.g. EV_ABS). All event codes
    within this type are available as class constants::

        >>> print(libevdev.EV_ABS)
        EV_ABS:3
        >>> print(libevdev.ABS_X)
        ABS_X:0
        >>> print(int(libevdev.EV_ABS.ABS_Y))
        1
        >>> print(libevdev.EV_ABS.max)
        63
        >>> print(libevdev.EV_ABS.ABS_MAX)
        63
        >>> for code in libevdev.EV_ABS.codes[:3]:
        ...     print(code)
        ...
        ABS_X:0
        ABS_Y:1
        ABS_Z:2

    .. attribute:: value

        The numeric value of the event type. This value is also returned when
        the object is converted to ``int``.

    .. attribute:: name

        The string name of this event type

    .. attribute:: codes

        A list of :class:`EventCode` objects for this type

    .. attribute:: max

        The maximum event code permitted in this type as integer
    """

    value: int = field(compare=False)
    name: str = field(compare=False)
    codes: list[EventCode] = field(compare=False, hash=False)
    max: int | None = field(compare=False, hash=False)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, EventType):
            return NotImplemented
        return self.value == other.value

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, EventType):
            return NotImplemented
        return self.value < other.value

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

    def __repr__(self) -> str:
        return f"{self.name}:{self.value}"

    def __str__(self) -> str:
        return repr(self)

    @classmethod
    def from_value(cls, value: int) -> Self | None:
        """
        Return the **known** EventType with the given value. The type
        must be known to libevdev at libevdev compilation time.
        """
        return next((t for t in libevdev.types if t.value == value), None)  # type: ignore

    @classmethod
    def from_name(cls, name: str) -> Self | None:
        """
        Return the **known** EventType with the given name. The type
        must be known to libevdev at libevdev compilation time.
        """
        return next((t for t in libevdev.types if t.name == name), None)  # type: ignore


@total_ordering
@dataclass(frozen=True)
class InputProperty:
    """
    .. warning ::

        Do not instantiate an object of this class, all objects you'll ever need
        are already present in the libevdev namespace. Use :func:`propbit()`
        to get an :class:`InputProperty` from numerical or string values.

    A class representing an evdev input property::

        >>> print(libevdev.INPUT_PROP_DIRECT)
        INPUT_PROP_DIRECT:1
        >>> int(libevdev.INPUT_PROP_DIRECT)
        1

    .. attribute:: value

        The numeric value of the property. This value is also returned when
        the object is converted to ``int``.

    .. attribute:: name

        The string name of this property

    .. attribute:: is_defined

        ``True`` if this event bit has a ``#define`` in the kernel header.
        :class:`InputProperty` objects not defined in the header have a name
        composed of the type name plus the hex code, e.g. ``INPUT_PROP_2E8``.

        Usually you will not need to check this property unless you need to
        filter for known codes only.
    """

    value: int = field(compare=False)
    name: str = field(compare=False)
    is_defined: bool = field(compare=False, default=True)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, InputProperty):
            return NotImplemented
        return self.value == other.value

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, InputProperty):
            return NotImplemented
        return self.value < other.value

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

    def __repr__(self) -> str:
        return f"{self.name}:{self.value}"

    def __str__(self) -> str:
        return repr(self)

    @classmethod
    def from_value(cls, value: int) -> Self | None:
        """
        Return the **known** InputProperty with the given value. The property
        must be known to libevdev at libevdev compilation time. Example::

            >>> InputProperty.from_value(0)
            INPUT_PROP_POINTER:0
            >>> InputProperty.from_value(9999)
            None
        """
        return next((p for p in libevdev.props if p.value == value), None)  # type: ignore

    @classmethod
    def from_name(cls, name: str) -> Self | None:
        """
        Return the **known** InputProperty with the given name. The property
        must be known to libevdev at libevdev compilation time. Example::

        The name must contain the ``INPUT_PROP_`` prefix.

            >>> InputProperty.from_name("INPUT_PROP_POINTER")
            INPUT_PROP_POINTER:0
            >>> InputProperty.from_name("INPUT_PROP_FOOBAR")
            None
        """
        return next((p for p in libevdev.props if p.name == name), None)  # type: ignore

    @classmethod
    def create(cls, value: int, name: str) -> Self:
        """
        Instantiate a new InputProperty with the given value and name.

        This function should only be used for the creation of new properties
        not known to libevdev. No checks are performed. Example::

            >>> InputProperty.create(0, "INPUT_PROP_FOOBAR")
            INPUT_PROP_FOOBAR:0
        """
        return cls(value=value, name=name, is_defined=False)


def evbit(
    evtype: int | str, evcode: int | str | None = None
) -> EventCode | EventType | None:
    """
    Takes an event type and an (optional) event code and returns the Enum
    representing that type or code, whichever applies. For example::

        >>> print(libevdev.evbit(0))
        EV_SYN:0

        >>> print(libevdev.evbit(2))
        EV_REL:2

        >>> print(libevdev.evbit(2, 1))
        REL_Y:1

        >>> print(libevdev.evbit(3, 4))
        ABS_RY:4

        >>> print(libevdev.evbit('EV_ABS'))
        EV_ABS:3

        >>> print(libevdev.evbit('EV_ABS', 'ABS_X'))
        ABS_X:0

    A special case is the lookup of an string-based event code without
    the type. Where the string identifier is unique, this will return the
    right value.

        >>> print(libevdev.evbit('ABS_X'))
        ABS_X:0

    The return value can be used in the libevdev API wherever an
    :class:`EventCode` or :class:`EventType` is expected.

    Notable behavior for invalid types or names:

    * If the type does not exist, this function returns None
    * If the type exists but the event code's numeric value does not have a
      symbolic name (and is within the allowed max of the type), this
      function returns a valid event code
    * If the code is outside the allowed maximum for the given type, this
      function returns None
    * If the type name exists but the string value is not a code name, this
      function returns None

    Examples for the above behaviour::

        >>> print(libevdev.evbit(8))
        None
        >>> print(libevdev.evbit('INVALID'))
        None
        >>> print(libevdev.evbit('EV_ABS', 62))
        ABS_3E:62
        >>> print(libevdev.evbit('EV_ABS', 5000))
        None
        >>> print(libevdev.evbit('EV_ABS', 'INVALID'))
        None

    :param evtype: the numeric value or string identifying the event type
    :param evcode: the numeric value or string identifying the event code
    :return: An event code value representing the code
    :rtype: EventCode or EventType
    """
    etype = None
    for t in libevdev.types:
        if t.value == evtype or t.name == evtype:
            etype = t
            break

    if evcode is None and isinstance(evtype, str) and not evtype.startswith("EV_"):
        for t in libevdev.types:
            for c in t.codes:
                if c.name == evtype:
                    return c

    if etype is None or evcode is None:
        return etype

    ecode = None
    for c in etype.codes:
        if c.value == evcode or c.name == evcode:
            ecode = c

    return ecode


def propbit(prop: int | str) -> InputProperty | None:
    """
    Takes a property value and returns the :class:`InputProperty`
    representing that property::

        >>> print(libevdev.propbit(0))
        INPUT_PROP_POINTER:0
        >>> print(libevdev.propbit('INPUT_PROP_POINTER'))
        INPUT_PROP_POINTER:0
        >>> print(libevdev.propbit(1000))
        None
        >>> print(libevdev.propbit('Invalid'))
        None

    :param prop: the numeric value or string identifying the property
    :return: the converted :class:`InputProperty` or None if it does not exist
    :rtype: InputProperty
    """
    return next((p for p in libevdev.props if p.value == prop or p.name == prop), None)


def _load_consts() -> None:
    """
    Loads all event type, code and property names and makes them available
    as enums in the module. Use as e.g. libevdev.EV_SYN.SYN_REPORT.

    Available are::

    libevdev.types ... an list containing all event types, e.g.
                         libevdev.EV_TYPES.EV_REL

    libevdev.EV_REL ... an enum containing all REL event types, e.g.
                        libevdev.EV_REL.REL_X. The name of each enum value
                        is the string of the code ('REL_X'), the value is the integer
                        value of that code.

    libevdev.EV_ABS ... as above, but for EV_ABS

    libevdev.EV_BITS ... libevdev.EV_FOO as an enum

    Special attributes are (an apply to all EV_foo enums):
        libevdev.EV_REL.type ... the EV_TYPES entry of the event type
        libevdev.EV_REL.max  ... the maximum code in this event type
    """
    Libevdev()  # classmethods, need to make sure it's loaded at once

    tmax = Libevdev.event_to_value("EV_MAX")
    assert tmax is not None

    types = []

    for t in range(tmax + 1):
        tname = Libevdev.event_to_name(t)
        if tname is None:
            continue

        cmax = Libevdev.type_max(t)

        et = EventType(name=tname, value=t, max=cmax, codes=[])
        types.append(et)

        # libevdev.EV_REL, libevdev.EV_ABS, etc.
        setattr(libevdev, tname, et)

        if cmax is None:
            continue

        codes = []
        for c in range(cmax + 1):
            cname = Libevdev.event_to_name(t, c)
            name = cname
            has_name = cname is not None
            # For those without names, we use the type name plus
            # hexcode for the actual name, but a prefixing underscore for
            # the class name (it's not stable API).
            # i.e. libedev.EV_REL._REL_0B.name == 'REL_0B'
            if name is None:
                name = f"{tname[3:]}_{c:02X}"

            if cname is None:
                cname = f"_{name}"

            ec = EventCode(type=et, name=name, value=c, is_defined=has_name)

            # libdevdev.EV_REL.FOO
            setattr(et, cname, ec)

            # defined event codes are set on the libevdev module directly
            if has_name:
                # libevdev.FOO
                setattr(libevdev, cname, ec)

            codes.append(ec)

        et.codes = codes

    # list of all types
    setattr(libevdev, "types", types)

    pmax = Libevdev.property_to_value("INPUT_PROP_MAX")
    assert pmax is not None
    props = []
    for p in range(pmax + 1):
        pname = Libevdev.property_to_name(p)
        if pname is None:
            continue

        ip = InputProperty(name=pname, value=p)

        setattr(libevdev, pname, ip)
        props.append(ip)

    setattr(libevdev, "props", props)


if not os.environ.get("READTHEDOCS"):
    _load_consts()
