using System;
using System.Collections.Generic;
using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade;
using MultiWheelC.TrajectoryPlanning.Mapping;

namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test;

/// <summary>Clumsy 粗路径手动测试可选择的固定场景。</summary>
public enum CoarsePathTestScenario
{
    /// <summary>明确允许的空地图直达场景。</summary>
    ExplicitEmpty,

    /// <summary>由中央矩形阻断直线的绕行场景。</summary>
    RectangleDetour,

    /// <summary>同时包含手工圆形、矩形与 TwoLeg 快照的多来源场景。</summary>
    ManualAndTwoLeg,

    /// <summary>与矩形绕行输入完全一致，用于在同一服务中验证输入缓存命中。</summary>
    CacheHit,

    /// <summary>起步前进、终点倒车进入的换向场景。</summary>
    ReverseGearSwitch,

    /// <summary>由贯穿边界的障碍带分隔起终点的无解场景。</summary>
    NoFeasiblePath,
}

/// <summary>手动障碍物输入支持的世界几何类型。</summary>
public enum ManualCoarsePathObstacleKind
{
    /// <summary>由世界中心和半径定义的圆形障碍物。</summary>
    Circle,

    /// <summary>由世界中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。</summary>
    AxisAlignedRectangle,
}

/// <summary>
/// 手动粗路径测试的不可变障碍物输入。
/// 所有中心和尺寸均使用世界 mm；矩形始终与世界坐标轴平行，不包含旋转角。
/// </summary>
public sealed class ManualCoarsePathObstacle
{
    private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters,
        double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters)
    {
        Kind = kind;
        CenterXMillimeters = centerXMillimeters;
        CenterYMillimeters = centerYMillimeters;
        SizeXMillimeters = sizeXMillimeters;
        SizeYMillimeters = sizeYMillimeters;
    }

    /// <summary>障碍物的支持几何类型。</summary>
    public ManualCoarsePathObstacleKind Kind { get; }

    /// <summary>几何中心世界 X 坐标，单位 mm。</summary>
    public double CenterXMillimeters { get; }

    /// <summary>几何中心世界 Y 坐标，单位 mm。</summary>
    public double CenterYMillimeters { get; }

    /// <summary>圆形时为半径，矩形时为 X 方向长度；单位 mm。</summary>
    public double SizeXMillimeters { get; }

    /// <summary>圆形时为半径，矩形时为 Y 方向宽度；单位 mm。</summary>
    public double SizeYMillimeters { get; }

    /// <summary>创建圆形障碍物。参数：圆心和半径均使用世界 mm，半径必须为有限正数。</summary>
    public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters,
        double radiusMillimeters)
    {
        EnsureFinite(centerXMillimeters, nameof(centerXMillimeters));
        EnsureFinite(centerYMillimeters, nameof(centerYMillimeters));
        EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters));
        return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters,
            centerYMillimeters, radiusMillimeters, radiusMillimeters);
    }

    /// <summary>
    /// 创建轴对齐矩形障碍物。
    /// 参数：中心、X 方向长度和 Y 方向宽度均使用世界 mm；两个尺寸必须为有限正数。
    /// </summary>
    public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters,
        double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters)
    {
        EnsureFinite(centerXMillimeters, nameof(centerXMillimeters));
        EnsureFinite(centerYMillimeters, nameof(centerYMillimeters));
        EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters));
        EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters));
        return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle,
            centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters);
    }

    private static void EnsureFinite(double value, string parameterName)
    {
        if (double.IsNaN(value) || double.IsInfinity(value))
            throw new ArgumentOutOfRangeException(parameterName, "Value must be finite.");
    }

    private static void EnsurePositiveFinite(double value, string parameterName)
    {
        EnsureFinite(value, parameterName);
        if (value <= 0d)
            throw new ArgumentOutOfRangeException(parameterName, "Value must be positive.");
    }
}

