"""Utility functions for the devices module."""
from __future__ import annotations

import threading
from dataclasses import dataclass
from math import ceil
from random import gauss
from typing import TypeVar

from controller import (
    GPS,
    LED,
    Accelerometer,
    Camera,
    Compass,
    Connector,
    DistanceSensor,
    Emitter,
    Gyro,
    InertialUnit,
    Lidar,
    LightSensor,
    Motor,
    PositionSensor,
    Radar,
    RangeFinder,
    Receiver,
    Robot,
    Speaker,
    TouchSensor,
    VacuumGripper,
)
from controller.device import Device

TDevice = TypeVar('TDevice', bound=Device)
__GLOBALS: 'GlobalData' | None = None


class WebotsDevice:
    """
    A collection of Webots device classes.

    Each class represents a different device that can be attached to the robot.
    """

    Accelerometer = Accelerometer
    Camera = Camera
    Compass = Compass
    Connector = Connector
    DistanceSensor = DistanceSensor
    Emitter = Emitter
    GPS = GPS
    Gyro = Gyro
    InertialUnit = InertialUnit
    LED = LED
    Lidar = Lidar
    LightSensor = LightSensor
    Motor = Motor
    PositionSensor = PositionSensor
    Radar = Radar
    RangeFinder = RangeFinder
    Receiver = Receiver
    Speaker = Speaker
    TouchSensor = TouchSensor
    VacuumGripper = VacuumGripper


@dataclass
class GlobalData:
    """
    Global data and functions for the simulator.

    When accessed through the get_globals function, a single instance of this
    class is created and stored in the module's global scope.

    :param robot: The robot object.
    :param timestep: The timestep size of the simulation.
    :param stop_event: The event to stop the simulation.
    """

    robot: Robot
    timestep: int
    stop_event: threading.Event | None = None

    def sleep(self, secs: float) -> None:
        """Sleeps for a given duration in simulator time."""
        if secs == 0:
            return
        elif secs < 0:
            raise ValueError("Sleep duration must be non-negative.")

        # Convert to a multiple of the timestep
        msecs = ceil((secs * 1000) / self.timestep) * self.timestep

        # Sleep for the given duration
        result = self.robot.step(msecs)

        # If the simulation has stopped, set the stop event
        if (result == -1) and (self.stop_event is not None):
            self.stop_event.set()


def get_globals() -> GlobalData:
    """Returns the global dictionary."""
    global __GLOBALS
    if __GLOBALS is None:
        # Robot constructor lacks a return type annotation in R2023b
        robot = Robot() if Robot.created is None else Robot.created  # type: ignore[no-untyped-call]

        __GLOBALS = GlobalData(
            robot=robot,
            timestep=int(robot.getBasicTimeStep()),
        )
    return __GLOBALS


def map_to_range(
    value: float,
    old_min_max: tuple[float, float],
    new_min_max: tuple[float, float],
) -> float:
    """Maps a value from within one range of inputs to within a range of outputs."""
    old_min, old_max = old_min_max
    new_min, new_max = new_min_max
    return ((value - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min


def get_robot_device(robot: Robot, name: str, kind: type[TDevice]) -> TDevice:
    """
    A helper function to get a device from the robot.

    Raises a TypeError if the device is not found or is not of the correct type.
    Weboots normally just returns None if the device is not found.

    :param robot: The robot object.
    :param name: The name of the device.
    :param kind: The type of the device.
    :return: The device object.
    :raises TypeError: If the device is not found or is not of the correct type.
    """
    device = robot.getDevice(name)
    if not isinstance(device, kind):
        raise TypeError(f"Failed to get device: {name}.")
    return device


def add_jitter(
    value: float,
    value_range: tuple[float, float],
    std_dev_percent: float = 2.0,
    offset_percent: float = 0.0,
) -> float:
    """Adds normally distributed jitter to a given value."""
    std_dev = value * (std_dev_percent / 100.0)
    mean_offset = value * (offset_percent / 100.0)

    error = gauss(mean_offset, std_dev)
    # Ensure the error is within the range
    return max(value_range[0], min(value_range[1], value + error))
