/*----------------------------------------------------------------------------*/
/* Copyright (c) 2008-2019 FIRST. All Rights Reserved.                        */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/

#include "frc/RobotDrive.h"

#include <algorithm>
#include <cmath>

#include <hal/FRCUsageReporting.h>

#include "frc/GenericHID.h"
#include "frc/Joystick.h"
#include "frc/Talon.h"
#include "frc/Utility.h"
#include "frc/WPIErrors.h"

using namespace frc;

static std::shared_ptr<SpeedController> make_shared_nodelete(
    SpeedController* ptr) {
  return std::shared_ptr<SpeedController>(ptr, NullDeleter<SpeedController>());
}

RobotDrive::RobotDrive(int leftMotorChannel, int rightMotorChannel) {
  InitRobotDrive();
  m_rearLeftMotor = std::make_shared<Talon>(leftMotorChannel);
  m_rearRightMotor = std::make_shared<Talon>(rightMotorChannel);
  SetLeftRightMotorOutputs(0.0, 0.0);
}

RobotDrive::RobotDrive(int frontLeftMotor, int rearLeftMotor,
                       int frontRightMotor, int rearRightMotor) {
  InitRobotDrive();
  m_rearLeftMotor = std::make_shared<Talon>(rearLeftMotor);
  m_rearRightMotor = std::make_shared<Talon>(rearRightMotor);
  m_frontLeftMotor = std::make_shared<Talon>(frontLeftMotor);
  m_frontRightMotor = std::make_shared<Talon>(frontRightMotor);
  SetLeftRightMotorOutputs(0.0, 0.0);
}

RobotDrive::RobotDrive(SpeedController* leftMotor,
                       SpeedController* rightMotor) {
  InitRobotDrive();
  if (leftMotor == nullptr || rightMotor == nullptr) {
    wpi_setWPIError(NullParameter);
    m_rearLeftMotor = m_rearRightMotor = nullptr;
    return;
  }
  m_rearLeftMotor = make_shared_nodelete(leftMotor);
  m_rearRightMotor = make_shared_nodelete(rightMotor);
}

RobotDrive::RobotDrive(SpeedController& leftMotor,
                       SpeedController& rightMotor) {
  InitRobotDrive();
  m_rearLeftMotor = make_shared_nodelete(&leftMotor);
  m_rearRightMotor = make_shared_nodelete(&rightMotor);
}

RobotDrive::RobotDrive(std::shared_ptr<SpeedController> leftMotor,
                       std::shared_ptr<SpeedController> rightMotor) {
  InitRobotDrive();
  if (leftMotor == nullptr || rightMotor == nullptr) {
    wpi_setWPIError(NullParameter);
    m_rearLeftMotor = m_rearRightMotor = nullptr;
    return;
  }
  m_rearLeftMotor = leftMotor;
  m_rearRightMotor = rightMotor;
}

RobotDrive::RobotDrive(SpeedController* frontLeftMotor,
                       SpeedController* rearLeftMotor,
                       SpeedController* frontRightMotor,
                       SpeedController* rearRightMotor) {
  InitRobotDrive();
  if (frontLeftMotor == nullptr || rearLeftMotor == nullptr ||
      frontRightMotor == nullptr || rearRightMotor == nullptr) {
    wpi_setWPIError(NullParameter);
    return;
  }
  m_frontLeftMotor = make_shared_nodelete(frontLeftMotor);
  m_rearLeftMotor = make_shared_nodelete(rearLeftMotor);
  m_frontRightMotor = make_shared_nodelete(frontRightMotor);
  m_rearRightMotor = make_shared_nodelete(rearRightMotor);
}

RobotDrive::RobotDrive(SpeedController& frontLeftMotor,
                       SpeedController& rearLeftMotor,
                       SpeedController& frontRightMotor,
                       SpeedController& rearRightMotor) {
  InitRobotDrive();
  m_frontLeftMotor = make_shared_nodelete(&frontLeftMotor);
  m_rearLeftMotor = make_shared_nodelete(&rearLeftMotor);
  m_frontRightMotor = make_shared_nodelete(&frontRightMotor);
  m_rearRightMotor = make_shared_nodelete(&rearRightMotor);
}

