# SPDX-License-Identifier: MIT
# Copyright (c) 2018-2023 The Pybricks Authors

"""Generic input/output devices."""

from __future__ import annotations

from typing import Tuple, Optional, overload, TYPE_CHECKING

from . import _common
from .parameters import Port as _Port

if TYPE_CHECKING:
    from ._common import MaybeAwaitable, MaybeAwaitableBytes, MaybeAwaitableTuple
    from .parameters import Number


class PUPDevice:
    """Powered Up motor or sensor."""

    def __init__(self, port: _Port):
        """PUPDevice(port)

        Arguments:
            port (Port): Port to which the device is connected.
        """

    def info(self) -> dict:
        """info() -> Dict

        Gets information about the device.

        For passive devices (such as DC motors or lights), returns a
        dictionary with only the ``id`` key.

        For UART devices, returns a dictionary with an ``id`` key and a
        ``modes`` key. The ``modes`` value is a tuple of tuples, one per
        mode, each containing the mode name, number of values, and data
        type.

        Returns:
            Dictionary with device information.
        """

    def read(self, mode: int) -> MaybeAwaitableTuple:
        """read(mode) -> Tuple

        Reads values from a given mode.

        For passive touch sensors, this returns a single boolean value
        indicating whether the sensor is pressed, regardless of the
        ``mode`` argument.

        Raises an error for other passive devices such as DC motors and
        lights, which do not support reading.

        Arguments:
            mode (int): Device mode.

        Returns:
            Values read from the device.

        Raises:
            OSError: If the device is a passive device that does not
                support reading (e.g. a DC motor or light).
        """

    def write(self, mode: int, data: Tuple) -> MaybeAwaitable:
        """write(mode, data)

        Writes values to the device. Only selected UART devices and modes
        support this.

        Arguments:
            mode (int): Device mode.
            data (tuple): Values to be written. The number of values and
                their types must match what the device expects for the
                given mode.

        Raises:
            OSError: If the device is a passive device that does not
                support writing.
            ValueError: If the mode is invalid, the mode is not writable,
                the number of values does not match, or a value is out of
                range for its data type.
        """

    def reset(self) -> None:
        """reset()

        Resets the UART device. After this, it should automatically synchronize
        and be ready for use after a few seconds. This is useful to forcefully
        re-trigger what such a sensor does when plugged in.

        Raises:
            OSError: If the device is a passive device that does not
                support reset.
        """


class LUMPDevice(PUPDevice):
    """Devices using the LEGO UART Messaging Protocol.

    See the equivalent :class:`PUPDevice() <pybricks.iodevices.PUPDevice>` for
    a description of available methods.

    On EV3, this class provides access to UART devices only. You can use other
    classes to interact with passive devices.
    """


class DCMotor(_common.DCMotor):
    """DC Motor for LEGO® MINDSTORMS EV3."""


class AnalogSensor:
    """Generic or custom analog sensor."""

    def __init__(self, port: _Port, custom: bool = False):
        """AnalogSensor(port, custom=False)

        Arguments:
            port (Port): Port to which the sensor is connected.
            custom (bool): Set to ``True`` if you are using a custom analog
                sensor.

        Raises:
            OSError: If no standard LEGO analog sensor is
                detected on the port. Only applies if ``custom=False``.
        """

    def voltage(self) -> int:
        """voltage() -> int: mV

        Measures analog voltage.

        Returns:
            Analog voltage.
        """

    def resistance(self) -> int:
        """resistance() -> int: Ω

        Measures resistance.

        This value is only meaningful if the analog device is a passive load
        such as a resistor or thermistor. It is calculated assuming a 10 kΩ
        internal pull-up resistor forming a voltage divider.

        If the circuit is open (no load connected), the maximum integer value
        is returned.

        Returns:
            Resistance of the analog device, or the maximum integer value
            if the circuit is open.
        """

    def active(self) -> None:
        """active()

        Sets sensor to active mode. This sets pin 5 of the sensor
        port to `high`.

        This is used in some analog
        sensors to control a switch. For example, if you use the NXT Light
        Sensor as a custom analog sensor, this method will turn the light on.
        From then on, ``voltage()`` returns the raw reflected light value.
        """

    def passive(self) -> None:
        """passive()

        Sets sensor to passive mode. This sets pin 5 of the sensor
        port to `low`.

        This is used in some analog
        sensors to control a switch. For example, if you use the NXT Light
        Sensor as a custom analog sensor, this method will turn the light off.
        From then on, ``voltage()`` returns the raw ambient light value.
        """


