import smbus
import struct
from typing import Any, Optional


class Controller:

    I2C_ADDR = 0x57

    WHO_AM_I = 0x0F
    MAX_MOTOR_PERCENTAGE = 0x11
    MOTOR_SHUTDOWN_TIMEOUT = 0x28
    MOTOR_SPEED = 0x30
    ENCODER_TICKS = 0x32
    STATUS = 0x36

    def __init__(self, i2cbus=8):
        self.I2C_BUS = i2cbus
        self.i2c = smbus.SMBus(self.I2C_BUS)

    def _read(self, command, n, unpack_spec) -> tuple:
        return struct.unpack(
            unpack_spec,
            bytes(self.i2c.read_i2c_block_data(self.I2C_ADDR, command, n)),
        )

    def _write(self, command: int, data: list[int]):
        self.i2c.write_i2c_block_data(self.I2C_ADDR, command, data)

    def who_am_i(self) -> int:
        """Check that the motors controller board is present. This
        should return the same value as Controller.I2C_ADDR."""
        return self._read(self.WHO_AM_I, 1, "B")

    def set_max_percentage(self, percent: int):
        """Set the maximum percentage of power which will be used
        (between 1 and 100) for further speed instructions. This has
        no effect on an running command."""
        if percent <= 0 or percent > 100:
            raise ValueError
        self._write(self.MAX_MOTOR_PERCENTAGE, [percent])

    def get_max_percentage(self) -> int:
        """Get the maximum percentage of power which will be used
        (between 1 and 100)."""
        return self._read(self.MAX_MOTOR_PERCENTAGE, 1, "B")[0]

    def set_motor_speed(self, left: Optional[int], right: Optional[int]):
        """Set the motor speed between -100 and 100. None means not to
        change the motor value. Using None for both motors will put
        the controller board in standby mode and motors will stop."""

        def convert(v: Optional[int], arg: str):
            if v is None:
                return -128
            if not isinstance(v, int) or v < -100 or v > 100:
                raise ValueError(
                    f"{arg} must be an integer between -100 and 100 or None"
                )
            return v

        self._write(
            self.MOTOR_SPEED,
            list(
                struct.pack(
                    "bb", convert(left, "left"), convert(right, "right")
                )
            ),
        )

    def get_motor_speed(self) -> tuple[int, int]:
        """Get the left and right motor speed as a tuple."""
        return self._read(self.MOTOR_SPEED, 2, "bb")

    def set_left_motor_speed(self, speed: int):
        """Set the left motor speed between -127 and 127."""
        self.set_motor_speed(speed, None)

    def set_right_motor_speed(self, speed: int):
        """Set the right motor speed between -127 and 127."""
        self.set_motor_speed(None, speed)

    def standby(self):
        """Stop the motors by putting the controller board in standby
        mode."""
        self.set_motor_speed(None, None)

    def get_encoder_ticks(self) -> tuple[int, int]:
        """Retrieve the encoder ticks since the last time it was
        queried. The ticks must be retrieved before they overflow a 2
        byte signed integer (-32768..32767) or the result will make no
        sense. Return a pair with left and right data."""
        return self._read(self.ENCODER_TICKS, 4, "hh")

    def get_status(self) -> dict[str, Any]:
        """Return a dict with various status fields:
        - "moving": True if at least one motor is moving, False otherwise"""
        (status,) = self._read(self.STATUS, 1, "?")
        return {"moving": (status & 1) != 0}

    def set_motor_shutdown_timeout(self, duration: float):
        """Set the duration in seconds after which the motors will shut down if no valid
        command is received. The minimum is 0.1 seconds, the maximum is 10 seconds.
        """
        if duration < 0.1 or duration > 10.0:
            raise ValueError
        self._write(self.MOTOR_SHUTDOWN_TIMEOUT, [round(duration * 10)])

    def get_motor_shutdown_timeout(self) -> float:
        """Get the duration in seconds after which the motors will shut down if no valid
        command is received."""
        return self._read(self.MOTOR_SHUTDOWN_TIMEOUT, 1, "B")[0] / 10