using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;

namespace MultiWheelC.TrajectoryPlanning.EMPlanner;

internal sealed class TrajectorySampleSchedule
{
    private const double ZeroTolerance = 1e-12d;

    public TrajectorySampleSchedule(LongitudinalCandidate candidate, double outputTimeStepSeconds, double holdDurationSeconds,
        EmLongitudinalMode mode, bool resampleMotion, double pathSTolerance)
    {
        if (candidate == null)
            throw new ArgumentNullException(nameof(candidate));
        if (!IsFinite(outputTimeStepSeconds) || outputTimeStepSeconds <= 0d)
            throw new ArgumentOutOfRangeException(nameof(outputTimeStepSeconds));
        if (!IsFinite(holdDurationSeconds) || holdDurationSeconds < 0d)
            throw new ArgumentOutOfRangeException(nameof(holdDurationSeconds));
        if (!IsFinite(pathSTolerance) || pathSTolerance < 0d)
            throw new ArgumentOutOfRangeException(nameof(pathSTolerance));
        if (!Enum.IsDefined(typeof(EmLongitudinalMode), mode))
            throw new ArgumentOutOfRangeException(nameof(mode));

        var samples = new List<TrajectorySample>(candidate.KnotTimes.Count + 4);
        double previousPathS = double.NegativeInfinity;
        double terminalPathS = candidate.S[candidate.S.Count - 1];
        if (resampleMotion)
        {
            double finalTime = candidate.KnotTimes[candidate.KnotTimes.Count - 1];
            int sourceInterval = 0;
            for (double sampleTime = 0d; sampleTime < finalTime - ZeroTolerance;
                sampleTime += outputTimeStepSeconds)
            {
                AddSample(Interpolate(candidate, sampleTime, ref sourceInterval), samples.Count, samples,
                    ref previousPathS, terminalPathS, pathSTolerance, candidate);
            }
            AddSample(Interpolate(candidate, finalTime, ref sourceInterval), samples.Count, samples,
                ref previousPathS, terminalPathS, pathSTolerance, candidate);
        }
        else
        {
            for (int index = 0; index < candidate.KnotTimes.Count; index++)
            {
                AddSample(new TrajectorySample(candidate.KnotTimes[index], candidate.S[index],
                    Math.Max(0d, candidate.U[index]), candidate.A[index], index < candidate.J.Count ? candidate.J[index] : 0d,
                    false), samples.Count, samples, ref previousPathS, terminalPathS, pathSTolerance, candidate);
            }
        }

        if (mode != EmLongitudinalMode.ExactStopAtBoundary)
        {
            Samples = new ReadOnlyCollection<TrajectorySample>(samples);
            TerminalAnchorSampleIndex = -1;
            return;
        }

        int sourceStabilizationStart = resampleMotion
            ? FindTerminalStationaryTailStart(candidate)
            : LongitudinalTerminalSchedule.GetStabilizationStartIndex(candidate.KnotTimes, outputTimeStepSeconds);
        double stabilizationStartTime = candidate.KnotTimes[sourceStabilizationStart];
        int stabilizationStart = 0;
        while (stabilizationStart < samples.Count - 1 &&
            samples[stabilizationStart].TimeFromStart < stabilizationStartTime - ZeroTolerance)
        {
            stabilizationStart++;
        }
        double stopPathS = samples[stabilizationStart].PathS;
        for (int index = stabilizationStart; index < samples.Count; index++)
        {
            if (Math.Abs(samples[index].PathS - stopPathS) > ZeroTolerance ||
                Math.Abs(samples[index].ProgressSpeed) > ZeroTolerance || Math.Abs(samples[index].Acceleration) > ZeroTolerance ||
                Math.Abs(samples[index].Jerk) > ZeroTolerance)
            {
                throw new ArgumentException("An exact stop requires a stationary S/U/A/J tail.", nameof(candidate));
            }
        }

        TerminalAnchorSampleIndex = stabilizationStart;
        TrajectorySample terminal = samples[samples.Count - 1];
        double holdElapsed = 0d;
        while (holdElapsed < holdDurationSeconds - ZeroTolerance)
        {
            holdElapsed = Math.Min(holdDurationSeconds, holdElapsed + outputTimeStepSeconds);
            samples.Add(new TrajectorySample(terminal.TimeFromStart + holdElapsed, terminal.PathS, 0d, 0d, 0d, true));
        }

        Samples = new ReadOnlyCollection<TrajectorySample>(samples);
    }

    public IReadOnlyList<TrajectorySample> Samples { get; }
    public int TerminalAnchorSampleIndex { get; }

    internal static bool ExceedsMaximumSampleCount(LongitudinalCandidate candidate, double outputTimeStepSeconds,
        double holdDurationSeconds, EmLongitudinalMode mode, bool resampleMotion, int maximumSampleCount,
        out int requiredSampleCount)
    {
        requiredSampleCount = 0;
        if (candidate == null)
            throw new ArgumentNullException(nameof(candidate));
        if (maximumSampleCount < 1)
            throw new ArgumentOutOfRangeException(nameof(maximumSampleCount));

        if (resampleMotion)
        {
            double finalTime = candidate.KnotTimes[candidate.KnotTimes.Count - 1];
            for (double sampleTime = 0d; sampleTime < finalTime - ZeroTolerance;
                sampleTime += outputTimeStepSeconds)
            {
                if (IncrementAndExceedsMaximum(ref requiredSampleCount, maximumSampleCount))
                    return true;
            }
            if (IncrementAndExceedsMaximum(ref requiredSampleCount, maximumSampleCount))
                return true;
        }
        else
        {
            for (int index = 0; index < candidate.KnotTimes.Count; index++)
            {
                if (IncrementAndExceedsMaximum(ref requiredSampleCount, maximumSampleCount))
                    return true;
            }
        }

        if (mode != EmLongitudinalMode.ExactStopAtBoundary)
            return false;

        double holdElapsed = 0d;
        while (holdElapsed < holdDurationSeconds - ZeroTolerance)
        {
            holdElapsed = Math.Min(holdDurationSeconds, holdElapsed + outputTimeStepSeconds);
            if (IncrementAndExceedsMaximum(ref requiredSampleCount, maximumSampleCount))
                return true;
        }
        return false;
    }