class I2CDevice:
    """Generic or custom I2C device.

    Note: Use the ``power_pin`` option at your own risk. Applying power to the
    pins can damage your hub or device if you are not careful. When you use
    this option, you will be prompted to confirm that you understand the risks.
    """

    def __init__(
        self,
        port: _Port,
        address: int,
        custom: bool = False,
        power_pin: int = 0,
        nxt_quirk: bool = False,
    ):
        """I2CDevice(port, address, custom=False, power_pin=0, nxt_quirk=False)

        Arguments:
            port (Port): Port to which the device is connected.
            address (int): I2C address of the client device. See
                :ref:`I2C Addresses <i2caddress>`.
            custom (bool): Set to ``True`` if you are using a custom I2C device.
            power_pin (int): Power requirements for the device. Use
                ``0`` (default) for no power on the pins. On NXT and EV3, use ``1``
                to apply battery power to pin 1. Other pins are not supported.
            nxt_quirk (bool): Set to ``True`` for older NXT I2C sensors that
                need slower compatibility timing to communicate reliably,
                such as the old NXT Ultrasonic Sensor.
        """

    @overload
    def read(
        self, reg: Optional[int] = None, length: int = 1
    ) -> MaybeAwaitableBytes: ...

    @overload
    def read(
        self, reg: Optional[int] = None, length: int = 1, map: callable = ...
    ) -> MaybeAwaitable: ...

    def read(
        self, reg: Optional[int] = None, length: int = 1, map=None
    ) -> MaybeAwaitableBytes:
        """read(reg=None, length=1) -> bytes
        read(reg=None, length=1, map=callable) -> Any

        Reads bytes starting at a given register.

        Arguments:
            reg (int): Register at which to begin reading: 0--255 or
                0x00--0xFF. Use ``None`` to read without writing a register
                address first.
            length (int): How many bytes to read.
            map (callable): Optional callable to convert the returned bytes.
                If given, it is called with the bytes as its argument and its
                return value is returned instead.

        Returns:
            Bytes returned from the device, or the return value of ``map``
            if a callable was provided.
        """

    def write(
        self, reg: Optional[int] = None, data: Optional[bytes] = None
    ) -> MaybeAwaitable:
        """write(reg=None, data=None)

        Writes bytes, optionally starting at a given register.

        Arguments:
            reg (int): Register at which to begin writing: 0--255 or
                0x00--0xFF. Use ``None`` to write without a register prefix.
            data (bytes): Bytes to be written. Use ``None`` to write nothing
                after the register.

        Raises:
            ValueError: If ``reg`` is given and ``data`` is more than 32 bytes.
                To write more data, omit the ``reg`` argument and include the
                register as the first byte of ``data``.
        """


class UARTDevice:
    """Generic UART device.

    Note: Use the ``power_pin`` option at your own risk. Applying power to the
    pins can damage your hub or device if you are not careful. When you use
    this option, you will be prompted to confirm that you understand the risks.
    """

    def __init__(
        self,
        port: _Port,
        baudrate: int = 115200,
        timeout: Optional[int] = None,
        power_pin: int = 0,
    ):
        """UARTDevice(port, baudrate=115200, timeout=None, power_pin=0)

        Arguments:
            port (Port): Port to which the device is connected. On Powered UP
                hubs, all ports are supported. On EV3, only the sensor ports
                are supported.
            baudrate (int): Baudrate of the UART device.
            timeout (Number, ms): How long to wait during ``read`` and
                ``write`` before giving up. If you choose ``None``, it will
                wait forever.
            power_pin (int): Power requirements for the device. Use ``0``
                (default) for no power on the pins. On Powered UP hubs, use
                ``1`` or ``2`` for pin 1 or 2, respectively. This will apply
                battery power to the pin, equivalent to powering a motor.
                On EV3, use ``1`` to apply battery power to pin 1, though only
                minimal current is available.

        Raises:
            ValueError: If ``timeout`` is 0 or negative.
        """

    def read(self, length: int = 1) -> MaybeAwaitableBytes:
        """read(length=1) -> bytes

        Reads a given number of bytes from the buffer.

        Your program will wait until the requested number of bytes are
        received. If this takes longer than ``timeout``, the ``ETIMEDOUT``
        exception is raised.

        Arguments:
            length (int): How many bytes to read. Must be at least 1.

        Returns:
            Bytes returned from the device.

        Raises:
            ValueError: If ``length`` is less than 1.
            OSError: If the read takes longer than ``timeout``.
        """

    def read_all(self) -> bytes:
        """read_all() -> bytes

        Reads all bytes currently in the buffer. Returns immediately without
        waiting, even if the buffer is empty.

        Returns:
            Bytes currently in the buffer, or an empty bytes object if there
            is nothing to read.
        """

    def write(self, data: bytes) -> MaybeAwaitable:
        """write(data)

        Writes bytes to the device.

        Arguments:
            data (bytes): Bytes to be written.

        Raises:
            TypeError: If ``data`` is not ``bytes``, ``bytearray``, or ``str``.
            OSError: If the write takes longer than ``timeout``.
        """

    def waiting(self) -> int:
        """waiting() -> int

        Gets how many bytes are still waiting to be read.

        Returns:
            Number of bytes in the buffer.
        """

    def set_baudrate(self, baudrate: int) -> None:
        """set_baudrate(baudrate)

        Changes the baud rate of the UART device.

        Arguments:
            baudrate (int): Not all values may be supported.

        Raises:
            ValueError: If ``baudrate`` is less than 1.
        """

    def wait_until(self, pattern: bytes) -> MaybeAwaitable:
        """wait_until(pattern)

        Waits until a specific byte sequence is received. Bytes that do not
        match the pattern are discarded.

        Arguments:
            pattern (bytes): Byte sequence to wait for. Must not be empty.

        Raises:
            ValueError: If ``pattern`` is empty.
            OSError: If this method is already in progress.
        """

    def clear(self) -> None:
        """clear()

        Empties the receive buffer."""


