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

#include "wpi/math/controller/DifferentialDriveAccelerationLimiter.hpp"

#include <gtest/gtest.h>

#include "wpi/math/system/Models.hpp"
#include "wpi/units/math.hpp"

namespace wpi::math {

TEST(DifferentialDriveAccelerationLimiterTest, LowLimits) {
  constexpr auto trackwidth = 0.9_m;
  constexpr wpi::units::second_t dt = 5_ms;
  constexpr auto maxA = 2_mps_sq;
  constexpr auto maxAlpha = 2_rad_per_s_sq;

  using Kv_t = decltype(1_V / 1_mps);
  using Ka_t = decltype(1_V / 1_mps_sq);
  auto plant = Models::DifferentialDriveFromSysId(Kv_t{1.0}, Ka_t{1.0},
                                                  Kv_t{1.0}, Ka_t{1.0});

  DifferentialDriveAccelerationLimiter accelLimiter{plant, trackwidth, maxA,
                                                    maxAlpha};

  Vectord<2> x{0.0, 0.0};
  Vectord<2> xAccelLimiter{0.0, 0.0};

  // Ensure voltage exceeds acceleration before limiting
  {
    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{12.0, 12.0};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    EXPECT_GT(wpi::units::math::abs(a), maxA);
  }
  {
    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{-12.0, 12.0};
    wpi::units::radians_per_second_squared_t alpha{(accels(1) - accels(0)) /
                                                   trackwidth.value()};
    EXPECT_GT(wpi::units::math::abs(alpha), maxAlpha);
  }

  // Forward
  Vectord<2> u{12.0, 12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{left, right};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    wpi::units::radians_per_second_squared_t alpha{(accels(1) - accels(0)) /
                                                   trackwidth.value()};
    EXPECT_LE(wpi::units::math::abs(a), maxA);
    EXPECT_LE(wpi::units::math::abs(alpha), maxAlpha);
  }

  // Backward
  u = Vectord<2>{-12.0, -12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{left, right};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    wpi::units::radians_per_second_squared_t alpha{(accels(1) - accels(0)) /
                                                   trackwidth.value()};
    EXPECT_LE(wpi::units::math::abs(a), maxA);
    EXPECT_LE(wpi::units::math::abs(alpha), maxAlpha);
  }

  // Rotate CCW
  u = Vectord<2>{-12.0, 12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{left, right};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    wpi::units::radians_per_second_squared_t alpha{(accels(1) - accels(0)) /
                                                   trackwidth.value()};
    EXPECT_LE(wpi::units::math::abs(a), maxA);
    EXPECT_LE(wpi::units::math::abs(alpha), maxAlpha);
  }
}

TEST(DifferentialDriveAccelerationLimiterTest, HighLimits) {
  constexpr auto trackwidth = 0.9_m;
  constexpr wpi::units::second_t dt = 5_ms;

  using Kv_t = decltype(1_V / 1_mps);
  using Ka_t = decltype(1_V / 1_mps_sq);

  auto plant = Models::DifferentialDriveFromSysId(Kv_t{1.0}, Ka_t{1.0},
                                                  Kv_t{1.0}, Ka_t{1.0});

  // Limits are so high, they don't get hit, so states of constrained and
  // unconstrained systems should match
  DifferentialDriveAccelerationLimiter accelLimiter{
      plant, trackwidth, 1e3_mps_sq, 1e3_rad_per_s_sq};

  Vectord<2> x{0.0, 0.0};
  Vectord<2> xAccelLimiter{0.0, 0.0};

  // Forward
  Vectord<2> u{12.0, 12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    EXPECT_DOUBLE_EQ(x(0), xAccelLimiter(0));
    EXPECT_DOUBLE_EQ(x(1), xAccelLimiter(1));
  }

  // Backward
  x.setZero();
  xAccelLimiter.setZero();
  u = Vectord<2>{-12.0, -12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    EXPECT_DOUBLE_EQ(x(0), xAccelLimiter(0));
    EXPECT_DOUBLE_EQ(x(1), xAccelLimiter(1));
  }

  // Rotate CCW
  x.setZero();
  xAccelLimiter.setZero();
  u = Vectord<2>{-12.0, 12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    EXPECT_DOUBLE_EQ(x(0), xAccelLimiter(0));
    EXPECT_DOUBLE_EQ(x(1), xAccelLimiter(1));
  }
}

TEST(DifferentialDriveAccelerationLimiterTest, SeparateMinMaxLowLimits) {
  constexpr auto trackwidth = 0.9_m;
  constexpr wpi::units::second_t dt = 5_ms;
  constexpr auto minA = -1_mps_sq;
  constexpr auto maxA = 2_mps_sq;
  constexpr auto maxAlpha = 2_rad_per_s_sq;

  using Kv_t = decltype(1_V / 1_mps);
  using Ka_t = decltype(1_V / 1_mps_sq);
  auto plant = Models::DifferentialDriveFromSysId(Kv_t{1.0}, Ka_t{1.0},
                                                  Kv_t{1.0}, Ka_t{1.0});

  DifferentialDriveAccelerationLimiter accelLimiter{plant, trackwidth, minA,
                                                    maxA, maxAlpha};

  Vectord<2> x{0.0, 0.0};
  Vectord<2> xAccelLimiter{0.0, 0.0};

  // Ensure voltage exceeds acceleration before limiting
  {
    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{12.0, 12.0};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    EXPECT_GT(wpi::units::math::abs(a), maxA);
    EXPECT_GT(wpi::units::math::abs(a), -minA);
  }

  // a should always be within [minA, maxA]
  // Forward
  Vectord<2> u{12.0, 12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{left, right};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    EXPECT_GE(a, minA);
    EXPECT_LE(a, maxA);
  }

  // Backward
  u = Vectord<2>{-12.0, -12.0};
  for (auto t = 0_s; t < 3_s; t += dt) {
    x = plant.CalculateX(x, u, dt);
    auto [left, right] = accelLimiter.Calculate(
        wpi::units::meters_per_second_t{xAccelLimiter(0)},
        wpi::units::meters_per_second_t{xAccelLimiter(1)},
        wpi::units::volt_t{u(0)}, wpi::units::volt_t{u(1)});
    xAccelLimiter =
        plant.CalculateX(xAccelLimiter, Vectord<2>{left, right}, dt);

    Vectord<2> accels =
        plant.A() * xAccelLimiter + plant.B() * Vectord<2>{left, right};
    wpi::units::meters_per_second_squared_t a{(accels(0) + accels(1)) / 2.0};
    EXPECT_GE(a, minA);
    EXPECT_LE(a, maxA);
  }
}

TEST(DifferentialDriveAccelerationLimiterTest, MinAccelGreaterThanMaxAccel) {
  using Kv_t = decltype(1_V / 1_mps);
  using Ka_t = decltype(1_V / 1_mps_sq);
  auto plant = Models::DifferentialDriveFromSysId(Kv_t{1.0}, Ka_t{1.0},
                                                  Kv_t{1.0}, Ka_t{1.0});
  EXPECT_NO_THROW({
    DifferentialDriveAccelerationLimiter accelLimiter(plant, 1_m, 1_mps_sq,
                                                      1_rad_per_s_sq);
  });

  EXPECT_THROW(
      {
        DifferentialDriveAccelerationLimiter accelLimiter(
            plant, 1_m, 1_mps_sq, -1_mps_sq, 1_rad_per_s_sq);
      },
      std::invalid_argument);
}

}  // namespace wpi::math
