"""为新版4m直线控制器实验CSV生成轨迹、横向/航向误差和速度响应图。"""

from __future__ import annotations

import argparse
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


SCRIPT_DIR = Path(__file__).resolve().parent


def configure_matplotlib() -> None:
    """配置可显示中文和负号的Matplotlib字体。"""
    plt.rcParams["font.sans-serif"] = [
        "Microsoft YaHei",
        "SimHei",
        "Noto Sans CJK SC",
        "Arial Unicode MS",
        "DejaVu Sans",
    ]
    plt.rcParams["axes.unicode_minus"] = False


def numeric_column(
    frame: pd.DataFrame,
    name: str,
    default: float = np.nan,
) -> np.ndarray:
    """将CSV列安全转换为浮点数组，缺失列使用指定默认值。"""
    if name not in frame.columns:
        return np.full(len(frame), default, dtype=float)
    return pd.to_numeric(frame[name], errors="coerce").to_numpy(
        dtype=float,
        copy=True,
    )


def first_finite(values: np.ndarray, default: float) -> float:
    """读取数组中的第一个有限值。"""
    finite = values[np.isfinite(values)]
    return float(finite[0]) if finite.size else default


def first_text(frame: pd.DataFrame, name: str, default: str) -> str:
    """读取文本元数据列中的第一个非空值。"""
    if name not in frame.columns:
        return default
    values = frame[name].dropna().astype(str)
    values = values[values.str.strip() != ""]
    return values.iloc[0] if not values.empty else default


def fill_reference_series(
    values: np.ndarray,
    fallback: np.ndarray,
) -> np.ndarray:
    """前后填充后台采样得到的控制参考值，缺失时使用解析速度曲线。"""
    series = pd.Series(values, dtype=float)
    filled = series.ffill().bfill().to_numpy(
        dtype=float,
        copy=True,
    )
    missing = ~np.isfinite(filled)
    filled[missing] = fallback[missing]
    return filled


def planned_motion(
    time_seconds: np.ndarray,
    length_meters: float,
    cruise_speed_mps: float,
    acceleration_mps2: float,
    deceleration_mps2: float,
) -> tuple[np.ndarray, np.ndarray]:
    """计算从静止出发并在终点静止的梯形或三角形理想时间速度轨迹。"""
    acceleration_distance = (
        cruise_speed_mps**2 / (2.0 * acceleration_mps2)
    )
    deceleration_distance = (
        cruise_speed_mps**2 / (2.0 * deceleration_mps2)
    )

    if acceleration_distance + deceleration_distance <= length_meters:
        peak_speed = cruise_speed_mps
    else:
        peak_speed = np.sqrt(
            2.0
            * length_meters
            / (1.0 / acceleration_mps2 + 1.0 / deceleration_mps2)
        )
        acceleration_distance = (
            peak_speed**2 / (2.0 * acceleration_mps2)
        )
        deceleration_distance = (
            peak_speed**2 / (2.0 * deceleration_mps2)
        )

    acceleration_time = peak_speed / acceleration_mps2
    deceleration_time = peak_speed / deceleration_mps2
    cruise_distance = max(
        0.0,
        length_meters - acceleration_distance - deceleration_distance,
    )
    cruise_time = cruise_distance / peak_speed
    deceleration_start_time = acceleration_time + cruise_time
    finish_time = deceleration_start_time + deceleration_time

    progress = np.zeros_like(time_seconds, dtype=float)
    speed = np.zeros_like(time_seconds, dtype=float)

    accelerating = time_seconds <= acceleration_time
    progress[accelerating] = (
        0.5 * acceleration_mps2 * time_seconds[accelerating] ** 2
    )
    speed[accelerating] = acceleration_mps2 * time_seconds[accelerating]

    cruising = (
        (time_seconds > acceleration_time)
        & (time_seconds <= deceleration_start_time)
    )
    progress[cruising] = (
        acceleration_distance
        + peak_speed * (time_seconds[cruising] - acceleration_time)
    )
    speed[cruising] = peak_speed

    decelerating = (
        (time_seconds > deceleration_start_time)
        & (time_seconds <= finish_time)
    )
    remaining_time = finish_time - time_seconds[decelerating]
    progress[decelerating] = (
        length_meters
        - 0.5 * deceleration_mps2 * remaining_time**2
    )
    speed[decelerating] = deceleration_mps2 * remaining_time

    finished = time_seconds > finish_time
    progress[finished] = length_meters
    speed[finished] = 0.0
    return progress, speed


