using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle;
using MultiWheelC.TrajectoryPlanning.Mapping;
using MultiWheelC.TrajectoryPlanning.Utils;

namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Search;

/// <summary>
/// 单次 Hybrid A* 搜索的只读结果。
/// 即使失败也会保留已经创建的运行期节点，供上层记录诊断；仅 <see cref="PlanningStatus.Success"/> 时 <see cref="SuccessNodeIndex"/> 有值。
/// </summary>
public sealed class HybridAStarSearchResult
{
    internal HybridAStarSearchResult(
        PlanningStatus status,
        IEnumerable<HybridAStarNode> nodes,
        int expandedNodeCount,
        int generatedNodeCount,
        int reopenedNodeCount,
        int staleOpenListEntryCount,
        int peakOpenListCount,
        int? successNodeIndex,
        string terminationReason)
    {
        Status = status;
        Nodes = new ReadOnlyCollection<HybridAStarNode>(new List<HybridAStarNode>(nodes ?? Array.Empty<HybridAStarNode>()));
        ExpandedNodeCount = expandedNodeCount;
        GeneratedNodeCount = generatedNodeCount;
        ReopenedNodeCount = reopenedNodeCount;
        StaleOpenListEntryCount = staleOpenListEntryCount;
        PeakOpenListCount = peakOpenListCount;
        SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null;
        TerminationReason = status == PlanningStatus.Success ? string.Empty : terminationReason ?? string.Empty;
    }

    /// <summary>搜索终止状态。</summary>
    public PlanningStatus Status { get; }

    /// <summary>本次搜索已经创建的全部节点；索引与 <see cref="HybridAStarNode.NodeIndex"/> 一致。</summary>
    public IReadOnlyList<HybridAStarNode> Nodes { get; }

    /// <summary>实际从 Open List 弹出并扩展的节点数量。</summary>
    public int ExpandedNodeCount { get; }

    /// <summary>已进入 Open List 的节点数量，包含根节点和因改进代价追加的节点。</summary>
    public int GeneratedNodeCount { get; }

    /// <summary>更优路径到达已关闭离散状态、并重新放回 Open List 的次数。</summary>
    public int ReopenedNodeCount { get; }

    /// <summary>从 Open List 弹出后因已有更优普通状态而被丢弃的陈旧条目数量。</summary>
    public int StaleOpenListEntryCount { get; }

    /// <summary>搜索期间 Open List 持有的最大条目数，包含等待惰性丢弃的旧条目。</summary>
    public int PeakOpenListCount { get; }

    /// <summary>成功时最后一个从 Open List 弹出且满足终点条件的节点索引；失败时为 null。</summary>
    public int? SuccessNodeIndex { get; }

    /// <summary>搜索边界记录的原始终止原因；成功时为空字符串，失败时非空。</summary>
    public string TerminationReason { get; }
}

/// <summary>
/// 在不可变 <see cref="PlanningGridMap"/> 上执行前进/倒车恒曲率原语的 Hybrid A* 搜索。
/// 搜索只消费已准备好的地图快照；所有候选原语均先完成连续扩大车体碰撞检查，再参与 Open List 排序。
/// </summary>
public sealed class HybridAStarSearch
{
    private const double CostImprovementToleranceMeters = 1e-9d;
    private readonly MotionPrimitiveGenerator _primitiveGenerator;
    private readonly SearchCostCalculator _costCalculator;
    private readonly FootprintCollisionChecker _collisionChecker;

    /// <summary>创建使用默认原语、代价和碰撞检查实现的搜索器。</summary>
    public HybridAStarSearch()
        : this(new MotionPrimitiveGenerator(), new SearchCostCalculator(), new FootprintCollisionChecker())
    {
    }

