using System;

namespace MultiWheelC.TrajectoryPlanning.EMPlanner;

public sealed class JerkLimitedStoppingProfile
{
    internal JerkLimitedStoppingProfile(double distanceMeters, double durationSeconds,
        double finalSpeedMetersPerSecond, double finalAccelerationMetersPerSecondSquared)
    {
        DistanceMeters = distanceMeters;
        DurationSeconds = durationSeconds;
        FinalSpeedMetersPerSecond = finalSpeedMetersPerSecond;
        FinalAccelerationMetersPerSecondSquared = finalAccelerationMetersPerSecondSquared;
    }

    public double DistanceMeters { get; }
    public double DurationSeconds { get; }
    public double FinalSpeedMetersPerSecond { get; }
    public double FinalAccelerationMetersPerSecondSquared { get; }
}

public static class JerkLimitedStoppingMath
{
    private const double NumericTolerance = 1e-12d;

    public static bool TryCalculate(double speed, double acceleration,
        double maximumDeceleration, double maximumJerk,
        out JerkLimitedStoppingProfile profile, out string failureReason)
    {
        profile = null;
        failureReason = string.Empty;
        if (!IsFinite(speed) || speed < 0d || !IsFinite(acceleration) ||
            !IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
            acceleration < -maximumDeceleration - NumericTolerance)
        {
            failureReason = "Stopping inputs are outside finite longitudinal bounds.";
            return false;
        }
        if (speed <= NumericTolerance && Math.Abs(acceleration) <= NumericTolerance)
        {
            profile = new JerkLimitedStoppingProfile(0d, 0d, 0d, 0d);
            return true;
        }

        double unavoidableReleaseLoss = acceleration < 0d
            ? acceleration * acceleration / (2d * maximumJerk)
            : 0d;
        if (speed + NumericTolerance < unavoidableReleaseLoss)
        {
            failureReason = "The current negative acceleration cannot be released before speed crosses zero.";
            return false;
        }

        double peakDeceleration = Math.Sqrt(maximumJerk * speed +
            0.5d * acceleration * acceleration);
        double downDuration;
        double plateauDuration;
        double upDuration;
        if (peakDeceleration <= maximumDeceleration + NumericTolerance)
        {
            peakDeceleration = Math.Min(peakDeceleration, maximumDeceleration);
            downDuration = (acceleration + peakDeceleration) / maximumJerk;
            plateauDuration = 0d;
            upDuration = peakDeceleration / maximumJerk;
        }
        else
        {
            peakDeceleration = maximumDeceleration;
            downDuration = (acceleration + peakDeceleration) / maximumJerk;
            upDuration = peakDeceleration / maximumJerk;
            double speedAfterDown = speed + acceleration * downDuration -
                0.5d * maximumJerk * downDuration * downDuration;
            double releaseLoss = peakDeceleration * peakDeceleration /
                (2d * maximumJerk);
            plateauDuration = (speedAfterDown - releaseLoss) / peakDeceleration;
        }
        if (downDuration < -NumericTolerance || plateauDuration < -NumericTolerance)
        {
            failureReason = "No monotone three-phase jerk-limited stop exists for the current state.";
            return false;
        }

        downDuration = Math.Max(0d, downDuration);
        plateauDuration = Math.Max(0d, plateauDuration);
        double s = 0d;
        double u = speed;
        double a = acceleration;
        Integrate(ref s, ref u, ref a, -maximumJerk, downDuration);
        Integrate(ref s, ref u, ref a, 0d, plateauDuration);
        Integrate(ref s, ref u, ref a, maximumJerk, upDuration);
        if (Math.Abs(u) > 1e-9d || Math.Abs(a) > 1e-9d || s < -NumericTolerance)
        {
            failureReason = "The jerk-limited stop did not end at zero speed and acceleration.";
            return false;
        }

        profile = new JerkLimitedStoppingProfile(Math.Max(0d, s),
            downDuration + plateauDuration + upDuration, 0d, 0d);
        return true;
    }

