#!/usr/bin/env python3
"""
PID 路径跟随 Demo — 最简版本

订阅 /plan (nav_msgs/Path)，收到路径后立即开始 PID 跟随。
不需要 planner_server，不需要状态机，不需要任何额外节点。

用法：
  python3 path_follower_demo.py

然后发一条 /plan 即可：
  ros2 topic pub /plan nav_msgs/msg/Path "{header: {frame_id: 'map'}, poses: [
    {pose: {position: {x: 1.0, y: 0.0}}},
    {pose: {position: {x: 2.0, y: 0.5}}},
    {pose: {position: {x: 3.0, y: 0.0}}}
  ]}"
"""

import rclpy
from rclpy.node import Node
from nav_msgs.msg import Odometry, Path
from geometry_msgs.msg import Twist
import math
import time


class PathFollower(Node):

    def __init__(self):
        super().__init__("path_follower")

        # ── PID 参数 ──
        self.declare_parameter("kp", 0.8)
        self.declare_parameter("ki", 0.0)
        self.declare_parameter("kd", 0.1)
        self.declare_parameter("linear_speed", 0.3)     # 前进速度 m/s
        self.declare_parameter("waypoint_tolerance", 0.1)  # 到达判定距离 m
        self.declare_parameter("max_angular", 10.0)       # 最大角速度 rad/s

        # ── 订阅 /plan 和 /odom ──
        self.plan_sub = self.create_subscription(
            Path, "/plan", self.plan_cb, 10)
        self.odom_sub = self.create_subscription(
            Odometry, "/odom", self.odom_cb, 10)

        # ── 发布 /cmd_vel ──
        self.cmd_pub = self.create_publisher(Twist, "/cmd_vel", 10)

        # ── 状态 ──
        self.path = []              # [(x, y), ...] 路径点列表(世界坐标)
        self.current_idx = 0        # 当前目标 waypoint 索引
        self.odom = None            # 最新里程计
        self.following = False      # 是否正在跟随

        # ── PID 状态 ──
        self._integral = 0.0
        self._last_err = 0.0
        self._last_time = None

        # ── 主循环 20Hz ──
        self.timer = self.create_timer(0.05, self.control_loop)

        self.get_logger().info("PathFollower ready, waiting for /plan...")

    # ── 回调 ──
    def plan_cb(self, msg: Path):
        """收到新路径，替换当前路径并开始跟随"""
        if len(msg.poses) < 2:
            return

        self.path = [(p.pose.position.x, p.pose.position.y)
                     for p in msg.poses]
        self.current_idx = 0
        self.following = True
        self._reset_pid()
        self.get_logger().info(
            f"Received path with {len(self.path)} waypoints, "
            f"from ({self.path[0][0]:.2f}, {self.path[0][1]:.2f}) "
            f"to ({self.path[-1][0]:.2f}, {self.path[-1][1]:.2f})")

    def odom_cb(self, msg: Odometry):
        self.odom = msg

    # ── PID ──
    def _reset_pid(self):
        self._integral = 0.0
        self._last_err = 0.0
        self._last_time = None

    def _pid(self, error: float, dt: float) -> float:
        kp = self.get_parameter("kp").value
        ki = self.get_parameter("ki").value
        kd = self.get_parameter("kd").value

        self._integral += error * dt
        if dt > 0.001:
            derivative = (error - self._last_err) / dt
        else:
            derivative = 0.0
        self._last_err = error

        return kp * error + ki * self._integral + kd * derivative

    # ── 主控制循环 ──
    def control_loop(self):
        if not self.following or self.odom is None:
            self._publish_cmd(0.0, 0.0)
            return

        # 当前位姿
        x = self.odom.pose.pose.position.x
        y = self.odom.pose.pose.position.y
        yaw = self._quat_to_yaw(self.odom.pose.pose.orientation)

        # 当前目标 waypoint
        if self.current_idx >= len(self.path):
            self._publish_cmd(0.0, 0.0)
            if self.following:
                self.get_logger().info("Path completed!")
                self.following = False
            return

        tx, ty = self.path[self.current_idx]
        dx = tx - x
        dy = ty - y
        dist = math.hypot(dx, dy)

        # 到达当前 waypoint → 进下一个
        tolerance = self.get_parameter("waypoint_tolerance").value
        if dist < tolerance:
            self.current_idx += 1
            if self.current_idx >= len(self.path):
                self._publish_cmd(0.0, 0.0)
                self.get_logger().info("Path completed!")
                self.following = False
                return
            tx, ty = self.path[self.current_idx]
            dx = tx - x
            dy = ty - y

        # 航向误差
        target_heading = math.atan2(dy, dx)
        heading_err = target_heading - yaw
        heading_err = math.atan2(math.sin(heading_err), math.cos(heading_err))

        # PID → 角速度
        now = time.time()
        dt = now - self._last_time if self._last_time else 0.05
        self._last_time = now
        angular = self._pid(heading_err, dt)

        # 限幅
        max_ang = self.get_parameter("max_angular").value
        angular = max(-max_ang, min(max_ang, angular))

        # 速度：误差大时减速
        linear = self.get_parameter("linear_speed").value
        if abs(heading_err) > 0.5:
            linear *= 0.5  # 航向偏差超过 ~30° 时减速
        if abs(heading_err) > 1.0:
            linear *= 0.3  # 偏差超过 ~57° 时更慢

        self._publish_cmd(linear, angular)

        # 每 2 秒打印一次状态
        if int(now * 10) % 20 == 0:
            self.get_logger().info(
                f"WP[{self.current_idx}/{len(self.path)}] "
                f"target=({tx:.2f},{ty:.2f}) dist={dist:.3f} "
                f"err={heading_err:.2f}rad cmd=(v={linear:.2f},ω={angular:.2f})")

    # ── 工具 ──
    def _publish_cmd(self, v, w):
        twist = Twist()
        twist.linear.x = v
        twist.angular.z = w
        self.cmd_pub.publish(twist)

    @staticmethod
    def _quat_to_yaw(q) -> float:
        return math.atan2(2.0 * (q.w * q.z + q.x * q.y),
                          1.0 - 2.0 * (q.y * q.y + q.z * q.z))


def main():
    rclpy.init()
    node = PathFollower()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node._publish_cmd(0.0, 0.0)
        node.destroy_node()
        rclpy.shutdown()


if __name__ == "__main__":
    main()
