using System;
using MultiWheelC.Control.Abstractions;

namespace MultiWheelC.Control.Lateral
{
    /// <summary>
    /// 将参考曲率、横向误差和航向误差分别转换为前、后GCP目标转角。
    /// </summary>
    public sealed class StanleyLateralController : ILateralController
    {
        /// <summary>
        /// 创建使用指定GCP几何、Stanley增益和转角保护参数的横向控制器。
        /// </summary>
        public StanleyLateralController(
            double controlPointRadiusMeters,
            double crossTrackGainPerSecond,
            double headingErrorGain,
            double minimumSpeedMetersPerSecond,
            bool useActualSpeedForGain = true,
            double maximumCrossTrackCorrectionRadians =
                10.0 * Math.PI / 180.0,
            double maximumHeadingCorrectionRadians =
                10.0 * Math.PI / 180.0)
        {
            EnsureFinitePositive(
                controlPointRadiusMeters,
                nameof(controlPointRadiusMeters));
            EnsureFiniteNonNegative(
                crossTrackGainPerSecond,
                nameof(crossTrackGainPerSecond));
            EnsureFiniteNonNegative(
                headingErrorGain,
                nameof(headingErrorGain));
            EnsureFinitePositive(
                minimumSpeedMetersPerSecond,
                nameof(minimumSpeedMetersPerSecond));
            EnsureFinitePositive(
                maximumCrossTrackCorrectionRadians,
                nameof(maximumCrossTrackCorrectionRadians));
            EnsureFinitePositive(
                maximumHeadingCorrectionRadians,
                nameof(maximumHeadingCorrectionRadians));

            ControlPointRadiusMeters = controlPointRadiusMeters;
            CrossTrackGainPerSecond = crossTrackGainPerSecond;
            HeadingErrorGain = headingErrorGain;
            MinimumSpeedMetersPerSecond = minimumSpeedMetersPerSecond;
            UseActualSpeedForGain = useActualSpeedForGain;
            MaximumCrossTrackCorrectionRadians =
                maximumCrossTrackCorrectionRadians;
            MaximumHeadingCorrectionRadians =
                maximumHeadingCorrectionRadians;
        }

        /// <summary>
        /// 获取车体中心到前、后GCP的距离，单位为m。
        /// </summary>
        public double ControlPointRadiusMeters { get; }

        /// <summary>
        /// 获取横向误差增益，单位为1/s。
        /// </summary>
        public double CrossTrackGainPerSecond { get; }

        /// <summary>
        /// 获取航向误差的无量纲增益。
        /// </summary>
        public double HeadingErrorGain { get; }

        /// <summary>
        /// 获取Stanley分母使用的最小速度绝对值，单位为m/s。
        /// </summary>
        public double MinimumSpeedMetersPerSecond { get; }

        /// <summary>
        /// 获取是否优先使用当前状态源提供的实际纵向速度计算横向修正。
        /// </summary>
        public bool UseActualSpeedForGain { get; }

        /// <summary>
        /// 获取横向误差共同转角分量的最大绝对值，单位为rad。
        /// </summary>
        public double MaximumCrossTrackCorrectionRadians { get; }

        /// <summary>
        /// 获取航向误差差动转角分量的最大绝对值，单位为rad。
        /// </summary>
        public double MaximumHeadingCorrectionRadians { get; }

