#
# 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.
#

from typing import Callable

from commands2 import Command, Subsystem
import wpilib
import wpimath

from constants import DriveConstants


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

        # The motors on the left side of the drive.
        self.leftLeader = wpilib.PWMSparkMax(DriveConstants.kLeftMotor1Port)
        self.leftFollower = wpilib.PWMSparkMax(DriveConstants.kLeftMotor2Port)

        # The motors on the right side of the drive.
        self.rightLeader = wpilib.PWMSparkMax(DriveConstants.kRightMotor1Port)
        self.rightFollower = wpilib.PWMSparkMax(DriveConstants.kRightMotor2Port)

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

        # The left-side drive encoder
        self.leftEncoder = wpilib.Encoder(
            DriveConstants.kLeftEncoderPorts[0],
            DriveConstants.kLeftEncoderPorts[1],
            DriveConstants.kLeftEncoderReversed,
        )

        # The right-side drive encoder
        self.rightEncoder = wpilib.Encoder(
            DriveConstants.kRightEncoderPorts[0],
            DriveConstants.kRightEncoderPorts[1],
            DriveConstants.kRightEncoderReversed,
        )

        self.imu = wpilib.OnboardIMU(wpilib.OnboardIMU.MountOrientation.FLAT)
        self.controller = wpimath.ProfiledPIDController(
            DriveConstants.kTurnP,
            DriveConstants.kTurnI,
            DriveConstants.kTurnD,
            wpimath.TrapezoidProfile.Constraints(
                DriveConstants.kMaxTurnRateDegPerS,
                DriveConstants.kMaxTurnAccelerationDegPerSSquared,
            ),
        )
        self.feedforward = wpimath.SimpleMotorFeedforwardMeters(
            DriveConstants.kS, DriveConstants.kV, DriveConstants.kA
        )

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

        # 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)

        # Sets the distance per pulse for the encoders
        self.leftEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse)
        self.rightEncoder.setDistancePerPulse(DriveConstants.kEncoderDistancePerPulse)

        # Set the controller to be continuous (because it is an angle controller)
        self.controller.enableContinuousInput(-180, 180)
        # Set the controller tolerance - the delta tolerance ensures the robot is stationary at the
        # setpoint before it is considered as having reached the reference
        self.controller.setTolerance(
            DriveConstants.kTurnToleranceDeg, DriveConstants.kTurnRateToleranceDegPerS
        )

    def arcadeDriveCommand(
        self, fwd: Callable[[], float], rot: Callable[[], float]
    ) -> Command:
        """Returns a command that drives the robot with arcade controls.

        :param fwd: the commanded forward movement
        :param rot: the commanded rotation
        """
        # A split-stick arcade command, with forward/backward controlled by the left
        # hand, and turning controlled by the right.
        return self.run(lambda: self.drive.arcadeDrive(fwd(), rot())).withName(
            "arcadeDrive"
        )

    def driveDistanceCommand(self, distance: float, velocity: float) -> Command:
        """Returns a command that drives the robot forward a specified distance at a specified
        velocity.

        :param distance: The distance to drive forward in meters
        :param velocity: The fraction of max velocity at which to drive
        """
        return (
            self.runOnce(
                lambda: (
                    self.leftEncoder.reset(),
                    self.rightEncoder.reset(),
                )
            )
            .andThen(self.run(lambda: self.drive.arcadeDrive(velocity, 0)))
            .until(
                lambda: max(
                    self.leftEncoder.getDistance(), self.rightEncoder.getDistance()
                )
                >= distance
            )
            .finallyDo(lambda interrupted: self.drive.stopMotor())
        )

    def turnToAngleCommand(self, angleDeg: float) -> Command:
        """Returns a command that turns to robot to the specified angle using a motion profile and
        PID controller.

        :param angleDeg: The angle to turn to
        """

        def _reset() -> None:
            self.controller.reset(self.imu.getRotation2d().degrees())

        def _run() -> None:
            rotation_output = self.controller.calculate(
                self.imu.getRotation2d().degrees(), angleDeg
            )
            feedforward = self.feedforward.calculate(
                self.controller.getSetpoint().velocity
            )
            self.drive.arcadeDrive(
                0,
                rotation_output
                + feedforward / wpilib.RobotController.getBatteryVoltage(),
            )

        return (
            self.startRun(_reset, _run)
            .until(self.controller.atGoal)
            .finallyDo(lambda interrupted: self.drive.arcadeDrive(0, 0))
        )