    private static void AddSample(TrajectorySample sample, int sampleIndex, ICollection<TrajectorySample> samples,
        ref double previousPathS, double terminalPathS, double pathSTolerance, LongitudinalCandidate candidate)
    {
        double lowerDifference = previousPathS - sample.PathS;
        double upperDifference = sample.PathS - terminalPathS;
        if (lowerDifference > pathSTolerance || upperDifference > pathSTolerance)
        {
            throw PathSFailure(sampleIndex, previousPathS, sample.PathS,
                Math.Max(lowerDifference, upperDifference), candidate);
        }

        double normalizedPathS = Math.Min(terminalPathS, Math.Max(previousPathS, sample.PathS));
        if (normalizedPathS != sample.PathS)
        {
            sample = new TrajectorySample(sample.TimeFromStart, normalizedPathS, sample.ProgressSpeed,
                sample.Acceleration, sample.Jerk, sample.IsHoldSample);
        }
        if (sample.ProgressSpeed < -ZeroTolerance)
            throw new ArgumentException("Longitudinal progress speed cannot be negative.", nameof(candidate));
        samples.Add(sample);
        previousPathS = sample.PathS;
    }

    private static ArgumentException PathSFailure(int sampleIndex, double previousPathS,
        double candidatePathS, double difference, LongitudinalCandidate candidate)
    {
        return new ArgumentException("Trajectory PathS violates monotonic publication bounds: sampleIndex=" +
            sampleIndex.ToString(CultureInfo.InvariantCulture) + ";previousPathS=" +
            previousPathS.ToString("R", CultureInfo.InvariantCulture) + ";candidatePathS=" +
            candidatePathS.ToString("R", CultureInfo.InvariantCulture) + ";difference=" +
            difference.ToString("R", CultureInfo.InvariantCulture) + ".", nameof(candidate));
    }

    private static bool IncrementAndExceedsMaximum(ref int sampleCount, int maximumSampleCount)
    {
        checked { sampleCount++; }
        return sampleCount > maximumSampleCount;
    }

    private static TrajectorySample Interpolate(LongitudinalCandidate candidate, double sampleTime, ref int sourceInterval)
    {
        int lastKnot = candidate.KnotTimes.Count - 1;
        if (sampleTime >= candidate.KnotTimes[lastKnot] - ZeroTolerance)
        {
            return new TrajectorySample(candidate.KnotTimes[lastKnot], candidate.S[lastKnot],
                Math.Max(0d, candidate.U[lastKnot]), candidate.A[lastKnot], 0d, false);
        }
        while (sourceInterval < lastKnot - 1 &&
            sampleTime >= candidate.KnotTimes[sourceInterval + 1] - ZeroTolerance)
        {
            sourceInterval++;
        }
        if (Math.Abs(sampleTime - candidate.KnotTimes[sourceInterval]) <= ZeroTolerance)
        {
            return new TrajectorySample(candidate.KnotTimes[sourceInterval], candidate.S[sourceInterval],
                Math.Max(0d, candidate.U[sourceInterval]), candidate.A[sourceInterval], candidate.J[sourceInterval], false);
        }
        double dt = sampleTime - candidate.KnotTimes[sourceInterval];
        double jerk = candidate.J[sourceInterval];
        double acceleration = candidate.A[sourceInterval] + jerk * dt;
        double speed = candidate.U[sourceInterval] + candidate.A[sourceInterval] * dt + 0.5d * jerk * dt * dt;
        double pathS = candidate.S[sourceInterval] + candidate.U[sourceInterval] * dt +
            0.5d * candidate.A[sourceInterval] * dt * dt + jerk * dt * dt * dt / 6d;
        return new TrajectorySample(sampleTime, pathS, Math.Max(0d, speed), acceleration, jerk, false);
    }

    private static int FindTerminalStationaryTailStart(LongitudinalCandidate candidate)
    {
        int start = candidate.KnotTimes.Count - 1;
        double terminalPathS = candidate.S[start];
        while (start > 0 && Math.Abs(candidate.S[start - 1] - terminalPathS) <= ZeroTolerance &&
            Math.Abs(candidate.U[start - 1]) <= ZeroTolerance && Math.Abs(candidate.A[start - 1]) <= ZeroTolerance &&
            Math.Abs(candidate.J[start - 1]) <= ZeroTolerance)
        {
            start--;
        }
        return start;
    }

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

internal sealed class TrajectorySample
{
    public TrajectorySample(double timeFromStart, double pathS, double progressSpeed, double acceleration, double jerk,
        bool isHoldSample)
    {
        TimeFromStart = timeFromStart;
        PathS = pathS;
        ProgressSpeed = progressSpeed;
        Acceleration = acceleration;
        Jerk = jerk;
        IsHoldSample = isHoldSample;
    }

    public double TimeFromStart { get; }
    public double PathS { get; }
    public double ProgressSpeed { get; }
    public double Acceleration { get; }
    public double Jerk { get; }
    public bool IsHoldSample { get; }
}