    /// <summary>创建使用指定协作对象的搜索器，便于在不引入地图或 UI 依赖的情况下测试搜索过程。</summary>
    public HybridAStarSearch(
        MotionPrimitiveGenerator primitiveGenerator,
        SearchCostCalculator costCalculator,
        FootprintCollisionChecker collisionChecker)
    {
        _primitiveGenerator = primitiveGenerator ?? throw new ArgumentNullException(nameof(primitiveGenerator));
        _costCalculator = costCalculator ?? throw new ArgumentNullException(nameof(costCalculator));
        _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker));
    }

    /// <summary>
    /// 执行一次不可变地图上的 Hybrid A* 搜索。
    /// 参数：request 提供地图、起终点、车辆和搜索配置；cancellationToken 在每次节点扩展前检查。
    /// 返回：成功仅在满足目标约束的候选已从 Open List 弹出时报告；任一失败状态均不报告成功节点。
    /// </summary>
    public HybridAStarSearchResult Search(PlanningRequest request, CancellationToken cancellationToken)
    {
        TimeSpan timeout = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero
            ? request.Configuration.SearchTimeout
            : TimeSpan.Zero;
        PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero
            ? new PlanningOperationBudget(cancellationToken, timeout)
            : PlanningOperationBudget.Unlimited(cancellationToken);
        return Search(request, budget);
    }

    /// <summary>使用门面传入的共享预算执行搜索；预算从整次规划开始计时。</summary>
    internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget)
    {
        var nodes = new List<HybridAStarNode>();
        int expandedNodeCount = 0;
        int generatedNodeCount = 0;
        int reopenedNodeCount = 0;
        int staleOpenListEntryCount = 0;
        int peakOpenListCount = 0;

        try
        {
            if (budget == null) throw new ArgumentNullException(nameof(budget));
            PlanningOperationStopReason stopReason = budget.GetStopReason();
            if (stopReason != PlanningOperationStopReason.None)
                return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            PlanningStatus validationStatus = ValidateRequest(request);
            if (validationStatus != PlanningStatus.Success)
                return CreateResult(validationStatus, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            PlanningGridMap map = request.Map;
            HybridAStarConfiguration configuration = request.Configuration;
            VehicleParameters vehicle = request.Vehicle;

            if (!IsFootprintInsideMap(request.Start, map, vehicle))
                return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);
            if (!_collisionChecker.IsPoseCollisionFree(request.Start, map, vehicle, 0d, out _))
                return CreateResult(PlanningStatus.StartInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);
            if (!IsFootprintInsideMap(request.Goal, map, vehicle))
                return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);
            if (!_collisionChecker.IsPoseCollisionFree(request.Goal, map, vehicle, 0d, out _))
                return CreateResult(PlanningStatus.GoalInCollision, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            stopReason = budget.GetStopReason();
            if (stopReason != PlanningOperationStopReason.None)
                return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);
            if (configuration.MaximumExpandedNodes == 0)
                return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            if (!map.TryWorldToGrid(request.Goal.X, request.Goal.Y, out int goalRow, out int goalColumn))
                return CreateResult(PlanningStatus.GoalOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);
            if (!GridDijkstraHeuristic.TryCreate(map, goalRow, goalColumn, budget, out GridDijkstraHeuristic heuristic, out stopReason))
                return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);
            if (!VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter))
                return CreateResult(PlanningStatus.InvalidVehicleParameters, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            IReadOnlyList<double> curvatureLevels = _primitiveGenerator.GetCurvatureLevels(vehicle, configuration);
            if (curvatureLevels.Count == 0)
                return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            int headingBinCount = GetHeadingBinCount(configuration.HeadingResolutionRadians);
            int startCurvatureLevelIndex = GetNearestCurvatureLevelIndex(curvatureLevels, request.StartVehicleCurvature);
            if (startCurvatureLevelIndex < 0)
                return CreateResult(PlanningStatus.InvalidCurvatureConfiguration, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            if (!map.TryWorldToGrid(request.Start.X, request.Start.Y, out int startRow, out int startColumn))
                return CreateResult(PlanningStatus.StartOutsideMap, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null);

            var openList = new BinaryMinHeap<int>();
            var bestStates = new Dictionary<HybridAStarNodeKey, NodeLabel>();
            foreach (TravelDirection startDirection in GetStartDirections(request, configuration))
            {
                int headingIndex = AngleMath.ToHeadingIndex(request.Start.Heading, configuration.HeadingResolutionRadians, headingBinCount);
                var key = new HybridAStarNodeKey(startRow, startColumn, headingIndex, startDirection, startCurvatureLevelIndex);
                if (bestStates.ContainsKey(key)) continue;

                double hCostMeters = GetHeuristicCost(heuristic, startRow, startColumn, configuration);
                if (double.IsPositiveInfinity(hCostMeters)) continue;
                var node = new HybridAStarNode(nodes.Count, key, request.Start, -1, null,
                    curvatureLevels[startCurvatureLevelIndex], 0d, hCostMeters);
                nodes.Add(node);
                bestStates.Add(key, new NodeLabel(node.NodeIndex, 0d, false));
                openList.Push(node.NodeIndex, node.FCostMeters, node.HCostMeters, node.GCostMeters);
                peakOpenListCount = Math.Max(peakOpenListCount, openList.Count);
                generatedNodeCount++;
            }

            if (openList.Count == 0)
                return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                    staleOpenListEntryCount, peakOpenListCount, null,
                    "二维启发式标记起点不可达目标，或起始方向无法进入 Open List。");

            while (openList.Count > 0)
            {
                stopReason = budget.GetStopReason();
                if (stopReason != PlanningOperationStopReason.None)
                    return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                        staleOpenListEntryCount, peakOpenListCount, null);
                if (expandedNodeCount >= configuration.MaximumExpandedNodes)
                    return CreateResult(PlanningStatus.SearchNodeLimitExceeded, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                        staleOpenListEntryCount, peakOpenListCount, null);

                int nodeIndex = openList.Pop();
                HybridAStarNode current = nodes[nodeIndex];
                NodeLabel currentLabel = null;
                if (!current.IsGoalCandidate)
                {
                    if (!bestStates.TryGetValue(current.Key, out currentLabel) || currentLabel.NodeIndex != nodeIndex || currentLabel.IsClosed)
                    {
                        staleOpenListEntryCount++;
                        continue;
                    }

                    currentLabel.IsClosed = true;
                }

                expandedNodeCount++;
                if (current.IsGoalCandidate)
                {
                    if (!GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection) ||
                        !_collisionChecker.IsPoseCollisionFree(current.Pose, map, vehicle, 0d, out _))
                        continue;

                    return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                        staleOpenListEntryCount, peakOpenListCount, current.NodeIndex);
                }

                if (GoalToleranceChecker.IsSatisfied(current.Pose, request.Goal, configuration, current.Direction, request.GoalDirection))
                    return CreateResult(PlanningStatus.Success, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                        staleOpenListEntryCount, peakOpenListCount, current.NodeIndex);

                for (int curvatureLevelIndex = 0; curvatureLevelIndex < curvatureLevels.Count; curvatureLevelIndex++)
                {
                    stopReason = budget.GetStopReason();
                    if (stopReason != PlanningOperationStopReason.None)
                        return CreateResult(ToPlanningStatus(stopReason), nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                            staleOpenListEntryCount, peakOpenListCount, null);
                    if (!MotionPrimitiveGenerator.AreCurvatureLevelsAdjacent(current.CurvatureLevelIndex, curvatureLevelIndex)) continue;

                    foreach (TravelDirection direction in GetSuccessorDirections(configuration))
                    {
                        MotionPrimitive primitive = _primitiveGenerator.Generate(current.Pose, curvatureLevels[curvatureLevelIndex], direction, request);
                        if (primitive == null || primitive.ActualLengthMeters <= 0d) continue;

                        if (!map.TryWorldToGrid(primitive.End.X, primitive.End.Y, out int row, out int column)) continue;
                        double hCostMeters = GetHeuristicCost(heuristic, row, column, configuration);
                        if (double.IsPositiveInfinity(hCostMeters)) continue;

                        double bodyClearanceMeters = GetMinimumBodyClearance(primitive);
                        bool isGearSwitch = current.ParentNodeIndex >= 0 && current.Direction != direction;
                        int curvatureLevelDelta = curvatureLevelIndex - current.CurvatureLevelIndex;
                        double incrementalCostMeters = _costCalculator.Calculate(
                            primitive.ActualLengthMeters,
                            direction,
                            isGearSwitch,
                            curvatureLevels[curvatureLevelIndex],
                            maximumCurvaturePerMeter,
                            curvatureLevelDelta,
                            bodyClearanceMeters,
                            configuration);
                        double gCostMeters = current.GCostMeters + incrementalCostMeters;
                        if (!NumericGuard.IsFinite(gCostMeters) || gCostMeters < 0d) continue;

                        int headingIndex = AngleMath.ToHeadingIndex(primitive.End.Heading, configuration.HeadingResolutionRadians, headingBinCount);
                        var key = new HybridAStarNodeKey(row, column, headingIndex, direction, curvatureLevelIndex);
                        bestStates.TryGetValue(key, out NodeLabel existingLabel);
                        var successor = new HybridAStarNode(nodes.Count, key, primitive.End, current.NodeIndex, primitive,
                            curvatureLevels[curvatureLevelIndex], gCostMeters, hCostMeters, primitive.IsGoalTruncation);
                        if (existingLabel != null && !ShouldEnqueueSuccessor(successor, existingLabel.GCostMeters)) continue;

                        if (!successor.IsGoalCandidate && existingLabel != null && existingLabel.IsClosed) reopenedNodeCount++;
                        nodes.Add(successor);
                        if (!successor.IsGoalCandidate)
                            bestStates[key] = new NodeLabel(successor.NodeIndex, gCostMeters, false);
                        openList.Push(successor.NodeIndex, successor.FCostMeters, successor.HCostMeters, successor.GCostMeters);
                        peakOpenListCount = Math.Max(peakOpenListCount, openList.Count);
                        generatedNodeCount++;
                    }
                }
            }

            return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                staleOpenListEntryCount, peakOpenListCount, null,
                "Hybrid A* 搜索的 Open List 已耗尽，未找到满足运动和碰撞约束的路径。");
        }
        catch (Exception exception)
        {
            string reason = "Hybrid A* 搜索内部错误：" + exception.GetType().Name +
                (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message);
            return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
                staleOpenListEntryCount, peakOpenListCount, null, reason);
        }
    }

    private static HybridAStarSearchResult CreateResult(
        PlanningStatus status,
        IEnumerable<HybridAStarNode> nodes,
        int expandedNodeCount,
        int generatedNodeCount,
        int reopenedNodeCount,
        int staleOpenListEntryCount,
        int peakOpenListCount,
        int? successNodeIndex,
        string terminationReason = null)
    {
        string reason = status == PlanningStatus.Success
            ? string.Empty
            : terminationReason ?? GetDefaultTerminationReason(status);
        return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
            staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason);
    }

    private static string GetDefaultTerminationReason(PlanningStatus status)
    {
        switch (status)
        {
            case PlanningStatus.Cancelled:
                return "Hybrid A* 搜索已取消。";
            case PlanningStatus.InvalidRequest:
                return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。";
            case PlanningStatus.InvalidMap:
                return "Hybrid A* 搜索地图结构无效。";
            case PlanningStatus.MapNotReady:
                return "Hybrid A* 搜索地图尚未准备好。";
            case PlanningStatus.InvalidVehicleParameters:
                return "Hybrid A* 搜索车辆参数无效。";
            case PlanningStatus.InvalidCurvatureConfiguration:
                return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。";
            case PlanningStatus.StartOutsideMap:
                return "Hybrid A* 搜索起始扩大车体不完全位于地图内。";
            case PlanningStatus.StartInCollision:
                return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。";
            case PlanningStatus.GoalOutsideMap:
                return "Hybrid A* 搜索目标扩大车体不完全位于地图内。";
            case PlanningStatus.GoalInCollision:
                return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。";
            case PlanningStatus.SearchTimeout:
                return "Hybrid A* 搜索使用的总规划预算已耗尽。";
            case PlanningStatus.SearchNodeLimitExceeded:
                return "Hybrid A* 搜索达到扩展节点上限。";
            case PlanningStatus.NoFeasiblePath:
                return "Hybrid A* 搜索的 Open List 已耗尽，未找到满足运动和碰撞约束的路径。";
            case PlanningStatus.BacktrackingFailed:
                return "Hybrid A* 成功节点无法回溯为完整父链。";
            case PlanningStatus.FinalValidationFailed:
                return "Hybrid A* 路径未通过最终复核。";
            case PlanningStatus.InternalError:
                return "Hybrid A* 搜索发生未预期内部错误。";
            default:
                return "Hybrid A* 搜索以未识别状态终止：" + status + "。";
        }
    }

    private static PlanningStatus ValidateRequest(PlanningRequest request)
    {
        if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null ||
            !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) ||
            !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value)))
            return PlanningStatus.InvalidRequest;

        PlanningGridMap map = request.Map;
        if (map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters) || map.Bounds == null)
            return PlanningStatus.InvalidMap;
        if (!map.PlanningReady) return PlanningStatus.MapNotReady;

        VehicleParameters vehicle = request.Vehicle;
        if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) ||
            !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d ||
            !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter))
            return PlanningStatus.InvalidVehicleParameters;

        HybridAStarConfiguration configuration = request.Configuration;
        if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter ||
            (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse))
            return PlanningStatus.InvalidCurvatureConfiguration;

        return PlanningStatus.Success;
    }

    private static bool IsValidConfiguration(HybridAStarConfiguration configuration)
    {
        return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) &&
            NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) &&
            NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) &&
            NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) &&
            configuration.HeadingResolutionRadians <= 2d * Math.PI &&
            configuration.CurvatureLevelCount >= 3 && configuration.CurvatureLevelCount % 2 == 1 &&
            NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && configuration.GoalPositionToleranceMeters >= 0d &&
            NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && configuration.GoalHeadingToleranceRadians >= 0d &&
            configuration.MaximumExpandedNodes >= 0 && configuration.SearchTimeout >= TimeSpan.Zero &&
            NumericGuard.IsFinite(configuration.HeuristicWeight) && configuration.HeuristicWeight >= 0d &&
            NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) &&
            NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d &&
            NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d &&
            NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && configuration.CurvatureChangePenaltyMetersPerLevel >= 0d &&
            NumericGuard.IsFinite(configuration.ClearanceCostWeight) && configuration.ClearanceCostWeight >= 0d &&
            NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters);
    }

    private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle)
    {
        if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false;

        double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters;
        double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters;
        double longitudinalX = Math.Cos(pose.Heading);
        double longitudinalY = Math.Sin(pose.Heading);
        double lateralX = -longitudinalY;
        double lateralY = longitudinalX;
        for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2)
        for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2)
        {
            double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX;
            double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY;
            if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false;
        }
        return true;
    }

    private static IEnumerable<TravelDirection> GetStartDirections(PlanningRequest request, HybridAStarConfiguration configuration)
    {
        if (request.StartDirection.HasValue)
        {
            yield return request.StartDirection.Value;
            yield break;
        }

        yield return TravelDirection.Forward;
        if (configuration.AllowReverse) yield return TravelDirection.Reverse;
    }

    private static IEnumerable<TravelDirection> GetSuccessorDirections(HybridAStarConfiguration configuration)
    {
        yield return TravelDirection.Forward;
        if (configuration.AllowReverse) yield return TravelDirection.Reverse;
    }

    private static int GetHeadingBinCount(double headingResolutionRadians)
    {
        double rawBinCount = Math.Ceiling(2d * Math.PI / headingResolutionRadians);
        if (!NumericGuard.IsFinite(rawBinCount) || rawBinCount < 1d || rawBinCount > int.MaxValue)
            throw new ArgumentOutOfRangeException(nameof(headingResolutionRadians));
        return (int)rawBinCount;
    }

    private static int GetNearestCurvatureLevelIndex(IReadOnlyList<double> curvatureLevels, double curvaturePerMeter)
    {
        int bestIndex = -1;
        double smallestDifference = double.PositiveInfinity;
        for (int index = 0; index < curvatureLevels.Count; index++)
        {
            double difference = Math.Abs(curvatureLevels[index] - curvaturePerMeter);
            if (difference >= smallestDifference) continue;
            smallestDifference = difference;
            bestIndex = index;
        }
        return bestIndex;
    }

    private static double GetMinimumBodyClearance(MotionPrimitive primitive)
    {
        double minimum = double.PositiveInfinity;
        foreach (double clearanceMeters in primitive.BodyClearancesMeters)
        {
            if (double.IsNaN(clearanceMeters) || clearanceMeters < 0d) return 0d;
            minimum = Math.Min(minimum, clearanceMeters);
        }
        return minimum;
    }

    private static double GetHeuristicCost(GridDijkstraHeuristic heuristic, int row, int column, HybridAStarConfiguration configuration)
    {
        double gridCostMeters = heuristic.GetCost(row, column);
        if (double.IsPositiveInfinity(gridCostMeters)) return gridCostMeters;
        double weightedCostMeters = gridCostMeters * configuration.HeuristicWeight;
        return NumericGuard.IsFinite(weightedCostMeters) && weightedCostMeters >= 0d
            ? weightedCostMeters
            : double.PositiveInfinity;
    }

    /// <summary>
    /// 判断后继是否应进入 Open List。
    /// 参数：successor 是已完成连续碰撞检查的后继；bestKnownGCostMeters 是相同离散键普通状态的当前最优 G 值。
    /// 返回：普通状态仅在严格改善最优 G 值时入队；终点候选始终入队，以保留其未量化的连续终点位姿。
    /// </summary>
    private static bool ShouldEnqueueSuccessor(HybridAStarNode successor, double bestKnownGCostMeters)
    {
        if (successor == null || !NumericGuard.IsFinite(bestKnownGCostMeters)) return false;
        return successor.IsGoalCandidate || successor.GCostMeters < bestKnownGCostMeters - CostImprovementToleranceMeters;
    }

    private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason)
    {
        if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled;
        if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout;
        throw new ArgumentOutOfRangeException(nameof(stopReason));
    }

    private static bool IsFinitePose(Pose2D pose)
    {
        return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading);
    }

    private static bool IsTravelDirection(TravelDirection direction)
    {
        return direction == TravelDirection.Forward || direction == TravelDirection.Reverse;
    }

    private static bool IsGoalDirection(GoalDirectionConstraint direction)
    {
        return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse;
    }

    private sealed class NodeLabel
    {
        public NodeLabel(int nodeIndex, double gCostMeters, bool isClosed)
        {
            NodeIndex = nodeIndex;
            GCostMeters = gCostMeters;
            IsClosed = isClosed;
        }

        public int NodeIndex { get; }
        public double GCostMeters { get; }
        public bool IsClosed { get; set; }
    }
}