def load_experiment(csv_path: Path) -> dict[str, object]:
    """读取新版CSV并构造绘图所需的统一SI单位数据。"""
    frame = pd.read_csv(csv_path, encoding="utf-8-sig")
    if frame.empty:
        raise ValueError("CSV没有任何采样行。")

    time_seconds = numeric_column(frame, "ElapsedSeconds")
    valid_time = np.isfinite(time_seconds)
    frame = frame.loc[valid_time].reset_index(drop=True)
    time_seconds = time_seconds[valid_time]
    if time_seconds.size < 2:
        raise ValueError("CSV中的有效时间采样不足2帧。")
    time_seconds = time_seconds - time_seconds[0]

    state_x = numeric_column(frame, "StateXMeters")
    state_y = numeric_column(frame, "StateYMeters")
    has_processed = numeric_column(frame, "HasProcessedState", 0.0) > 0.5
    processed_valid = has_processed & np.isfinite(state_x) & np.isfinite(state_y)

    raw_x_meters = numeric_column(frame, "DetourX") / 1000.0
    raw_y_meters = numeric_column(frame, "DetourY") / 1000.0
    actual_x = np.where(processed_valid, state_x, raw_x_meters)
    actual_y = np.where(processed_valid, state_y, raw_y_meters)
    valid_position = np.isfinite(actual_x) & np.isfinite(actual_y)
    if np.count_nonzero(valid_position) < 2:
        raise ValueError("CSV中没有足够的有效车辆位置。")

    start = np.array(
        [
            first_finite(numeric_column(frame, "ReferenceStartX"), np.nan),
            first_finite(numeric_column(frame, "ReferenceStartY"), np.nan),
        ],
        dtype=float,
    ) / 1000.0
    end = np.array(
        [
            first_finite(numeric_column(frame, "ReferenceEndX"), np.nan),
            first_finite(numeric_column(frame, "ReferenceEndY"), np.nan),
        ],
        dtype=float,
    ) / 1000.0
    if not np.all(np.isfinite(start)) or not np.all(np.isfinite(end)):
        raise ValueError("CSV缺少有效的参考起点或终点。")

    line = end - start
    length_meters = float(np.linalg.norm(line))
    if length_meters <= 1e-6:
        raise ValueError("参考直线长度必须大于0。")
    tangent = line / length_meters
    left_normal = np.array([-tangent[1], tangent[0]])
    displacement = np.column_stack([actual_x, actual_y]) - start
    # 与C# TrajectoryProjector保持一致：轨迹位于车辆左侧时为正。
    derived_lateral_error = -(displacement @ left_normal)
    recorded_lateral_error = numeric_column(
        frame,
        "ControlLateralErrorMeters",
    )
    has_control_reference = (
        numeric_column(frame, "HasControlReference", 0.0) > 0.5
    )
    lateral_error = np.where(
        has_control_reference & np.isfinite(recorded_lateral_error),
        recorded_lateral_error,
        derived_lateral_error,
    )

    state_yaw = numeric_column(frame, "StateYawRadians")
    raw_yaw = np.deg2rad(numeric_column(frame, "DetourTheta"))
    actual_yaw = np.where(
        has_processed & np.isfinite(state_yaw),
        state_yaw,
        raw_yaw,
    )
    reference_yaw = np.arctan2(tangent[1], tangent[0])
    derived_heading_error = np.arctan2(
        np.sin(reference_yaw - actual_yaw),
        np.cos(reference_yaw - actual_yaw),
    )
    recorded_heading_error = numeric_column(
        frame,
        "ControlHeadingErrorRadians",
    )
    heading_error = np.where(
        has_control_reference & np.isfinite(recorded_heading_error),
        recorded_heading_error,
        derived_heading_error,
    )

    cruise_speed = first_finite(
        numeric_column(frame, "ReferenceSpeed"),
        0.30,
    )
    acceleration = first_finite(
        numeric_column(
            frame,
            "ReferenceAccelerationMetersPerSecondSquared",
        ),
        0.20,
    )
    deceleration = first_finite(
        numeric_column(
            frame,
            "ReferenceDecelerationMetersPerSecondSquared",
        ),
        0.20,
    )
    if acceleration <= 0.0:
        acceleration = 0.20
    if deceleration <= 0.0:
        deceleration = 0.20

    _, ideal_speed = planned_motion(
        time_seconds,
        length_meters,
        cruise_speed,
        acceleration,
        deceleration,
    )
    reference_speed = fill_reference_series(
        numeric_column(frame, "ControlReferenceSpeedMetersPerSecond"),
        ideal_speed,
    )
    actual_speed = numeric_column(frame, "StateBodyVxMetersPerSecond")
    velocity_valid = (
        numeric_column(frame, "StateVelocityEstimateValid", 0.0) > 0.5
    )
    actual_speed[~velocity_valid] = np.nan
    command_speed = numeric_column(frame, "CommandSpeed")

    return {
        "frame": frame,
        "time": time_seconds,
        "actual_x": actual_x,
        "actual_y": actual_y,
        "valid_position": valid_position,
        "start": start,
        "end": end,
        "length": length_meters,
        "lateral_error": lateral_error,
        "heading_error": heading_error,
        "reference_speed": reference_speed,
        "actual_speed": actual_speed,
        "command_speed": command_speed,
        "controller_name": first_text(
            frame,
            "ControllerName",
            "NewController",
        ),
        "trajectory_name": first_text(
            frame,
            "TrajectoryName",
            "Trajectory",
        ),
    }