RobotDrive::RobotDrive(std::shared_ptr<SpeedController> frontLeftMotor,
                       std::shared_ptr<SpeedController> rearLeftMotor,
                       std::shared_ptr<SpeedController> frontRightMotor,
                       std::shared_ptr<SpeedController> rearRightMotor) {
  InitRobotDrive();
  if (frontLeftMotor == nullptr || rearLeftMotor == nullptr ||
      frontRightMotor == nullptr || rearRightMotor == nullptr) {
    wpi_setWPIError(NullParameter);
    return;
  }
  m_frontLeftMotor = frontLeftMotor;
  m_rearLeftMotor = rearLeftMotor;
  m_frontRightMotor = frontRightMotor;
  m_rearRightMotor = rearRightMotor;
}

void RobotDrive::Drive(double outputMagnitude, double curve) {
  double leftOutput, rightOutput;
  static bool reported = false;
  if (!reported) {
    HAL_Report(HALUsageReporting::kResourceType_RobotDrive,
               HALUsageReporting::kRobotDrive_ArcadeRatioCurve, GetNumMotors());
    reported = true;
  }

  if (curve < 0) {
    double value = std::log(-curve);
    double ratio = (value - m_sensitivity) / (value + m_sensitivity);
    if (ratio == 0) ratio = 0.0000000001;
    leftOutput = outputMagnitude / ratio;
    rightOutput = outputMagnitude;
  } else if (curve > 0) {
    double value = std::log(curve);
    double ratio = (value - m_sensitivity) / (value + m_sensitivity);
    if (ratio == 0) ratio = 0.0000000001;
    leftOutput = outputMagnitude;
    rightOutput = outputMagnitude / ratio;
  } else {
    leftOutput = outputMagnitude;
    rightOutput = outputMagnitude;
  }
  SetLeftRightMotorOutputs(leftOutput, rightOutput);
}

void RobotDrive::TankDrive(GenericHID* leftStick, GenericHID* rightStick,
                           bool squaredInputs) {
  if (leftStick == nullptr || rightStick == nullptr) {
    wpi_setWPIError(NullParameter);
    return;
  }
  TankDrive(leftStick->GetY(), rightStick->GetY(), squaredInputs);
}

void RobotDrive::TankDrive(GenericHID& leftStick, GenericHID& rightStick,
                           bool squaredInputs) {
  TankDrive(leftStick.GetY(), rightStick.GetY(), squaredInputs);
}

void RobotDrive::TankDrive(GenericHID* leftStick, int leftAxis,
                           GenericHID* rightStick, int rightAxis,
                           bool squaredInputs) {
  if (leftStick == nullptr || rightStick == nullptr) {
    wpi_setWPIError(NullParameter);
    return;
  }
  TankDrive(leftStick->GetRawAxis(leftAxis), rightStick->GetRawAxis(rightAxis),
            squaredInputs);
}

void RobotDrive::TankDrive(GenericHID& leftStick, int leftAxis,
                           GenericHID& rightStick, int rightAxis,
                           bool squaredInputs) {
  TankDrive(leftStick.GetRawAxis(leftAxis), rightStick.GetRawAxis(rightAxis),
            squaredInputs);
}

void RobotDrive::TankDrive(double leftValue, double rightValue,
                           bool squaredInputs) {
  static bool reported = false;
  if (!reported) {
    HAL_Report(HALUsageReporting::kResourceType_RobotDrive,
               HALUsageReporting::kRobotDrive_Tank, GetNumMotors());
    reported = true;
  }

  leftValue = Limit(leftValue);
  rightValue = Limit(rightValue);

  // square the inputs (while preserving the sign) to increase fine control
  // while permitting full power
  if (squaredInputs) {
    leftValue = std::copysign(leftValue * leftValue, leftValue);
    rightValue = std::copysign(rightValue * rightValue, rightValue);
  }

  SetLeftRightMotorOutputs(leftValue, rightValue);
}

void RobotDrive::ArcadeDrive(GenericHID* stick, bool squaredInputs) {
  // simply call the full-featured ArcadeDrive with the appropriate values
  ArcadeDrive(stick->GetY(), stick->GetX(), squaredInputs);
}

void RobotDrive::ArcadeDrive(GenericHID& stick, bool squaredInputs) {
  // simply call the full-featured ArcadeDrive with the appropriate values
  ArcadeDrive(stick.GetY(), stick.GetX(), squaredInputs);
}

