using System;
using System.Collections.Generic;

namespace MultiWheelC.TrajectoryPlanning.EMPlanner;

/// <summary>Independently validates ST candidates directly in physical units before they may become fallbacks.</summary>
public sealed class LongitudinalSolutionValidator
{
    public bool TryValidate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate candidate,
        out LongitudinalCandidate validatedCandidate, out string failureReason)
    {
        validatedCandidate = null;
        failureReason = string.Empty;
        if (input == null || speedLimit == null || candidate == null)
        {
            failureReason = "ST input, speed envelope, and candidate are required.";
            return false;
        }
        try
        {
            if (!PathSpeedLimitBuilder.TryGetLimits(input, out _, out double maximumAcceleration,
                    out double maximumDeceleration, out double maximumJerk, out _, out _, out failureReason))
            {
                return false;
            }
            IReadOnlyList<double> expectedTimes = LongitudinalCandidate.CreateKnotTimes(
                input.Configuration.Scheduling.TimeHorizonSeconds, input.Configuration.Scheduling.OutputTimeStepSeconds);
            double tolerance = RequireNonnegative(input.Configuration.Validation.KinematicTolerance, nameof(tolerance));
            if (!HasMatchingTimes(candidate.KnotTimes, expectedTimes, tolerance))
            {
                failureReason = "ST candidate knot times do not match the configured horizon.";
                return false;
            }
            if (candidate.S.Count != expectedTimes.Count || candidate.U.Count != expectedTimes.Count ||
                candidate.A.Count != expectedTimes.Count || candidate.J.Count != expectedTimes.Count - 1)
            {
                failureReason = "ST candidate value counts do not match its time knots.";
                return false;
            }
            if (!candidate.SatisfiesExactDiscreteDynamics(tolerance))
            {
                failureReason = "ST candidate violates exact constant-jerk dynamics.";
                return false;
            }
            if (!AreClose(candidate.S[0], 0d, tolerance) ||
                !AreClose(candidate.U[0], input.InitialProgressSpeedMetersPerSecond, tolerance) ||
                !AreClose(candidate.A[0], input.InitialAccelerationMetersPerSecondSquared, tolerance))
            {
                failureReason = "ST candidate does not satisfy the exact initial state.";
                return false;
            }

            for (int index = 0; index < candidate.S.Count; index++)
            {
                double progress = candidate.S[index];
                double speed = candidate.U[index];
                double acceleration = candidate.A[index];
                if (!IsFinite(progress) || !IsFinite(speed) || !IsFinite(acceleration) || progress < -tolerance ||
                    progress > input.TerminalPathS + tolerance || speed < -tolerance ||
                    acceleration < -maximumDeceleration - tolerance || acceleration > maximumAcceleration + tolerance)
                {
                    failureReason = "ST candidate violates physical bounds at knot " + index + ".";
                    return false;
                }
                double speedLimitAtProgress = speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.TerminalPathS, progress)));
                if (speed > speedLimitAtProgress + tolerance)
                {
                    failureReason = "ST candidate violates the actual-PathS speed envelope at knot " + index +
                        " (S=" + progress + ", U=" + speed + ", limit=" + speedLimitAtProgress + ").";
                    return false;
                }
                if (index > 0 && progress < candidate.S[index - 1] - tolerance)
                {
                    failureReason = "ST candidate PathS decreases at knot " + index + ".";
                    return false;
                }
            }
            for (int index = 0; index < candidate.J.Count; index++)
            {
                if (!IsFinite(candidate.J[index]) || Math.Abs(candidate.J[index]) > maximumJerk + tolerance)
                {
                    failureReason = "ST candidate violates the jerk bound at interval " + index + ".";
                    return false;
                }
            }
            int terminalIndex = candidate.S.Count - 1;
            if (!AreClose(candidate.S[terminalIndex], input.TerminalPathS, tolerance) ||
                !AreClose(candidate.U[terminalIndex], 0d, tolerance))
            {
                failureReason = "ST candidate does not satisfy the exact zero-speed terminal.";
                return false;
            }

            var canonicalS = new double[candidate.S.Count];
            var canonicalU = new double[candidate.U.Count];
            var canonicalA = new double[candidate.A.Count];
            for (int index = 0; index < candidate.S.Count; index++)
            {
                canonicalS[index] = candidate.S[index];
                canonicalU[index] = candidate.U[index];
                canonicalA[index] = candidate.A[index];
            }
            canonicalS[0] = 0d;
            canonicalU[0] = input.InitialProgressSpeedMetersPerSecond;
            canonicalA[0] = input.InitialAccelerationMetersPerSecondSquared;
            canonicalS[terminalIndex] = input.TerminalPathS;
            canonicalU[terminalIndex] = 0d;
            var canonicalCandidate = new LongitudinalCandidate(candidate.KnotTimes, canonicalS, canonicalU, canonicalA,
                candidate.J);
            if (!canonicalCandidate.SatisfiesExactDiscreteDynamics(tolerance))
            {
                failureReason = "Canonical ST hard-boundary values exceed the dynamics tolerance.";
                return false;
            }
            validatedCandidate = canonicalCandidate;
            return true;
        }
        catch (ArgumentException exception)
        {
            failureReason = exception.Message;
            return false;
        }
    }

    private static bool HasMatchingTimes(IReadOnlyList<double> actual, IReadOnlyList<double> expected, double tolerance)
    {
        if (actual.Count != expected.Count)
            return false;
        for (int index = 0; index < expected.Count; index++)
        {
            if (!AreClose(actual[index], expected[index], tolerance))
                return false;
        }
        return true;
    }

    private static bool AreClose(double actual, double expected, double tolerance)
    {
        return Math.Abs(actual - expected) <= tolerance;
    }

    private static double RequireNonnegative(double value, string parameterName)
    {
        if (!IsFinite(value) || value < 0d)
            throw new ArgumentOutOfRangeException(parameterName);
        return value;
    }

    private static bool IsFinite(double value)
    {
        return !double.IsNaN(value) && !double.IsInfinity(value);
    }
}