def finite_rmse(values: np.ndarray) -> float:
    """计算忽略无效样本后的均方根值。"""
    finite = values[np.isfinite(values)]
    return float(np.sqrt(np.mean(finite**2))) if finite.size else np.nan


def save_figure(
    fig: plt.Figure,
    destination: Path,
    show: bool,
) -> None:
    """保存并关闭一张实验图。"""
    fig.tight_layout()
    fig.savefig(destination, dpi=300, bbox_inches="tight")
    if show:
        plt.show()
    plt.close(fig)


def plot_experiment(
    csv_path: Path,
    output_directory: Path,
    show: bool,
) -> list[Path]:
    """为单份新版控制器CSV生成四类对比图。"""
    data = load_experiment(csv_path)
    output_directory.mkdir(parents=True, exist_ok=True)
    title = f"{data['controller_name']} - {data['trajectory_name']}"
    destinations: list[Path] = []

    valid_position = data["valid_position"]
    fig, axis = plt.subplots(figsize=(9.0, 6.5))
    axis.plot(
        [data["start"][0], data["end"][0]],
        [data["start"][1], data["end"][1]],
        "--",
        linewidth=2.0,
        label="期望4m直线轨迹",
    )
    axis.plot(
        data["actual_x"][valid_position],
        data["actual_y"][valid_position],
        linewidth=1.5,
        label="状态估计后的实际轨迹",
    )
    axis.scatter(*data["start"], color="green", s=45, label="起点")
    axis.scatter(*data["end"], color="red", s=45, label="终点")
    axis.set_aspect("equal", adjustable="box")
    axis.set_xlabel("世界坐标X / m")
    axis.set_ylabel("世界坐标Y / m")
    axis.set_title(f"期望轨迹与实际轨迹对比\n{title}")
    axis.grid(True, alpha=0.3)
    axis.legend()
    destination = output_directory / f"{csv_path.stem}_trajectory.png"
    save_figure(fig, destination, show)
    destinations.append(destination)

    lateral_mm = data["lateral_error"] * 1000.0
    lateral_rmse_mm = finite_rmse(lateral_mm)
    fig, axis = plt.subplots(figsize=(10.0, 5.5))
    axis.plot(data["time"], lateral_mm, linewidth=1.5)
    axis.axhline(0.0, color="black", linewidth=0.8)
    axis.set_xlabel("时间 / s")
    axis.set_ylabel("横向误差 / mm")
    axis.set_title(
        f"横向误差（轨迹在车辆左侧为正）\n{title}，RMSE={lateral_rmse_mm:.2f}mm"
    )
    axis.grid(True, alpha=0.3)
    destination = output_directory / f"{csv_path.stem}_lateral_error.png"
    save_figure(fig, destination, show)
    destinations.append(destination)

    heading_degrees = np.rad2deg(data["heading_error"])
    heading_rmse_degrees = finite_rmse(heading_degrees)
    fig, axis = plt.subplots(figsize=(10.0, 5.5))
    axis.plot(data["time"], heading_degrees, linewidth=1.5)
    axis.axhline(0.0, color="black", linewidth=0.8)
    axis.set_xlabel("时间 / s")
    axis.set_ylabel("航向角偏差 / °")
    axis.set_title(
        "航向角偏差：参考轨迹航向－实际车体航向（逆时针为正）\n"
        f"{title}，RMSE={heading_rmse_degrees:.3f}°"
    )
    axis.grid(True, alpha=0.3)
    destination = output_directory / f"{csv_path.stem}_heading_error.png"
    save_figure(fig, destination, show)
    destinations.append(destination)

    speed_error = data["actual_speed"] - data["reference_speed"]
    speed_rmse = finite_rmse(speed_error)
    fig, axis = plt.subplots(figsize=(10.0, 5.8))
    axis.plot(
        data["time"],
        data["reference_speed"],
        linewidth=1.8,
        label="控制器实际参考速度",
    )
    axis.plot(
        data["time"],
        data["command_speed"],
        "--",
        linewidth=1.3,
        label="纵向控制器下发速度",
    )
    axis.plot(
        data["time"],
        data["actual_speed"],
        linewidth=1.5,
        label="状态估计实际车体纵向速度",
    )
    axis.set_xlabel("时间 / s")
    axis.set_ylabel("速度 / (m/s)")
    axis.set_title(f"参考速度与实际速度对比\n{title}，RMSE={speed_rmse:.4f}m/s")
    axis.grid(True, alpha=0.3)
    axis.legend()
    destination = output_directory / f"{csv_path.stem}_speed_response.png"
    save_figure(fig, destination, show)
    destinations.append(destination)

    print(
        f"{csv_path.name}: 横向RMSE={lateral_rmse_mm:.3f}mm, "
        f"航向RMSE={heading_rmse_degrees:.4f}°, "
        f"速度RMSE={speed_rmse:.5f}m/s"
    )
    for destination in destinations:
        print(f"已生成：{destination}")
    return destinations


