from smc.visualization.visualizer import manipulatorVisualizer
from smc.logging.logger import Logger
from smc.multiprocessing.process_manager import ProcessManager
import abc
from argparse import Namespace
import pinocchio as pin
import numpy as np
from functools import partial


class AbstractRobotManager(abc.ABC):
    """
    RobotManager:
    ---------------
    - design goal: rely on pinocchio as much as possible while
                   concealing obvious bookkeeping
    - this class serves to:
        - store the non-interfacing information about the robot
          like maximum allowed velocity, pin.model etc
        - provide functions every robot has to have like getQ
    """

    def __init__(self, args: Namespace):
        self._model: pin.Model
        self._data: pin.Data
        self._visual_model: pin.pinocchio_pywrap_default.GeometryModel
        self._collision_model: pin.pinocchio_pywrap_default.GeometryModel
        self.args: Namespace = args
        if args.debug_prints:
            print("AbstractRobotManager init")
        ####################################################################
        #                    robot-related attributes                      #
        ####################################################################
        self.robot_name: str = args.robot  # const
        if self.args.ctrl_freq > 0:
            self._dt = 1 / self.args.ctrl_freq  # [s]
        else:
            # if we're going as fast as possible, we are in sim.
            # if we're in sim, we need to know for long to integrate,
            # and we set this to 1e-3 because there's absolutely no need to be more precise than that
            self._dt = 1e-3
        self._t: float = 0.0
        # NOTE: _MAX_ACCELERATION probably should be an array, but since UR robots only accept a float,
        #       we go for a float and pretend the every vector element is this 1 number
        # NOTE: this makes sense only for velocity-controlled robots.
        #       when torque control robot support will be introduce, we'll do something else here
        self._MAX_ACCELERATION: float | np.ndarray  # in joint space
        assert (
            self.args.acceleration <= self._MAX_ACCELERATION
            and self.args.acceleration > 0.0
        )
        self._acceleration = self.args.acceleration
        # _MAX_V is the robot's hardware joint velocity limit
        self._MAX_V: np.ndarray = self.model.velocityLimit
        # but you usually want to run at lower speeds for safety reasons
        assert self.args.max_v_percentage <= 1.0 and self.args.max_v_percentage > 0.0
        # so velocity commands are internally clipped to self._max_v
        self._max_v = np.clip(self._MAX_V, 0.0, args.max_v_percentage * self._MAX_V)
        # NOTE: make sure you've read pinocchio docs and understand why
        #       nq and nv are not necessarily the same number
        self._q = np.zeros(self.model.nq)
        self._v = np.zeros(self.model.nv)
        self._a = np.zeros(self.model.nv)

        # TODO: each robot should know which grippers are available
        # and set this there.
        self.gripper = None
        # initialize things that depend on q here
        self._step()

        ####################################################################
        #                    processes and utilities robotmanager owns     #
        ####################################################################
        # TODO: make a default
        self._log_manager: Logger
        if args.save_log:
            self._log_manager = Logger(args)

        # since there is only one robot and one visualizer, this is robotmanager's job
        # TODO: this should probably be transferred to a multiprocess manager,
        # or in whatever way be detangled from robots
        self.visualizer_manager: ProcessManager
        if self.args.visualizer:
            side_function = partial(
                manipulatorVisualizer,
                self.model,
                self.collision_model,
                self.visual_model,
            )
            # NOTE: it HAS TO be _q, we override q to support control of subsets of joints
            self.visualizer_manager = ProcessManager(
                args, side_function, {"q": self._q}, 0
            )
            # TODO: move this bs to visualizer, there are 0 reasons to keep it here
            if args.visualize_collision_approximation:
                # TODO: import the ellipses here, and write an update ellipse function
                # then also call that in controlloopmanager
                raise NotImplementedError(
                    "sorry, almost done - look at utils to get 90% of the solution!"
                )

    @property
    def model(self) -> pin.Model:
        return self._model

    @property
    def data(self) -> pin.Data:
        return self._data

    @property
    def visual_model(self) -> pin.pinocchio_pywrap_default.GeometryModel:
        return self._visual_model

    @property
    def collision_model(self) -> pin.pinocchio_pywrap_default.GeometryModel:
        return self._collision_model

    @abc.abstractmethod
    def setInitialPose(self) -> None:
        """
        setInitialPose
        --------------
        for example, if just integrating, do:
        self.q = pin.randomConfiguration(
            self.model, -1 * np.ones(self.model.nq), np.ones(self.model.nq)
        )
        NOTE: you probably want to specialize this for your robot
              to have it reasonable (no-self collisions, particular home etc)
        """
        pass

    @property
    def dt(self):
        return self._dt

    @property
    def q(self):
        return self._q.copy()

    @property
    def nq(self) -> int:
        return self.model.nq

    @property
    def v(self):
        return self._v.copy()

    @property
    def nv(self) -> int:
        return self.model.nv

    # _updateQ and _updateV could be put into getters and setters of q and v respectively.
    # however, because i don't trust the interfaces of a random robot to play ball,
    # i want to keep this separated.
    # the point of q and v as properties is that you don't have to worry about using q or v when you like,
    # and this is not the case if a shitty interface creates lag upon every call.
    # if these functions are separate then i can guarantee that they are called once per control cycle in step
    # and i can know i used the shitty interface the least amount of times required - once per cycle.
    @abc.abstractmethod
    def _updateQ(self):
        """
        update internal _q joint angles variable with current angles obtained
        from your robot's communication interface
        """
        pass

    @abc.abstractmethod
    def _updateV(self):
        """
        update internal _v joint velocities variable with current velocities obtained
        from your robot's communication interface
        """
        pass

    @abc.abstractmethod
    def forwardKinematics(self):
        """
        forwardKinematics
        -----------------
        compute all the frames you care about based on current configuration
        and put them into class attributes.
        for a single manipulator with just the end-effector frame,
        this is a pointless function, but there is more to do for dual
        arm robots for example.

        This should be implemented by an interface.
        For example, for the SingleArmInterface you would do:

        def forwardKinematics(self):
            pin.forwardKinematics(self.model, self.data, self._q)
            pin.updateFramePlacement(self.model, self.data, self._ee_frame_id)
            self.T_w_e = self.data.oMf[self._ee_frame_id]
        """
        pass

    @abc.abstractmethod
    def _step(self):
        """
        _step
        ----
        Purpose:
        - update everything that should be updated on a step-by-step basis
        Reason for existance:
        - the problem this is solving is that you're not calling
          forwardKinematics, an expensive call, more than once per step.
        - you also don't need to remember to do call forwardKinematics
        Usage:
        - don't call this method yourself! this is CommandLoopManager's job
        - use getters to get copies of the variables you want

        Look at some interfaces to see particular implementations.
        """

    def sendVelocityCommand(self, v) -> None:
        """
        sendVelocityCommand
        -------------------
        1) saturate the command to comply with hardware limits or smaller limits you've set
        2) send it via the particular robot's particular communication interface
        """
        assert type(v) == np.ndarray
        assert len(v) == self.model.nv
        v = np.clip(v, -1 * self._max_v, self._max_v)
        self.sendVelocityCommandToReal(v)

    @abc.abstractmethod
    def sendVelocityCommandToReal(self, v): ...

    @abc.abstractmethod
    def stopRobot(self):
        """
        stopRobot
        ---------
        implement whatever stops the robot as cleanly as possible on your robot.
        does not need to be emergency option, could be sending zero velocity
        commands, could be switching to freedrive/leadthrough/whatever.

        depending on the communication interface on your robot,
        it could happen that it stays in "externally guided motion" mode,
        i.e. it keeps listening to new velocity commands, while executing the last velocity command.
        such a scenario is problematic because they last control command might not be
        all zeros, meaning the robot will keep slowly moving and hit something after 5min
        (obviously this exact thing happened to the author...)
        """
        pass

    @abc.abstractmethod
    def setFreedrive(self):
        """
        setFreedrive
        ------------
        set the robot into the "teaching mode", "freedrive mode", "leadthrough mode"
        or whatever the term is for when you can manually (with your hands) move the robot
        around
        """
        pass

    @abc.abstractmethod
    def unSetFreedrive(self):
        """
        setFreedrive
        ------------
        unset the robot into the "teaching mode", "freedrive mode", "leadthrough mode"
        or whatever the term is for when you can manually (with your hands) move the robot
        around, back to "wait" or "accept commands" or whatever mode the robot has to
        receive external commands.
        """
        pass

    # TODO: make faux gripper class to avoid this bullshit here
    def openGripper(self):
        if self.gripper is None:
            if self.args.debug_prints:
                print(
                    "you didn't select a gripper (no gripper is the default parameter) so no gripping for you"
                )
            return
        if (not self.args.simulation) and (not self.args.pinocchio_only):
            self.gripper.open()
        else:
            print("not implemented yet, so nothing is going to happen!")

    # TODO: make faux gripper class to avoid this bullshit here
    def closeGripper(self):
        if self.gripper is None:
            if self.args.debug_prints:
                print(
                    "you didn't select a gripper (no gripper is the default parameter) so no gripping for you"
                )
            return
        if (not self.args.simulation) and (not self.args.pinocchio_only):
            self.gripper.close()
        else:
            print("not implemented yet, so nothing is going to happen!")

    ########################################################################################
    # visualizer management. ideally transferred elsewhere
    ###################################################################################

    def killManipulatorVisualizer(self):
        """
        killManipulatorVisualizer
        ---------------------------
        if you're using the manipulator visualizer, you want to start it only once.
        because you start the meshcat server, initialize the manipulator and then
        do any subsequent changes with that server. there's no point in restarting.
        but this means you have to kill it manually, because the ControlLoopManager
        can't nor should know whether this is the last control loop you're running -
        RobotManager has to handle the meshcat server.
        and in this case the user needs to say when the tasks are done.
        """
        self.visualizer_manager.terminateProcess()

    def updateViz(self, viz_dict: dict):
        """
        updateViz
        ---------
        updates the viz and only the viz according to arguments
        NOTE: this function does not change internal variables!
        because it shouldn't - it should only update the visualizer
        """
        if self.args.visualize_manipulator:
            self.visualizer_manager.sendCommand(viz_dict)
        else:
            if self.args.debug_prints:
                print("you didn't select viz")

    def sendRectangular2DMapToVisualizer(self, map_as_list):
        for obstacle in map_as_list:
            length = obstacle[1][0] - obstacle[0][0]
            width = obstacle[3][1] - obstacle[0][1]
            height = 0.4  # doesn't matter because plan because planning is 2D
            pose = pin.SE3(
                np.eye(3),
                np.array(
                    [
                        obstacle[0][0] + (obstacle[1][0] - obstacle[0][0]) / 2,
                        obstacle[0][1] + (obstacle[3][1] - obstacle[0][1]) / 2,
                        0.0,
                    ]
                ),
            )
            dims = [length, width, height]
            command = {"obstacle_box": [pose, dims]}
            self.visualizer_manager.sendCommand(command)


class AbstractRealRobotManager(AbstractRobotManager, abc.ABC):
    """
    RobotManagerRealAbstract
    ------------------------
    enumerate the templates that the interface for your real robot
    has to have, connectToRobot
    """

    def __init__(self, args):
        super().__init__(args)
        if args.debug_prints:
            print("AbstractRealRobotManager init")
        self.connectToRobot()
        self.connectToGripper()
        self.setInitialPose()

    @abc.abstractmethod
    def connectToRobot(self):
        """
        connectToRobot
        --------------
        create whatever is necessary to:
        1) send commands to the robot
        2) receive state information
        3) set options if applicable
        Setting up a gripper, force-torque sensor and similar is handled separately
        """
        pass

    @abc.abstractmethod
    def connectToGripper(self):
        """
        connectToGripper
        --------------
        create a gripper class based on selected argument.
        this needs to be made for each robot individually because even if the gripper
        is the same, the robot is likely to have its own way of interfacing it
        """
        pass