    public static double MaximumInitialSpeedForDistance(double availableDistance,
        double conservativeInitialAcceleration, double maximumDeceleration,
        double maximumJerk, double directionMaximumSpeed)
    {
        if (!IsFinite(availableDistance) || availableDistance < 0d ||
            !IsFinite(conservativeInitialAcceleration) ||
            !IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
            !IsPositiveFinite(directionMaximumSpeed))
            throw new ArgumentOutOfRangeException(nameof(availableDistance));

        double lower = 0d;
        double upper = directionMaximumSpeed;
        for (int iteration = 0; iteration < 64; iteration++)
        {
            double candidate = 0.5d * (lower + upper);
            bool fits = TryCalculate(candidate, conservativeInitialAcceleration,
                maximumDeceleration, maximumJerk,
                out JerkLimitedStoppingProfile stop, out _) &&
                stop.DistanceMeters <= availableDistance + NumericTolerance;
            if (fits)
                lower = candidate;
            else
                upper = candidate;
        }
        return lower;
    }

    public static double CalculateMaximumStoppedDistance(double initialSpeed,
        double initialAcceleration, double maximumSpeed, double maximumAcceleration,
        double maximumDeceleration, double maximumJerk, double timeHorizon)
    {
        if (!IsFinite(initialSpeed) || initialSpeed < 0d ||
            !IsFinite(initialAcceleration) || !IsPositiveFinite(maximumSpeed) ||
            !IsPositiveFinite(maximumAcceleration) || !IsPositiveFinite(maximumDeceleration) ||
            !IsPositiveFinite(maximumJerk) || !IsPositiveFinite(timeHorizon))
            throw new ArgumentOutOfRangeException(nameof(timeHorizon));

        double lower = 0d;
        double upper = timeHorizon;
        double bestDistance = 0d;
        for (int iteration = 0; iteration < 64; iteration++)
        {
            double driveDuration = 0.5d * (lower + upper);
            AdvanceTowardMaximumSpeed(initialSpeed, initialAcceleration,
                maximumSpeed, maximumAcceleration, maximumJerk, driveDuration,
                out double driveDistance, out double speed, out double acceleration);
            bool fits = TryCalculate(speed, acceleration, maximumDeceleration,
                maximumJerk, out JerkLimitedStoppingProfile stop, out _) &&
                driveDuration + stop.DurationSeconds <= timeHorizon + NumericTolerance;
            if (fits)
            {
                lower = driveDuration;
                bestDistance = Math.Max(bestDistance, driveDistance + stop.DistanceMeters);
            }
            else
            {
                upper = driveDuration;
            }
        }
        return bestDistance;
    }

    private static void AdvanceTowardMaximumSpeed(double initialSpeed, double initialAcceleration,
        double maximumSpeed, double maximumAcceleration, double maximumJerk, double duration,
        out double distance, out double speed, out double acceleration)
    {
        distance = 0d;
        speed = initialSpeed;
        acceleration = initialAcceleration;
        double remaining = duration;
        while (remaining > NumericTolerance)
        {
            double dt = Math.Min(0.001d, remaining);
            double speedNeededToReleaseAcceleration = acceleration > 0d
                ? acceleration * acceleration / (2d * maximumJerk)
                : 0d;
            double jerk = speed + speedNeededToReleaseAcceleration >= maximumSpeed
                ? (acceleration > 0d ? -maximumJerk : 0d)
                : (acceleration < maximumAcceleration ? maximumJerk : 0d);
            Integrate(ref distance, ref speed, ref acceleration, jerk, dt);
            if (speed > maximumSpeed && speed - maximumSpeed <= 1e-6d)
            {
                speed = maximumSpeed;
                acceleration = 0d;
            }
            remaining -= dt;
        }
    }

    private static void Integrate(ref double s, ref double u, ref double a,
        double jerk, double duration)
    {
        s += u * duration + 0.5d * a * duration * duration +
            jerk * duration * duration * duration / 6d;
        u += a * duration + 0.5d * jerk * duration * duration;
        a += jerk * duration;
    }

    private static bool IsPositiveFinite(double value) => IsFinite(value) && value > 0d;

    private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value);
}
