"""HEROS driver for newport picomotors."""

from enum import IntEnum

from herosdevices.core import DeviceCommandQuantity
from herosdevices.core.templates import SerialDeviceTemplate
from herosdevices.helper import mark_driver


class MotorType(IntEnum):
    """Motor types supported by Picomotor controller 8742."""

    UNCONNECTED = 0
    UNKNOWN = 1
    TINY = 2
    STANDARD = 3


@mark_driver(
    state="alpha",
    info="4 channel controller for picomotors",
    product_page="https://www.newport.com/p/8742"
)
class PicomotorController8742(SerialDeviceTemplate):
    """HEROS driver for 4-channel picomotor controller 8742.

    Args:
        host: IP address of the controller, e.g. "192.168.1.42".
        port: listening port, typically 23.
    """

    MIN_AXIS = 1
    MAX_AXIS = 4

    identifier: str = DeviceCommandQuantity(command_get="*IDN?", dtype=str)

    def __init__(self, host: str, port: int = 23, timeout: int = 5000) -> None:
        SerialDeviceTemplate.__init__(
            self,
            f"socket://{host}:{port}",
            baudrate=None,
            timeout=timeout,
            read_line_termination=b"\r",
            write_line_termination=b"\r",
        )

    def send_command(self, command: str, axis: int | None = None, argument: int | str | None = None) -> None:
        """Send a command to the controller.

        Args:
            command: the command string.
            axis: the axis the command is to be executed on. Not required for global commands.
            argument: arguments to the command if required.
        """
        _cmd = f"{axis if axis is not None else ''}{command}{argument if argument is not None else ''}"
        self.connection.write(_cmd)

    def query_for_int(self, query: str, axis: int | None = None) -> int | None:
        """Query an integer from the controller.

        Args:
            query: the query command.
            axis: the axis the command is to be executed on. Not required for global queries.
        """
        self.connection.write(f"{axis if axis is not None else ''}{query}")
        resp = self.connection.read_line()
        return int(resp) if resp is not None else None

    def query_for_string(self, query: str, axis: int | None = None) -> str:
        """Query a string from the controller.

        Args:
            query: the query command.
            axis: the axis the command is to be executed on. Not required for global queries.
        """
        self.send_command(query, axis)
        resp = self.device.readline()
        return resp.decode("utf-8").strip()

    def query_error(self) -> tuple[int, str]:
        """Query the latest error from the controller.

        Upon reading, errors are cleared from the controller's buffer and thus can only be read once.

        Returns:
            tuple (error code, error message). The first digit of the error code indicates the axis the error has
            occured on.

        """
        self.send_command("TB?")
        resp = self.connection.readline()
        err_code, err_msg = resp.strip().split(",")
        return int(err_code), err_msg.strip()

    def reset(self) -> None:
        """Soft reset the controller.

        Note:
            Connection will be interrupted.
        """
        self.connection.write("RS")

    def abort_motion(self) -> None:
        """Stop any motion immediately without deceleration."""
        self.connection.write("AB")