        /// <summary>
        /// 分别计算横向共同转角以及曲率和航向差动转角，并生成前后GCP命令。
        /// </summary>
        public LateralControlCommand Compute(
            PathTrackingContext context)
        {
            var speedForGain = SelectSpeedForGain(context);
            var speedMagnitude = Math.Max(
                Math.Abs(speedForGain),
                MinimumSpeedMetersPerSecond);
            var travelDirection = SelectTravelDirection(context);

            // 参考曲率按轨迹点序的实际行进方向定义；倒车时底盘有符号
            // 纵向速度反向，因此GCP曲率前馈也必须反向才能保持相同几何曲率。
            var feedforwardAngleRadians =
                travelDirection *
                Math.Atan(
                    context.FeedforwardCurvaturePerMeter *
                    ControlPointRadiusMeters);

            // 横向误差生成前后同向的共同转角，使四舵轮车辆平稳靠近轨迹。
            var crossTrackCorrectionRadians =
                ClampSymmetric(
                    Math.Atan(
                        CrossTrackGainPerSecond *
                        context.LateralErrorMeters /
                        speedMagnitude),
                    MaximumCrossTrackCorrectionRadians);

            // 航向误差生成前后反向的差动转角，只负责调整车身朝向。
            var headingCorrectionRadians =
                ClampSymmetric(
                    HeadingErrorGain *
                    context.HeadingErrorRadians,
                    MaximumHeadingCorrectionRadians);

            // 横向误差已经按轨迹执行点序定义；倒车轨迹的点序会自然
            // 翻转横向轴，因此共同转角不能再按行驶方向重复反号。
            var commonAngleRadians =
                crossTrackCorrectionRadians;
            var differentialAngleRadians =
                feedforwardAngleRadians +
                travelDirection *
                headingCorrectionRadians;

            return new LateralControlCommand(
                commonAngleRadians +
                differentialAngleRadians,
                commonAngleRadians -
                differentialAngleRadians);
        }

        /// <summary>
        /// 清除横向控制器状态；当前Stanley实现没有跨周期状态。
        /// </summary>
        public void Reset()
        {
        }

        /// <summary>
        /// 选择Stanley横向误差项使用的实际速度或参考速度。
        /// </summary>
        private double SelectSpeedForGain(
            PathTrackingContext context)
        {
            if (UseActualSpeedForGain &&
                context.HasValidVelocityEstimate)
            {
                return context
                    .ActualLongitudinalSpeedMetersPerSecond;
            }

            return context.ControlReferenceSpeedMetersPerSecond;
        }

        /// <summary>
        /// 根据有符号参考速度确定前进或倒车时的反馈修正方向。
        /// </summary>
        private static double SelectTravelDirection(
            PathTrackingContext context)
        {
            const double directionDeadbandMetersPerSecond = 1e-6;

            if (Math.Abs(context.ControlReferenceSpeedMetersPerSecond) >
                directionDeadbandMetersPerSecond)
            {
                return Math.Sign(
                    context.ControlReferenceSpeedMetersPerSecond);
            }

            if (context.HasValidVelocityEstimate &&
                Math.Abs(
                    context.ActualLongitudinalSpeedMetersPerSecond) >
                directionDeadbandMetersPerSecond)
            {
                return Math.Sign(
                    context.ActualLongitudinalSpeedMetersPerSecond);
            }

            return 1.0;
        }

        /// <summary>
        /// 将数值按正负对称方式限制在指定绝对值内。
        /// </summary>
        private static double ClampSymmetric(
            double value,
            double maximumAbsoluteValue)
        {
            return Math.Max(
                -maximumAbsoluteValue,
                Math.Min(maximumAbsoluteValue, value));
        }

        /// <summary>
        /// 检查控制参数是否为正有限值。
        /// </summary>
        private static void EnsureFinitePositive(
            double value,
            string parameterName)
        {
            EnsureFinite(value, parameterName);

            if (value <= 0.0)
            {
                throw new ArgumentOutOfRangeException(
                    parameterName,
                    "Stanley控制器的几何尺寸、速度和角度限制必须是正有限值。");
            }
        }

        /// <summary>
        /// 检查控制增益是否为非负有限值。
        /// </summary>
        private static void EnsureFiniteNonNegative(
            double value,
            string parameterName)
        {
            EnsureFinite(value, parameterName);

            if (value < 0.0)
            {
                throw new ArgumentOutOfRangeException(
                    parameterName,
                    "Stanley控制增益必须是非负有限值。");
            }
        }

        /// <summary>
        /// 检查控制参数是否为有限值。
        /// </summary>
        private static void EnsureFinite(
            double value,
            string parameterName)
        {
            if (double.IsNaN(value) ||
                double.IsInfinity(value))
            {
                throw new ArgumentOutOfRangeException(
                    parameterName,
                    "Stanley控制参数必须是有限值。");
            }
        }
    }
}