/// <summary>
/// Clumsy 粗路径测试的纯输入工厂。
/// 固定场景不读取 UI、传感器、定位或时钟；传入 AMR 位姿的手动入口仅在此处完成世界 mm/deg 到核心 m/rad 的转换。
/// </summary>
public static class CoarsePathScenarioFactory
{
    private const float MapXMinMillimeters = 0f;
    private const float MapXMaxMillimeters = 6000f;
    private const float MapYMinMillimeters = 0f;
    private const float MapYMaxMillimeters = 4000f;
    private const float ResolutionMillimeters = 50f;
    private const double MillimetersPerMeter = 1000d;
    private const double DegreesToRadians = Math.PI / 180d;
    private const double ManualMapPaddingMillimeters = 8000d;
    private const int MaximumManualObstacleCount = 20;

    /// <summary>
    /// 创建一个新的固定测试业务请求。
    /// 返回：每次调用都返回独立的可变请求对象，供调用方安全地传入同一个长期存活的规划服务。
    /// </summary>
    public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario)
    {
        return CreateCore(scenario, null);
    }

    /// <summary>
    /// 创建以当前 AMR 世界位姿为起点的固定测试业务请求。
    /// 参数：X/Y 使用世界 mm，航向使用 deg；地图、目标和障碍物仅随 AMR 坐标平移，TwoLeg 朝向保持不变。
    /// </summary>
    public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario,
        double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)
    {
        return CreateCore(scenario,
            new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees));
    }

    private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, FixedScenarioAnchor anchor)
    {
        switch (scenario)
        {
            case CoarsePathTestScenario.ExplicitEmpty:
                return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
            case CoarsePathTestScenario.RectangleDetour:
            case CoarsePathTestScenario.CacheHit:
                return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
            case CoarsePathTestScenario.ManualAndTwoLeg:
                return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor));
            case CoarsePathTestScenario.ReverseGearSwitch:
                return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
            case CoarsePathTestScenario.NoFeasiblePath:
                return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
            default:
                throw new ArgumentOutOfRangeException(nameof(scenario));
        }
    }

    /// <summary>
    /// 创建传入 AMR 世界位姿和手动世界终点的空图演示请求。
    /// 参数：X/Y 使用世界 mm，航向使用 deg；返回请求中的 <see cref="Pose2D"/> 使用世界 m/rad。
    /// 注意：这是坐标、路径和取消流程的演示空图，不能表示现场不存在障碍物。
    /// </summary>
    public static CoarsePathPlanningJob CreateManualGoalDemo(
        double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
        double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees)
    {
        return CreateManualObstacleDemo(startXMillimeters, startYMillimeters, startHeadingDegrees,
            goalXMillimeters, goalYMillimeters, goalHeadingDegrees,
            Array.Empty<ManualCoarsePathObstacle>(), 0L);
    }

    /// <summary>
    /// 创建传入 AMR 世界位姿、手动世界终点和手动障碍物快照的测试请求。
    /// 参数：位姿 X/Y、障碍物中心和尺寸使用世界 mm，航向使用 deg；返回的 <see cref="Pose2D"/> 使用 m/rad。
    /// 障碍物非空时 obstacleSnapshotVersion 必须为正数，以避免长期服务错误复用旧地图；零障碍物才创建显式空图演示。
    /// </summary>
    public static CoarsePathPlanningJob CreateManualObstacleDemo(
        double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
        double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees,
        IReadOnlyList<ManualCoarsePathObstacle> obstacles, long obstacleSnapshotVersion)
    {
        EnsureFinite(startXMillimeters, nameof(startXMillimeters));
        EnsureFinite(startYMillimeters, nameof(startYMillimeters));
        EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees));
        EnsureFinite(goalXMillimeters, nameof(goalXMillimeters));
        EnsureFinite(goalYMillimeters, nameof(goalYMillimeters));
        EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees));

        if (obstacles == null) throw new ArgumentNullException(nameof(obstacles));
        if (obstacles.Count > MaximumManualObstacleCount)
            throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle count exceeds the supported limit.");
        if (obstacles.Count != 0 && obstacleSnapshotVersion <= 0L)
            throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion), "Obstacle snapshots require a positive version.");

        return CreateJob(
            CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters,
                obstacles, obstacleSnapshotVersion),
            ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees),
            ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees),
            null,
            GoalDirectionConstraint.Any);
    }

    private static CoarsePathPlanningJob CreateExplicitEmpty(FixedScenarioTransform transform)
    {
        return CreateJob(
            CreateMap(true, Array.Empty<IMapObstacleSource>(), transform),
            transform.Pose(1d, 2d, 0d),
            transform.Pose(5d, 2d, 0d),
            null,
            GoalDirectionConstraint.Forward);
    }

    private static CoarsePathPlanningJob CreateRectangleDetour(FixedScenarioTransform transform)
    {
        IMapObstacleSource[] sources =
        {
            new ManualObstacleSource("manual", 1L, true, new IMapObstacle[]
            {
                new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f),
                    transform.Y(1200f), transform.Y(2800f)),
            }),
        };
        CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d),
            transform.Pose(5d, 2d, 0d), null, GoalDirectionConstraint.Forward);
        // 固定绕行场景保留最优启发式；30 秒覆盖较慢测试环境，UI 仍可随时取消。
        job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d);
        return job;
    }

    private static CoarsePathPlanningJob CreateManualAndTwoLeg(FixedScenarioTransform transform)
    {
        IMapObstacleSource[] sources =
        {
            new ManualObstacleSource("manual", 2L, true, new IMapObstacle[]
            {
                new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f),
                new AxisAlignedRectangleObstacle(transform.X(3000f), transform.X(3600f),
                    transform.Y(2000f), transform.Y(2600f)),
            }),
            new TwoLegObstacleSource("two-leg", 1L, true, new TwoLegProjectionInput(true,
                transform.X(3900f), transform.Y(2500f), 0d,
                    -180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")),
        };
        CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 1d, 0d),
            transform.Pose(5d, 3d, 0d), null, GoalDirectionConstraint.Forward);
        // 多来源场景的最优绕行会受机器负载影响；放宽演示总预算但保留全部碰撞与目标判定。
        job.Configuration.SearchTimeout = TimeSpan.FromSeconds(15d);
        return job;
    }

    private static CoarsePathPlanningJob CreateReverseGearSwitch(FixedScenarioTransform transform)
    {
        return CreateJob(
            CreateMap(true, Array.Empty<IMapObstacleSource>(), transform),
            transform.Pose(1d, 2d, 0d),
            transform.Pose(4d, 2d, 0d),
            TravelDirection.Forward,
            GoalDirectionConstraint.Reverse);
    }

    private static CoarsePathPlanningJob CreateNoFeasiblePath(FixedScenarioTransform transform)
    {
        IMapObstacleSource[] sources =
        {
            new ManualObstacleSource("manual", 3L, true, new IMapObstacle[]
            {
                new AxisAlignedRectangleObstacle(transform.X(2900f), transform.X(3100f),
                    transform.Y(0f), transform.Y(4000f)),
            }),
        };
        return CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d),
            transform.Pose(5d, 2d, 0d),
            null, GoalDirectionConstraint.Forward);
    }

    private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal,
        TravelDirection? startDirection, GoalDirectionConstraint goalDirection)
    {
        return new CoarsePathPlanningJob
        {
            MapRequest = mapRequest,
            Start = start,
            Goal = goal,
            Vehicle = new VehicleParameters
            {
                LengthMeters = 0.80d,
                WidthMeters = 0.60d,
                SafetyMarginMeters = 0.05d,
                MaximumCurvaturePerMeter = 1d / 1.20d,
            },
            Configuration = new HybridAStarConfiguration(),
            StartDirection = startDirection,
            GoalDirection = goalDirection,
        };
    }

    private static PlanningMapRequest CreateMap(bool allowExplicitEmptyMap, IReadOnlyList<IMapObstacleSource> sources,
        FixedScenarioTransform transform)
    {
        return new PlanningMapRequest
        {
            Bounds = new MapBoundsMm(transform.X(MapXMinMillimeters), transform.X(MapXMaxMillimeters),
                transform.Y(MapYMinMillimeters), transform.Y(MapYMaxMillimeters)),
            ResolutionMm = ResolutionMillimeters,
            ObstacleSources = sources,
            AllowExplicitEmptyMap = allowExplicitEmptyMap,
        };
    }

    private sealed class FixedScenarioAnchor
    {
        public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees)
        {
            EnsureFinite(xMillimeters, nameof(xMillimeters));
            EnsureFinite(yMillimeters, nameof(yMillimeters));
            EnsureFinite(headingDegrees, nameof(headingDegrees));
            XMillimeters = xMillimeters;
            YMillimeters = yMillimeters;
            HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians);
        }

        public double XMillimeters { get; }
        public double YMillimeters { get; }
        public double HeadingRadians { get; }
    }

    private sealed class FixedScenarioTransform
    {
        private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians)
        {
            DeltaXMillimeters = deltaXMillimeters;
            DeltaYMillimeters = deltaYMillimeters;
            HeadingDeltaRadians = headingDeltaRadians;
        }

        public double DeltaXMillimeters { get; }
        public double DeltaYMillimeters { get; }
        public double HeadingDeltaRadians { get; }

        public static FixedScenarioTransform From(double baselineStartXMillimeters,
            double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor)
        {
            if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d);
            return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters,
                anchor.YMillimeters - baselineStartYMillimeters,
                NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians));
        }

        public float X(float value)
        {
            return ToFiniteFloat(value + DeltaXMillimeters, nameof(value));
        }

        public float Y(float value)
        {
            return ToFiniteFloat(value + DeltaYMillimeters, nameof(value));
        }

        public Pose2D Pose(double xMeters, double yMeters, double headingRadians)
        {
            return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter,
                (yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter,
                NormalizeRadians(headingRadians + HeadingDeltaRadians));
        }
    }

    private static PlanningMapRequest CreateManualDemoMap(double startXMillimeters, double startYMillimeters,
        double goalXMillimeters, double goalYMillimeters, IReadOnlyList<ManualCoarsePathObstacle> obstacles,
        long obstacleSnapshotVersion)
    {
        double minimumX = Math.Min(startXMillimeters, goalXMillimeters);
        double maximumX = Math.Max(startXMillimeters, goalXMillimeters);
        double minimumY = Math.Min(startYMillimeters, goalYMillimeters);
        double maximumY = Math.Max(startYMillimeters, goalYMillimeters);
        for (int index = 0; index < obstacles.Count; index++)
        {
            ManualCoarsePathObstacle obstacle = obstacles[index] ??
                throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles));
            double halfX;
            double halfY;
            switch (obstacle.Kind)
            {
                case ManualCoarsePathObstacleKind.Circle:
                    EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles));
                    halfX = obstacle.SizeXMillimeters;
                    halfY = obstacle.SizeYMillimeters;
                    break;
                case ManualCoarsePathObstacleKind.AxisAlignedRectangle:
                    EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles));
                    EnsurePositiveFinite(obstacle.SizeYMillimeters, nameof(obstacles));
                    halfX = obstacle.SizeXMillimeters / 2d;
                    halfY = obstacle.SizeYMillimeters / 2d;
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported.");
            }

            minimumX = Math.Min(minimumX, obstacle.CenterXMillimeters - halfX);
            maximumX = Math.Max(maximumX, obstacle.CenterXMillimeters + halfX);
            minimumY = Math.Min(minimumY, obstacle.CenterYMillimeters - halfY);
            maximumY = Math.Max(maximumY, obstacle.CenterYMillimeters + halfY);
        }

        float xMin = ToGridLowerBound(minimumX - ManualMapPaddingMillimeters);
        float xMax = ToGridUpperBound(maximumX + ManualMapPaddingMillimeters);
        float yMin = ToGridLowerBound(minimumY - ManualMapPaddingMillimeters);
        float yMax = ToGridUpperBound(maximumY + ManualMapPaddingMillimeters);
        bool isExplicitEmptyMap = obstacles.Count == 0;
        return new PlanningMapRequest
        {
            Bounds = new MapBoundsMm(xMin, xMax, yMin, yMax),
            ResolutionMm = ResolutionMillimeters,
            ObstacleSources = isExplicitEmptyMap ? Array.Empty<IMapObstacleSource>() :
                new IMapObstacleSource[]
                {
                    new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true,
                        ConvertManualObstacles(obstacles)),
                },
            AllowExplicitEmptyMap = isExplicitEmptyMap,
        };
    }

    private static IMapObstacle[] ConvertManualObstacles(IReadOnlyList<ManualCoarsePathObstacle> obstacles)
    {
        var result = new IMapObstacle[obstacles.Count];
        for (int index = 0; index < obstacles.Count; index++)
        {
            ManualCoarsePathObstacle obstacle = obstacles[index] ??
                throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles));
            switch (obstacle.Kind)
            {
                case ManualCoarsePathObstacleKind.Circle:
                    result[index] = new CircleObstacle(ToFiniteFloat(obstacle.CenterXMillimeters, nameof(obstacles)),
                        ToFiniteFloat(obstacle.CenterYMillimeters, nameof(obstacles)),
                        ToFiniteFloat(obstacle.SizeXMillimeters, nameof(obstacles)));
                    break;
                case ManualCoarsePathObstacleKind.AxisAlignedRectangle:
                    double halfX = obstacle.SizeXMillimeters / 2d;
                    double halfY = obstacle.SizeYMillimeters / 2d;
                    result[index] = new AxisAlignedRectangleObstacle(
                        ToFiniteFloat(obstacle.CenterXMillimeters - halfX, nameof(obstacles)),
                        ToFiniteFloat(obstacle.CenterXMillimeters + halfX, nameof(obstacles)),
                        ToFiniteFloat(obstacle.CenterYMillimeters - halfY, nameof(obstacles)),
                        ToFiniteFloat(obstacle.CenterYMillimeters + halfY, nameof(obstacles)));
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported.");
            }
        }
        return result;
    }

    private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees)
    {
        return new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter,
            headingDegrees * DegreesToRadians);
    }

    private static double NormalizeRadians(double angle)
    {
        double normalized = angle % (2d * Math.PI);
        if (normalized <= -Math.PI) return normalized + 2d * Math.PI;
        return normalized > Math.PI ? normalized - 2d * Math.PI : normalized;
    }

    private static float ToGridLowerBound(double millimeters)
    {
        double rounded = Math.Floor(millimeters / ResolutionMillimeters) * ResolutionMillimeters;
        return ToFiniteFloat(rounded, nameof(millimeters));
    }

    private static float ToGridUpperBound(double millimeters)
    {
        double rounded = Math.Ceiling(millimeters / ResolutionMillimeters) * ResolutionMillimeters;
        return ToFiniteFloat(rounded, nameof(millimeters));
    }

    private static float ToFiniteFloat(double value, string parameterName)
    {
        if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue)
            throw new ArgumentOutOfRangeException(parameterName, "Value cannot be represented as a finite millimeter coordinate.");
        return (float)value;
    }

    private static void EnsureFinite(double value, string parameterName)
    {
        if (double.IsNaN(value) || double.IsInfinity(value))
            throw new ArgumentOutOfRangeException(parameterName, "Value must be finite.");
    }

    private static void EnsurePositiveFinite(double value, string parameterName)
    {
        EnsureFinite(value, parameterName);
        if (value <= 0d)
            throw new ArgumentOutOfRangeException(parameterName, "Value must be positive.");
    }
}