void RobotDrive::ArcadeDrive(GenericHID* moveStick, int moveAxis,
                             GenericHID* rotateStick, int rotateAxis,
                             bool squaredInputs) {
  double moveValue = moveStick->GetRawAxis(moveAxis);
  double rotateValue = rotateStick->GetRawAxis(rotateAxis);

  ArcadeDrive(moveValue, rotateValue, squaredInputs);
}

void RobotDrive::ArcadeDrive(GenericHID& moveStick, int moveAxis,
                             GenericHID& rotateStick, int rotateAxis,
                             bool squaredInputs) {
  double moveValue = moveStick.GetRawAxis(moveAxis);
  double rotateValue = rotateStick.GetRawAxis(rotateAxis);

  ArcadeDrive(moveValue, rotateValue, squaredInputs);
}

void RobotDrive::ArcadeDrive(double moveValue, double rotateValue,
                             bool squaredInputs) {
  static bool reported = false;
  if (!reported) {
    HAL_Report(HALUsageReporting::kResourceType_RobotDrive,
               HALUsageReporting::kRobotDrive_ArcadeStandard, GetNumMotors());
    reported = true;
  }

  // local variables to hold the computed PWM values for the motors
  double leftMotorOutput;
  double rightMotorOutput;

  moveValue = Limit(moveValue);
  rotateValue = Limit(rotateValue);

  // square the inputs (while preserving the sign) to increase fine control
  // while permitting full power
  if (squaredInputs) {
    moveValue = std::copysign(moveValue * moveValue, moveValue);
    rotateValue = std::copysign(rotateValue * rotateValue, rotateValue);
  }

  if (moveValue > 0.0) {
    if (rotateValue > 0.0) {
      leftMotorOutput = moveValue - rotateValue;
      rightMotorOutput = std::max(moveValue, rotateValue);
    } else {
      leftMotorOutput = std::max(moveValue, -rotateValue);
      rightMotorOutput = moveValue + rotateValue;
    }
  } else {
    if (rotateValue > 0.0) {
      leftMotorOutput = -std::max(-moveValue, rotateValue);
      rightMotorOutput = moveValue + rotateValue;
    } else {
      leftMotorOutput = moveValue - rotateValue;
      rightMotorOutput = -std::max(-moveValue, -rotateValue);
    }
  }
  SetLeftRightMotorOutputs(leftMotorOutput, rightMotorOutput);
}

void RobotDrive::MecanumDrive_Cartesian(double x, double y, double rotation,
                                        double gyroAngle) {
  static bool reported = false;
  if (!reported) {
    HAL_Report(HALUsageReporting::kResourceType_RobotDrive,
               HALUsageReporting::kRobotDrive_MecanumCartesian, GetNumMotors());
    reported = true;
  }

  double xIn = x;
  double yIn = y;
  // Negate y for the joystick.
  yIn = -yIn;
  // Compenstate for gyro angle.
  RotateVector(xIn, yIn, gyroAngle);

  double wheelSpeeds[kMaxNumberOfMotors];
  wheelSpeeds[kFrontLeftMotor] = xIn + yIn + rotation;
  wheelSpeeds[kFrontRightMotor] = -xIn + yIn - rotation;
  wheelSpeeds[kRearLeftMotor] = -xIn + yIn + rotation;
  wheelSpeeds[kRearRightMotor] = xIn + yIn - rotation;

  Normalize(wheelSpeeds);

  m_frontLeftMotor->Set(wheelSpeeds[kFrontLeftMotor] * m_maxOutput);
  m_frontRightMotor->Set(wheelSpeeds[kFrontRightMotor] * m_maxOutput);
  m_rearLeftMotor->Set(wheelSpeeds[kRearLeftMotor] * m_maxOutput);
  m_rearRightMotor->Set(wheelSpeeds[kRearRightMotor] * m_maxOutput);

  Feed();
}