class LWP3Device:
    """
    Connects to a hub running official LEGO firmware using the
    `LEGO Wireless Protocol v3`_.

    .. _`LEGO Wireless Protocol v3`:
        https://lego.github.io/lego-ble-wireless-protocol-docs/
    """

    def __init__(
        self,
        hub_kind: int,
        name: str = None,
        timeout: int = 10000,
        pair: bool = False,
        num_notifications: int = 8,
        connect: bool = True,
    ):
        """LWP3Device(hub_kind, name=None, timeout=10000, pair=False, num_notifications=8, connect=True)

        Arguments:
            hub_kind (int):
                The `hub type identifier`_ of the hub to connect to.
            name (str):
                The name of the hub to connect to or ``None`` to connect to any
                hub.
            timeout (int):
                The time, in milliseconds, to wait for a connection before
                raising an exception.
            pair (bool): Whether to attempt pairing for a secure connection.
                This is required for some newer hubs.
            num_notifications (int): Number of incoming messages from the remote
                hub to store before discarding older messages.
            connect (bool): Choose ``False`` to skip connecting.
                ``connect()`` can be called later to connect.

        .. versionchanged:: 3.6

            Added ``pair`` parameter.

        .. versionchanged:: 3.7

            Added ``num_notifications`` parameter.

        .. _`hub type identifier`:
            https://github.com/pybricks/technical-info/blob/master/assigned-numbers.md#hub-type-ids
        """

    def connect(self) -> MaybeAwaitable:
        """connect()

        Connects to the device. Only needed if you disconnected or initialized
        with ``connect=False``.

        Raises:
            OSError: If the connection attempt fails or times out.
        """

    @overload
    def name(self, name: str) -> MaybeAwaitable: ...

    @overload
    def name(self) -> str: ...

    def name(self, *args):
        """name(name)
        name() -> str

        Sets or gets the Bluetooth name of the device.

        Arguments:
            name (str): New Bluetooth name of the device. If no name is given,
                this method returns the current name.

        Raises:
            OSError: If the device is not connected.
        """

    def write(self, buf: bytes) -> MaybeAwaitable:
        """write(buf)

        Sends a message to the remote hub.

        Arguments:
            buf (bytes): The raw binary message to send. Maximum 20 bytes.

        Raises:
            ValueError: If the message exceeds 20 bytes.
            OSError: If the device is not connected or the write fails.
        """

    def read(self) -> bytes | None:
        """read() -> bytes | None

        Retrieves the oldest buffered message received from the remote hub.

        If all buffered messages have already been read, this returns ``None``.

        Returns:
            The oldest raw binary message or ``None`` if there are no more messages.

        .. versionchanged:: 3.7

            Now supports reading multiple buffered messages instead of blocking
            until one new message was received.
        """

    def disconnect(self) -> MaybeAwaitable:
        """disconnect()

        Disconnects the device.

        Raises:
            OSError: If disconnecting fails.
        """


