"""A collection of wrappers for the devices that can be connected to the Arduino."""
from abc import ABC, abstractmethod
from enum import Enum

from sbot_interface.devices.led import Led as _Led
from sbot_interface.devices.util import WebotsDevice, get_globals, get_robot_device

ANALOG_MAX = 1023


class GPIOPinMode(str, Enum):
    """The possible modes for a GPIO pin."""

    INPUT = 'INPUT'
    INPUT_PULLUP = 'INPUT_PULLUP'
    OUTPUT = 'OUTPUT'


class BasePin(ABC):
    """The base class for all the devices that can be connected to the Arduino."""

    @abstractmethod
    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        pass

    @abstractmethod
    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        pass

    @abstractmethod
    def get_digital(self) -> bool:
        """Get the digital input value of the pin."""
        pass

    @abstractmethod
    def set_digital(self, value: bool) -> None:
        """Set the digital output value of the pin."""
        pass

    @abstractmethod
    def get_analog(self) -> int:
        """Get the analog input value of the pin."""
        pass


class EmptyPin(BasePin):
    """A pin that does nothing. Used for pins that are not connected."""

    def __init__(self) -> None:
        self._mode = GPIOPinMode.INPUT
        self._digital = False
        self._analog = 0

    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        return self._mode

    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        self._mode = mode

    def get_digital(self) -> bool:
        """Get the digital input value of the pin."""
        return self._digital

    def set_digital(self, value: bool) -> None:
        """Set the digital output value of the pin."""
        self._digital = value

    def get_analog(self) -> int:
        """Get the analog input value of the pin."""
        return self._analog


class UltrasonicSensor(BasePin):
    """
    A sensor that can measure the distance to an object.

    This is attached to the pin specified to be the echo pin, with the trigger pin unused.
    """

    def __init__(self, device_name: str) -> None:
        g = get_globals()
        self._device = get_robot_device(g.robot, device_name, WebotsDevice.DistanceSensor)
        self._device.enable(g.timestep)
        self._mode = GPIOPinMode.INPUT

    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        return self._mode

    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        self._mode = mode

    def get_digital(self) -> bool:
        """Get the digital input value of the pin. This is always False."""
        return False

    def set_digital(self, value: bool) -> None:
        """
        Set the digital output value of the pin.

        This has no effect here.
        """
        pass

    def get_analog(self) -> int:
        """Get the analog input value of the pin. This is always 0."""
        return 0

    def get_distance(self) -> int:
        """
        Get the distance measured by the sensor in mm.

        Relies on the lookup table mapping to the distance in mm.
        """
        return int(self._device.getValue())


class MicroSwitch(BasePin):
    """A simple switch that can be pressed or released."""

    def __init__(self, device_name: str) -> None:
        g = get_globals()
        self._device = get_robot_device(g.robot, device_name, WebotsDevice.TouchSensor)
        self._device.enable(g.timestep)
        self._mode = GPIOPinMode.INPUT

    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        return self._mode

    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        self._mode = mode

    def get_digital(self) -> bool:
        """Get the digital input value of the pin."""
        return bool(self._device.getValue())

    def set_digital(self, value: bool) -> None:
        """
        Set the digital output value of the pin.

        This has no effect here.
        """
        pass

    def get_analog(self) -> int:
        """Get the analog input value of the pin, either 0 or 1023."""
        return 1023 if self.get_digital() else 0


class PressureSensor(BasePin):
    """
    A sensor that can measure the force applied to it.

    This is attached to the pin specified, with the force proportional to the analog value.
    """

    # Use lookupTable [0 0 0, 50 1023 0] // 50 Newton max force
    def __init__(self, device_name: str) -> None:
        g = get_globals()
        self._device = get_robot_device(g.robot, device_name, WebotsDevice.TouchSensor)
        self._device.enable(g.timestep)
        self._mode = GPIOPinMode.INPUT

    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        return self._mode

    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        self._mode = mode

    def get_digital(self) -> bool:
        """
        Get the digital input value of the pin.

        True when the force is above half the maximum value.
        """
        return self.get_analog() > ANALOG_MAX / 2

    def set_digital(self, value: bool) -> None:
        """
        Set the digital output value of the pin.

        This has no effect here.
        """
        pass

    def get_analog(self) -> int:
        """Get the analog input value of the pin. This is proportional to the force applied."""
        return int(self._device.getValue())


class ReflectanceSensor(BasePin):
    """
    A simple sensor that can detect the reflectance of a surface.

    Used for line following, with a higher value indicating a lighter surface.
    """

    def __init__(self, device_name: str) -> None:
        g = get_globals()
        self._device = get_robot_device(g.robot, device_name, WebotsDevice.DistanceSensor)
        self._device.enable(g.timestep)
        self._mode = GPIOPinMode.INPUT

    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        return self._mode

    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        self._mode = mode

    def get_digital(self) -> bool:
        """
        Get the digital input value of the pin.

        True when the reflectance is above half the maximum value.
        """
        return self.get_analog() > ANALOG_MAX / 2

    def set_digital(self, value: bool) -> None:
        """
        Set the digital output value of the pin.

        This has no effect here.
        """
        pass

    def get_analog(self) -> int:
        """
        Get the analog input value of the pin.

        This is proportional to the reflectance of the surface.
        """
        return int(self._device.getValue())


class Led(BasePin):
    """A simple LED that can be turned on or off."""

    def __init__(self, device_name: str) -> None:
        self._led = _Led(device_name)
        self._mode = GPIOPinMode.OUTPUT

    def get_mode(self) -> GPIOPinMode:
        """Get the current mode of the pin."""
        return self._mode

    def set_mode(self, mode: GPIOPinMode) -> None:
        """Set the mode of the pin."""
        self._mode = mode

    def get_digital(self) -> bool:
        """
        Get the digital input value of the pin.

        True when the LED is on.
        """
        return self._led.get_colour() > 0

    def set_digital(self, value: bool) -> None:
        """Set the digital output value of the pin. This turns the LED on or off."""
        self._led.set_colour(1 if value else 0)

    def get_analog(self) -> int:
        """Get the analog input value of the pin. This is always 0."""
        return 0
