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

#pragma once

#include <numbers>
#include <type_traits>

#include <gcem.hpp>

#include "wpi/math/geometry/Translation2d.hpp"
#include "wpi/math/geometry/Translation3d.hpp"
#include "wpi/math/util/MathShared.hpp"
#include "wpi/units/angle.hpp"
#include "wpi/units/base.hpp"
#include "wpi/units/length.hpp"
#include "wpi/units/math.hpp"
#include "wpi/units/time.hpp"
#include "wpi/units/velocity.hpp"
#include "wpi/util/SymbolExports.hpp"

namespace wpi::math {

/**
 * Returns 0.0 if the given value is within the specified range around zero. The
 * remaining range between the deadband and the maximum magnitude is scaled from
 * 0.0 to the maximum magnitude.
 *
 * @param value Value to clip.
 * @param deadband Range around zero.
 * @param maxMagnitude The maximum magnitude of the input (defaults to 1). Can
 * be infinite.
 * @return The value after the deadband is applied.
 */
template <typename T>
  requires std::is_arithmetic_v<T> || wpi::units::traits::is_unit_t_v<T>
constexpr T ApplyDeadband(T value, T deadband, T maxMagnitude = T{1.0}) {
  T magnitude;
  if constexpr (std::is_arithmetic_v<T>) {
    magnitude = gcem::abs(value);
  } else {
    magnitude = wpi::units::math::abs(value);
  }

  if (magnitude < deadband) {
    return T{0.0};
  }

  if (value > T{0.0}) {
    // Map deadband to 0 and map max to max with a linear relationship.
    //
    //   y - y₁ = m(x - x₁)
    //   y - y₁ = (y₂ - y₁)/(x₂ - x₁) (x - x₁)
    //   y = (y₂ - y₁)/(x₂ - x₁) (x - x₁) + y₁
    //
    // (x₁, y₁) = (deadband, 0) and (x₂, y₂) = (max, max).
    //
    //   x₁ = deadband
    //   y₁ = 0
    //   x₂ = max
    //   y₂ = max
    //   y = (max - 0)/(max - deadband) (x - deadband) + 0
    //   y = max/(max - deadband) (x - deadband)
    //
    // To handle high values of max, rewrite so that max only appears on the
    // denominator.
    //
    //   y = ((max - deadband) + deadband)/(max - deadband) (x - deadband)
    //   y = (1 + deadband/(max - deadband)) (x - deadband)
    return (1.0 + deadband / (maxMagnitude - deadband)) * (value - deadband);
  } else {
    // Map -deadband to 0 and map -max to -max with a linear relationship.
    //
    //   y - y₁ = m(x - x₁)
    //   y - y₁ = (y₂ - y₁)/(x₂ - x₁) (x - x₁)
    //   y = (y₂ - y₁)/(x₂ - x₁) (x - x₁) + y₁
    //
    // (x₁, y₁) = (-deadband, 0) and (x₂, y₂) = (-max, -max).
    //
    //   x₁ = -deadband
    //   y₁ = 0
    //   x₂ = -max
    //   y₂ = -max
    //   y = (-max - 0)/(-max + deadband) (x + deadband) + 0
    //   y = max/(max - deadband) (x + deadband)
    //
    // To handle high values of max, rewrite so that max only appears on the
    // denominator.
    //
    //   y = ((max - deadband) + deadband)/(max - deadband) (x + deadband)
    //   y = (1 + deadband/(max - deadband)) (x + deadband)
    return (1.0 + deadband / (maxMagnitude - deadband)) * (value + deadband);
  }
}

/**
 * Returns a zero vector if the given vector is within the specified
 * distance from the origin. The remaining distance between the deadband and the
 * maximum distance is scaled from the origin to the maximum distance.
 *
 * @param value Value to clip.
 * @param deadband Distance from origin.
 * @param maxMagnitude The maximum distance from the origin of the input
 * (defaults to 1). Can be infinite.
 * @return The value after the deadband is applied.
 */
template <typename T, int N>
  requires std::is_arithmetic_v<T> || wpi::units::traits::is_unit_t_v<T>
Eigen::Vector<T, N> ApplyDeadband(const Eigen::Vector<T, N>& value, T deadband,
                                  T maxMagnitude = T{1.0}) {
  if constexpr (std::is_arithmetic_v<T>) {
    if (value.norm() < T{1e-9}) {
      return Eigen::Vector<T, N>::Zero();
    }
    return value.normalized() *
           ApplyDeadband(value.norm(), deadband, maxMagnitude);
  } else {
    const Eigen::Vector<double, N> asDouble = value.template cast<double>();
    const Eigen::Vector<double, N> processed =
        ApplyDeadband(asDouble, deadband.value(), maxMagnitude.value());
    return processed.template cast<T>();
  }
}

/**
 * Raises the input to the power of the given exponent while preserving its
 * sign.
 *
 * The function normalizes the input value to the range [0, 1] based on the
 * maximum magnitude so that the output stays in the range.
 *
 * This is useful for applying smoother or more aggressive control response
 * curves (e.g. joystick input shaping).
 *
 * @param value The input value to transform.
 * @param exponent The exponent to apply (e.g. 1.0 = linear, 2.0 = squared
 * curve). Must be positive.
 * @param maxMagnitude The maximum expected absolute value of input (defaults to
 * 1). Must be positive.
 * @return The transformed value with the same sign and scaled to the input
 * range.
 */
template <typename T>
  requires std::is_arithmetic_v<T> || wpi::units::traits::is_unit_t_v<T>
constexpr T CopyDirectionPow(T value, double exponent,
                             T maxMagnitude = T{1.0}) {
  if constexpr (std::is_arithmetic_v<T>) {
    return gcem::copysign(
        gcem::pow(gcem::abs(value) / maxMagnitude, exponent) * maxMagnitude,
        value);
  } else {
    return wpi::units::math::copysign(
        gcem::pow((wpi::units::math::abs(value) / maxMagnitude).value(),
                  exponent) *
            maxMagnitude,
        value);
  }
}

/**
 * Raises the norm of the input to the power of the given exponent while
 * preserving its direction.
 *
 * The function normalizes the input value to the range [0, 1] based on the
 * maximum magnitude so that the output stays in the range.
 *
 * This is useful for applying smoother or more aggressive control response
 * curves (e.g. joystick input shaping).
 *
 * @param value The input vector to transform.
 * @param exponent The exponent to apply (e.g. 1.0 = linear, 2.0 = squared
 * curve). Must be positive.
 * @param maxMagnitude The maximum expected distance from origin of input
 * (defaults to 1). Must be positive.
 * @return The transformed value with the same direction and norm scaled to
 * the input range.
 */
template <typename T, int N>
  requires std::is_arithmetic_v<T> || wpi::units::traits::is_unit_t_v<T>
Eigen::Vector<T, N> CopyDirectionPow(const Eigen::Vector<T, N>& value,
                                     double exponent, T maxMagnitude = T{1.0}) {
  if constexpr (std::is_arithmetic_v<T>) {
    if (value.norm() < T{1e-9}) {
      return Eigen::Vector<T, N>::Zero();
    }
    return value.normalized() *
           CopyDirectionPow(value.norm(), exponent, maxMagnitude);
  } else {
    const Eigen::Vector<double, N> asDouble = value.template cast<double>();
    const Eigen::Vector<double, N> processed =
        CopyDirectionPow(asDouble, exponent, maxMagnitude.value());
    return processed.template cast<T>();
  }
}

/**
 * Returns modulus of input.
 *
 * @param input        Input value to wrap.
 * @param minimumInput The minimum value expected from the input.
 * @param maximumInput The maximum value expected from the input.
 */
template <typename T>
constexpr T InputModulus(T input, T minimumInput, T maximumInput) {
  T modulus = maximumInput - minimumInput;

  // Wrap input if it's above the maximum input
  int numMax = (input - minimumInput) / modulus;
  input -= numMax * modulus;

  // Wrap input if it's below the minimum input
  int numMin = (input - maximumInput) / modulus;
  input -= numMin * modulus;

  return input;
}

/**
 * Checks if the given value matches an expected value within a certain
 * tolerance.
 *
 * @param expected The expected value
 * @param actual The actual value
 * @param tolerance The allowed difference between the actual and the expected
 * value
 * @return Whether or not the actual value is within the allowed tolerance
 */
template <typename T>
  requires std::is_arithmetic_v<T> || wpi::units::traits::is_unit_t_v<T>
constexpr bool IsNear(T expected, T actual, T tolerance) {
  if constexpr (std::is_arithmetic_v<T>) {
    return std::abs(expected - actual) < tolerance;
  } else {
    return wpi::units::math::abs(expected - actual) < tolerance;
  }
}

/**
 * Checks if the given value matches an expected value within a certain
 * tolerance. Supports continuous input for cases like absolute encoders.
 *
 * Continuous input means that the min and max value are considered to be the
 * same point, and tolerances can be checked across them. A common example
 * would be for absolute encoders: calling isNear(2, 359, 5, 0, 360) returns
 * true because 359 is 1 away from 360 (which is treated as the same as 0) and
 * 2 is 2 away from 0, adding up to an error of 3 degrees, which is within the
 * given tolerance of 5.
 *
 * @param expected The expected value
 * @param actual The actual value
 * @param tolerance The allowed difference between the actual and the expected
 * value
 * @param min Smallest value before wrapping around to the largest value
 * @param max Largest value before wrapping around to the smallest value
 * @return Whether or not the actual value is within the allowed tolerance
 */
template <typename T>
  requires std::is_arithmetic_v<T> || wpi::units::traits::is_unit_t_v<T>
constexpr bool IsNear(T expected, T actual, T tolerance, T min, T max) {
  T errorBound = (max - min) / 2.0;
  T error =
      wpi::math::InputModulus<T>(expected - actual, -errorBound, errorBound);

  if constexpr (std::is_arithmetic_v<T>) {
    return std::abs(error) < tolerance;
  } else {
    return wpi::units::math::abs(error) < tolerance;
  }
}

/**
 * Wraps an angle to the range -π to π radians (-180 to 180 degrees).
 *
 * @param angle Angle to wrap.
 */
WPILIB_DLLEXPORT
constexpr wpi::units::radian_t AngleModulus(wpi::units::radian_t angle) {
  return InputModulus<wpi::units::radian_t>(
      angle, wpi::units::radian_t{-std::numbers::pi},
      wpi::units::radian_t{std::numbers::pi});
}

// floorDiv and floorMod algorithms taken from Java

/**
 * Returns the largest (closest to positive infinity)
 * {@code int} value that is less than or equal to the algebraic quotient.
 *
 * @param x the dividend
 * @param y the divisor
 * @return the largest (closest to positive infinity)
 *   {@code int} value that is less than or equal to the algebraic quotient.
 */
constexpr std::signed_integral auto FloorDiv(std::signed_integral auto x,
                                             std::signed_integral auto y) {
  auto quot = x / y;
  auto rem = x % y;
  // if the signs are different and modulo not zero, round down
  if ((x < 0) != (y < 0) && rem != 0) {
    --quot;
  }
  return quot;
}

/**
 * Returns the floor modulus of the {@code int} arguments.
 * <p>
 * The floor modulus is {@code r = x - (floorDiv(x, y) * y)},
 * has the same sign as the divisor {@code y} or is zero, and
 * is in the range of {@code -std::abs(y) < r < +std::abs(y)}.
 *
 * @param x the dividend
 * @param y the divisor
 * @return the floor modulus {@code x - (floorDiv(x, y) * y)}
 */
constexpr std::signed_integral auto FloorMod(std::signed_integral auto x,
                                             std::signed_integral auto y) {
  return x - FloorDiv(x, y) * y;
}

/**
 * Limits translation velocity.
 *
 * @param current Translation at current timestep.
 * @param next Translation at next timestep.
 * @param dt Timestep duration.
 * @param maxVelocity Maximum translation velocity.
 * @return Returns the next Translation2d limited to maxVelocity
 */
constexpr Translation2d SlewRateLimit(
    const Translation2d& current, const Translation2d& next,
    wpi::units::second_t dt, wpi::units::meters_per_second_t maxVelocity) {
  if (maxVelocity < 0_mps) {
    wpi::math::MathSharedStore::ReportError(
        "maxVelocity must be a non-negative number, got {}!", maxVelocity);
    return next;
  }
  Translation2d diff = next - current;
  wpi::units::meter_t dist = diff.Norm();
  if (dist < 1e-9_m) {
    return next;
  }
  if (dist > maxVelocity * dt) {
    // Move maximum allowed amount in direction of the difference
    // NOLINTNEXTLINE(bugprone-integer-division)
    return current + diff * (maxVelocity * dt / dist);
  }
  return next;
}

/**
 * Limits translation velocity.
 *
 * @param current Translation at current timestep.
 * @param next Translation at next timestep.
 * @param dt Timestep duration.
 * @param maxVelocity Maximum translation velocity.
 * @return Returns the next Translation3d limited to maxVelocity
 */
constexpr Translation3d SlewRateLimit(
    const Translation3d& current, const Translation3d& next,
    wpi::units::second_t dt, wpi::units::meters_per_second_t maxVelocity) {
  if (maxVelocity < 0_mps) {
    wpi::math::MathSharedStore::ReportError(
        "maxVelocity must be a non-negative number, got {}!", maxVelocity);
    return next;
  }
  Translation3d diff = next - current;
  wpi::units::meter_t dist = diff.Norm();
  if (dist < 1e-9_m) {
    return next;
  }
  if (dist > maxVelocity * dt) {
    // Move maximum allowed amount in direction of the difference
    // NOLINTNEXTLINE(bugprone-integer-division)
    return current + diff * (maxVelocity * dt / dist);
  }
  return next;
}

}  // namespace wpi::math