void RobotDrive::MecanumDrive_Polar(double magnitude, double direction,
                                    double rotation) {
  static bool reported = false;
  if (!reported) {
    HAL_Report(HALUsageReporting::kResourceType_RobotDrive,
               HALUsageReporting::kRobotDrive_MecanumPolar, GetNumMotors());
    reported = true;
  }

  // Normalized for full power along the Cartesian axes.
  magnitude = Limit(magnitude) * std::sqrt(2.0);
  // The rollers are at 45 degree angles.
  double dirInRad = (direction + 45.0) * 3.14159 / 180.0;
  double cosD = std::cos(dirInRad);
  double sinD = std::sin(dirInRad);

  double wheelSpeeds[kMaxNumberOfMotors];
  wheelSpeeds[kFrontLeftMotor] = sinD * magnitude + rotation;
  wheelSpeeds[kFrontRightMotor] = cosD * magnitude - rotation;
  wheelSpeeds[kRearLeftMotor] = cosD * magnitude + rotation;
  wheelSpeeds[kRearRightMotor] = sinD * magnitude - rotation;

  Normalize(wheelSpeeds);

  m_frontLeftMotor->Set(wheelSpeeds[kFrontLeftMotor] * m_maxOutput);
  m_frontRightMotor->Set(wheelSpeeds[kFrontRightMotor] * m_maxOutput);
  m_rearLeftMotor->Set(wheelSpeeds[kRearLeftMotor] * m_maxOutput);
  m_rearRightMotor->Set(wheelSpeeds[kRearRightMotor] * m_maxOutput);

  Feed();
}

void RobotDrive::HolonomicDrive(double magnitude, double direction,
                                double rotation) {
  MecanumDrive_Polar(magnitude, direction, rotation);
}

void RobotDrive::SetLeftRightMotorOutputs(double leftOutput,
                                          double rightOutput) {
  wpi_assert(m_rearLeftMotor != nullptr && m_rearRightMotor != nullptr);

  if (m_frontLeftMotor != nullptr)
    m_frontLeftMotor->Set(Limit(leftOutput) * m_maxOutput);
  m_rearLeftMotor->Set(Limit(leftOutput) * m_maxOutput);

  if (m_frontRightMotor != nullptr)
    m_frontRightMotor->Set(-Limit(rightOutput) * m_maxOutput);
  m_rearRightMotor->Set(-Limit(rightOutput) * m_maxOutput);

  Feed();
}

void RobotDrive::SetInvertedMotor(MotorType motor, bool isInverted) {
  if (motor < 0 || motor > 3) {
    wpi_setWPIError(InvalidMotorIndex);
    return;
  }
  switch (motor) {
    case kFrontLeftMotor:
      m_frontLeftMotor->SetInverted(isInverted);
      break;
    case kFrontRightMotor:
      m_frontRightMotor->SetInverted(isInverted);
      break;
    case kRearLeftMotor:
      m_rearLeftMotor->SetInverted(isInverted);
      break;
    case kRearRightMotor:
      m_rearRightMotor->SetInverted(isInverted);
      break;
  }
}

void RobotDrive::SetSensitivity(double sensitivity) {
  m_sensitivity = sensitivity;
}

void RobotDrive::SetMaxOutput(double maxOutput) { m_maxOutput = maxOutput; }

void RobotDrive::GetDescription(wpi::raw_ostream& desc) const {
  desc << "RobotDrive";
}

void RobotDrive::StopMotor() {
  if (m_frontLeftMotor != nullptr) m_frontLeftMotor->StopMotor();
  if (m_frontRightMotor != nullptr) m_frontRightMotor->StopMotor();
  if (m_rearLeftMotor != nullptr) m_rearLeftMotor->StopMotor();
  if (m_rearRightMotor != nullptr) m_rearRightMotor->StopMotor();
  Feed();
}

void RobotDrive::InitRobotDrive() { SetSafetyEnabled(true); }

double RobotDrive::Limit(double number) {
  if (number > 1.0) {
    return 1.0;
  }
  if (number < -1.0) {
    return -1.0;
  }
  return number;
}

void RobotDrive::Normalize(double* wheelSpeeds) {
  double maxMagnitude = std::fabs(wheelSpeeds[0]);
  for (int i = 1; i < kMaxNumberOfMotors; i++) {
    double temp = std::fabs(wheelSpeeds[i]);
    if (maxMagnitude < temp) maxMagnitude = temp;
  }
  if (maxMagnitude > 1.0) {
    for (int i = 0; i < kMaxNumberOfMotors; i++) {
      wheelSpeeds[i] = wheelSpeeds[i] / maxMagnitude;
    }
  }
}

void RobotDrive::RotateVector(double& x, double& y, double angle) {
  double cosA = std::cos(angle * (3.14159 / 180.0));
  double sinA = std::sin(angle * (3.14159 / 180.0));
  double xOut = x * cosA - y * sinA;
  double yOut = x * sinA + y * cosA;
  x = xOut;
  y = yOut;
}
