#
# Copyright (c) FIRST and other WPILib contributors.
# Open Source Software; you can modify and/or share it under the terms of
# the WPILib BSD license file in the root directory of this project.
#

import wpilib
import commands2
import constants
import examplesmartmotorcontroller
import wpimath


class DriveSubsystem(commands2.Subsystem):
    def __init__(self) -> None:
        """Creates a new DriveSubsystem"""
        super().__init__()

        # The motors on the left side of the drive.
        self.leftLeader = examplesmartmotorcontroller.ExampleSmartMotorController(
            constants.DriveConstants.kLeftMotor1Port
        )

        self.leftFollower = examplesmartmotorcontroller.ExampleSmartMotorController(
            constants.DriveConstants.kLeftMotor2Port
        )

        # The motors on the right side of the drive.
        self.rightLeader = examplesmartmotorcontroller.ExampleSmartMotorController(
            constants.DriveConstants.kRightMotor1Port
        )

        self.rightFollower = examplesmartmotorcontroller.ExampleSmartMotorController(
            constants.DriveConstants.kRightMotor1Port
        )

        # We need to invert one side of the drivetrain so that positive voltages
        # result in both sides moving forward. Depending on how your robot's
        # gearbox is constructed, you might have to invert the left side instead.
        self.rightLeader.setInverted(True)

        # You might need to not do this depending on the specific motor controller
        # that you are using -- contact the respective vendor's documentation for
        # more details.
        self.rightFollower.setInverted(True)

        self.leftFollower.follow(self.leftLeader)
        self.rightFollower.follow(self.rightLeader)

        self.leftLeader.setPID(constants.DriveConstants.kp, 0, 0)
        self.rightLeader.setPID(constants.DriveConstants.kp, 0, 0)

        # The feedforward controller (note that these are example values only - DO NOT USE THESE FOR YOUR OWN ROBOT!)
        # check DriveConstants for more information.
        self.feedforward = wpimath.SimpleMotorFeedforwardMeters(
            constants.DriveConstants.ks,
            constants.DriveConstants.kv,
            constants.DriveConstants.ka,
        )

        # The robot's drive
        self.drive = wpilib.DifferentialDrive(self.leftLeader.set, self.rightLeader.set)

        self.profile = wpimath.TrapezoidProfile(
            wpimath.TrapezoidProfile.Constraints(
                constants.DriveConstants.kMaxVelocity,
                constants.DriveConstants.kMaxAcceleration,
            )
        )
        self.timer = wpilib.Timer()
        self._initialLeftDistance = 0.0
        self._initialRightDistance = 0.0

    def arcadeDrive(self, fwd: float, rot: float):
        """
        Drives the robot using arcade controls.

        :param fwd: the commanded forward movement
        :param rot: the commanded rotation
        """
        self.drive.arcadeDrive(fwd, rot)

    def setDriveStates(
        self,
        currentLeft: wpimath.TrapezoidProfile.State,
        currentRight: wpimath.TrapezoidProfile.State,
        nextLeft: wpimath.TrapezoidProfile.State,
        nextRight: wpimath.TrapezoidProfile.State,
    ):
        """
        Attempts to follow the given drive states using offboard PID.

        :param currentLeft: The current left wheel state.
        :param currentRight: The current right wheel state.
        :param nextLeft: The next left wheel state.
        :param nextRight: The next right wheel state.
        """
        battery_voltage = wpilib.RobotController.getBatteryVoltage()
        left_feedforward = self.feedforward.calculate(
            currentLeft.velocity,
            (nextLeft.velocity - currentLeft.velocity) / constants.DriveConstants.kDt,
        )
        right_feedforward = self.feedforward.calculate(
            currentRight.velocity,
            (nextRight.velocity - currentRight.velocity) / constants.DriveConstants.kDt,
        )
        self.leftLeader.setSetPoint(
            examplesmartmotorcontroller.ExampleSmartMotorController.PIDMode.kPosition,
            currentLeft.position,
            left_feedforward / battery_voltage,
        )

        self.rightLeader.setSetPoint(
            examplesmartmotorcontroller.ExampleSmartMotorController.PIDMode.kPosition,
            currentRight.position,
            right_feedforward / battery_voltage,
        )

    def getLeftEncoderDistance(self) -> float:
        """
        Returns the left drive encoder distance.

        :returns: the left drive encoder distance
        """
        return self.leftLeader.getEncoderDistance()

    def getRightEncoderDistance(self) -> float:
        """
        Returns the right drive encoder distance.

        :returns: the right drive encoder distance
        """
        return self.rightLeader.getEncoderDistance()

    def resetEncoders(self):
        """Resets the drive encoders"""
        self.leftLeader.resetEncoder()
        self.rightLeader.resetEncoder()

    def setMaxOutput(self, maxOutput: float):
        """
        Sets the max output of the drive. Useful for scaling the drive to drive more slowly.

        :param maxOutput: the maximum output to which the drive will be constrained
        """
        self.drive.setMaxOutput(maxOutput)

    def profiledDriveDistance(self, distance: float) -> commands2.Command:
        def on_init():
            self.timer.restart()
            self.resetEncoders()

        def on_execute():
            current_time = self.timer.get()
            current_setpoint = self.profile.calculate(
                current_time,
                wpimath.TrapezoidProfile.State(),
                wpimath.TrapezoidProfile.State(distance, 0),
            )
            next_setpoint = self.profile.calculate(
                current_time + constants.DriveConstants.kDt,
                wpimath.TrapezoidProfile.State(),
                wpimath.TrapezoidProfile.State(distance, 0),
            )
            self.setDriveStates(
                current_setpoint, current_setpoint, next_setpoint, next_setpoint
            )

        def on_end(interrupted: bool):
            self.leftLeader.set(0)
            self.rightLeader.set(0)

        def is_finished() -> bool:
            return self.profile.isFinished(0)

        return commands2.FunctionalCommand(
            on_init, on_execute, on_end, is_finished, self
        )

    def dynamicProfiledDriveDistance(self, distance: float) -> commands2.Command:
        def on_init():
            self.timer.restart()
            self._initialLeftDistance = self.getLeftEncoderDistance()
            self._initialRightDistance = self.getRightEncoderDistance()

        def on_execute():
            current_time = self.timer.get()
            current_left = self.profile.calculate(
                current_time,
                wpimath.TrapezoidProfile.State(self._initialLeftDistance, 0),
                wpimath.TrapezoidProfile.State(self._initialLeftDistance + distance, 0),
            )
            current_right = self.profile.calculate(
                current_time,
                wpimath.TrapezoidProfile.State(self._initialRightDistance, 0),
                wpimath.TrapezoidProfile.State(
                    self._initialRightDistance + distance, 0
                ),
            )
            next_left = self.profile.calculate(
                current_time + constants.DriveConstants.kDt,
                wpimath.TrapezoidProfile.State(self._initialLeftDistance, 0),
                wpimath.TrapezoidProfile.State(self._initialLeftDistance + distance, 0),
            )
            next_right = self.profile.calculate(
                current_time + constants.DriveConstants.kDt,
                wpimath.TrapezoidProfile.State(self._initialRightDistance, 0),
                wpimath.TrapezoidProfile.State(
                    self._initialRightDistance + distance, 0
                ),
            )
            self.setDriveStates(current_left, current_right, next_left, next_right)

        def on_end(interrupted: bool):
            self.leftLeader.set(0)
            self.rightLeader.set(0)

        def is_finished() -> bool:
            return self.profile.isFinished(0)

        return commands2.FunctionalCommand(
            on_init, on_execute, on_end, is_finished, self
        )