class XboxController:
    """Use the Microsoft® Xbox® controller as a sensor in your projects to
    control them remotely.

    The hub will scan for the controller and connect to it. It will disconnect
    when the program ends.

    For tips on connectivity and pairing, see :ref:`below <xbox-controller-pairing>`.
    """

    buttons = _common.Keypad([])

    def __init__(
        self,
        joystick_deadzone: int = 10,
        name: Optional[str] = None,
        timeout: int = 10000,
        connect: bool = True,
    ):
        """__init__(joystick_deadzone=10, name=None, timeout=10000, connect=True)

        Arguments:
            joystick_deadzone (Number, %): Joystick deadzone (0 to 100). Values
                below this threshold in both axes will be reported as 0 to
                prevent stick drift.
            name (str): The Bluetooth name of the Xbox controller to connect to,
                or ``None`` to connect to any available controller.
            timeout (Number, ms): How long to wait for a connection before
                giving up. Choose ``None`` to wait indefinitely.
            connect (bool): Choose ``False`` to skip connecting to the controller.
                ``connect()`` can be called later to connect.
        """

    def connect(self) -> MaybeAwaitable:
        """connect()

        Connects to the Xbox controller. Only needed if you disconnected or
        initialized the controller with ``connect=False``.
        """

    def disconnect(self) -> MaybeAwaitable:
        """disconnect()

        Disconnects the Xbox controller.
        """

    def name(self) -> str:
        """name() -> str

        Gets the Bluetooth name of the connected controller.

        Returns:
            Bluetooth name of the controller.

        Raises:
            OSError: If the controller is not connected.
        """

    def state(self) -> Tuple:
        """state() -> Tuple

        Gets all raw controller input values as a single tuple. This gives
        access to values not exposed by the other methods.

        The joystick axes (x, y, z, rz) are centered at 0. The trigger axes
        are raw 10-bit values (0-1023).

        Returns:
            Tuple of ``(x, y, z, rz, left_trigger, right_trigger, dpad,
            buttons, upload, profile, trigger_switches, paddles)``.

        Raises:
            OSError: If the controller is not connected.
        """

    def joystick_left(self) -> Tuple[int, int]:
        """joystick_left() -> Tuple

        Gets the left joystick position as percentages between -100%
        and 100%. The center position is (0, 0). A square deadzone is applied:
        if both axes are within the deadzone, both are reported as 0.

        Returns:
            Tuple of X (horizontal) and Y (vertical) position.

        Raises:
            OSError: If the controller is not connected.
        """

    def joystick_right(self) -> Tuple[int, int]:
        """joystick_right() -> Tuple

        Gets the right joystick position as percentages between -100%
        and 100%. The center position is (0, 0). A square deadzone is applied:
        if both axes are within the deadzone, both are reported as 0.

        Returns:
            Tuple of X (horizontal) and Y (vertical) position.

        Raises:
            OSError: If the controller is not connected.
        """

    def triggers(self) -> Tuple[int, int]:
        """triggers() -> Tuple

        Gets the left and right trigger positions as percentages between 0%
        and 100%.

        Returns:
            Tuple of left and right trigger positions.

        Raises:
            OSError: If the controller is not connected.
        """

    def dpad(self) -> int:
        """dpad() -> int

        Gets the direction-pad value. ``1`` is up, ``2`` is up-right, ``3``
        is right, ``4`` is down-right, ``5`` is down, ``6`` is down-left,
        ``7`` is left, ``8`` is up-left, and ``0`` is not pressed.

        This is essentially the same as reading the state of the
        ``Button.UP``, ``Button.RIGHT``, ``Button.DOWN``, and ``Button.LEFT``
        buttons, but this method conveniently returns a number that indicates
        a direction.

        Returns:
            Direction-pad position, indicating a direction.

        Raises:
            OSError: If the controller is not connected.
        """

    def profile(self) -> int:
        """profile() -> int

        Gets the current profile of the controller. Only available on the
        Xbox Elite Controller Series 2.

        Returns:
            Profile number.

        Raises:
            OSError: If the controller is not connected.
        """

    def rumble(
        self,
        power: Number | Tuple[Number, Number, Number, Number] = 100,
        duration: int = 200,
        count: int = 1,
        delay: int = 100,
    ) -> MaybeAwaitable:
        """rumble(power=100, duration=200, count=1, delay=100)

        Makes the builtin actuators rumble, creating force feedback.

        If you give a single ``power`` value, the left and right main actuators
        will both rumble with that power while the trigger actuators stay off.
        For more fine-grained control, set ``power`` as a tuple of four values,
        which control the left main actuator, right main actuator, left trigger
        actuator, and the right trigger actuator, respectively. For example,
        ``power=(0, 0, 100, 0)`` makes the left trigger rumble at full power.

        The rumble runs in the background while your program continues. To
        make your program wait, just pause the program for a matching duration.
        For one rumble, this equals ``duration``. For multiple rumbles, this
        equals ``count * (duration + delay)``.

        This method does nothing if all actuator powers are zero, if
        ``duration`` is zero, or if ``count`` is less than 1.

        Arguments:
            power (Number, % or tuple): Rumble power. A single value applies
                to both main actuators (0-100%). A tuple applies individually
                to (left handle, right handle, left trigger, right trigger).
            duration (Number, ms): Duration of each rumble. Capped at 2500 ms.
            count (int): Number of rumbles (0-100).
            delay (Number, ms): Delay before each rumble. Only used if
                ``count > 1``. Capped at 2500 ms.
        """


# hide from jedi
if TYPE_CHECKING:
    del MaybeAwaitable
    del MaybeAwaitableBytes
    del MaybeAwaitableTuple
    del Number