def discover_csv_files(arguments: list[str]) -> list[Path]:
    """读取命令行文件；未指定时扫描脚本目录及data子目录中的CSV。"""
    if arguments:
        files = [Path(item).expanduser().resolve() for item in arguments]
    else:
        files = sorted(SCRIPT_DIR.glob("*.csv"))
        files.extend(sorted((SCRIPT_DIR / "data").glob("*.csv")))
    files = [path for path in files if path.is_file()]
    if not files:
        raise FileNotFoundError(
            "没有找到CSV；请传入文件路径，或将文件放到脚本目录/data中。"
        )
    return files


def main() -> None:
    """解析命令行并批量处理新版控制器实验CSV。"""
    parser = argparse.ArgumentParser(
        description="绘制新版控制器4m直线实验的四类对比图。"
    )
    parser.add_argument("csv", nargs="*", help="需要处理的CSV文件路径。")
    parser.add_argument(
        "--output-dir",
        help="图片输出目录；默认使用脚本目录/plots。",
    )
    parser.add_argument(
        "--show",
        action="store_true",
        help="保存图片后同时显示窗口。",
    )
    arguments = parser.parse_args()
    configure_matplotlib()

    output_directory = (
        Path(arguments.output_dir).expanduser().resolve()
        if arguments.output_dir
        else SCRIPT_DIR / "plots"
    )
    failed = 0
    for csv_path in discover_csv_files(arguments.csv):
        try:
            plot_experiment(csv_path, output_directory, arguments.show)
        except Exception as exception:
            failed += 1
            print(f"处理失败：{csv_path}：{exception}")

    if failed:
        raise SystemExit(f"共有{failed}个CSV处理失败。")


if __name__ == "__main__":
    main()