@mark_driver(
    state="alpha",
    info="picomotor linear actuator",
    product_page="https://www.newport.com/p/8301NF"
)
class Picomotor:
    """HEROS driver for a single Picomotor.

    Args:
        controller: RemoteHERO of the controller the motor is attached to.
        axis: port of the controller. Available axes are 1,2,3,4.
    """

    MIN_VELOCITY = 1
    MAX_VELOCITY = 2_000
    MIN_ACCELERATION = 1
    MAX_ACCELERATION = 200_000
    MIN_STEP_COUNT = -2_147_483_648
    MAX_STEP_COUNT = 2_147_483_647

    def __init__(self, controller: PicomotorController8742, axis: int) -> None:
        self.controller = controller
        if not PicomotorController8742.MIN_AXIS <= axis <= PicomotorController8742.MAX_AXIS:
            raise RuntimeError("Invalid axis: " + str(axis))
        self.axis = axis

    @property
    def velocity(self) -> int:
        """Query the velocity.

        Returns:
            velocity in steps/second.
        """
        return self.controller.query_for_int("VA?", self.axis)

    @velocity.setter
    def velocity(self, velocity: int) -> None:
        """Set the velocity.

        Args:
            velocity: velocity in steps/second. Must be within [1, 2_000].
        """
        if not self.MIN_VELOCITY <= velocity <= self.MAX_VELOCITY:
            raise RuntimeError("Invalid velocity: " + str(velocity))
        self.controller.send_command("VA", self.axis, velocity)

    @property
    def acceleration(self) -> int:
        """Query the acceleration.

        Returns:
            acceleration in steps/second².
        """
        return self.controller.query_for_int("AC?", self.axis)

    @acceleration.setter
    def acceleration(self, acceleration: int) -> None:
        """Set the acceleration.

        Args:
            acceleration: acceleration in steps/second². Must be within [1, 200_000].
        """
        if not self.MIN_ACCELERATION <= acceleration <= self.MAX_ACCELERATION:
            raise RuntimeError("Invelid acceleration: " + str(acceleration))
        self.controller.send_command("AC", self.axis, acceleration)

    @property
    def home_position(self) -> int:
        """Query the home position.

        All absolute moves are relative to "home".

        Returns:
            step count considered home.
        """
        return self.controller.query_for_int("DH?", self.axis)

    @home_position.setter
    def home_position(self, home_position: int | None) -> None:
        """Set the home position.

        Set the step count considered "home". When `home_position` is not given, home is set to 0. All absolute moves
        are relative to "home".

        Args:
            home_position: step count considered home. Must be within [-2_147_483_648, 2_147_483_647].
        """
        if not self.MIN_STEP_COUNT <= home_position <= self.MAX_STEP_COUNT:
            raise RuntimeError("Invalid home_position: " + str(home_position))
        self.controller.send_command("DH", self.axis, home_position)

    @property
    def motion_done(self) -> bool:
        """Check if a move is in progress.

        Returns:
            True when no movement is in progrogress.
        """
        movement_done = self.controller.query_for_int("MD?", self.axis)
        return movement_done > 0

    def move_indefinite(self, direction: int) -> None:
        """Start a movement.

        The motor will move indefinitely until a :meth:`stop_motion` command is issued.

        Args:
            direction: positive value for increasing step count, negative for decreasing.
        """
        self.controller.send_command("MV", self.axis, "-" if direction < 0 else "+")

    def move_absolute(self, target_position: int) -> None:
        """Move to the desired step count.

        Args:
            target_position: absolute position relative to "home" (see :meth:`set_home_position`). Must be within
                [-2_147_483_648, 2_147_483_647]
        """
        if not self.MIN_STEP_COUNT <= target_position <= self.MAX_STEP_COUNT:
            raise RuntimeError("Invalid target_position: " + str(target_position))
        self.controller.send_command("PA", self.axis, target_position)

    def move_relative(self, steps: int) -> None:
        """Move relative by the desired step count.

        Args:
            steps: amount of steps to move by. Must be within [-2_147_483_648, 2_147_483_647].
        """
        if not self.MIN_STEP_COUNT <= steps <= self.MAX_STEP_COUNT:
            raise RuntimeError("Invalid steps: " + str(steps))
        self.controller.send_command("PR", self.axis, steps)

    @property
    def target_position(self) -> int:
        """Query the target position of the current move.

        Returns:
            target step count of the current move.
        """
        return self.controller.query_for_int("PR?", self.axis)

    @property
    def actual_position(self) -> int:
        """Query the position of the motor.

        Returns:
            current step count of the motor.
        """
        return self.controller.query_for_int("PA?", self.axis)

    def stop_motion(self) -> None:
        """Stop the movement."""
        self.controller.send_command("ST", self.axis)

    @property
    def motor_type(self) -> MotorType:
        """Query the type of Motor.

        Returns:
            motor type as specified in :class:`MotorType`
        """
        motor_type = self.controller.query_for_int("QM?", self.axis)
        return MotorType(motor_type)

    @motor_type.setter
    def motor_type(self, motor_type: MotorType) -> None:
        """Set the type of of motor.

        This loads the default values for acceleration and velocity.

        Args:
            motor_type: See :class:`MotorType`.
        """
        self.controller.send_command("QM", self.axis, motor_type.value)
