# target_simulator/gui/ppi_display.py

"""
A reusable Tkinter widget that displays a Plan Position Indicator (PPI)
using Matplotlib, capable of showing both live targets and trajectory previews.
"""

import tkinter as tk
from tkinter import ttk
import math
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# Use absolute imports
from target_simulator.core.models import Target, Waypoint, ManeuverType, NM_TO_FT
from typing import List


class PPIDisplay(ttk.Frame):
    """A custom, reusable widget for the PPI radar display."""

    def __init__(self, master, max_range_nm: int = 100, scan_limit_deg: int = 60):
        super().__init__(master)
        self.max_range = max_range_nm
        self.scan_limit_deg = scan_limit_deg

        # Artists for dynamic target display
        self.target_artists = []
        self.active_targets: List[Target] = []

        # Artists for trajectory preview display
        self.preview_artists = []

        self._create_controls()
        self._create_plot()

    def _create_controls(self):
        """Creates the control widgets for the PPI display."""
        self.controls_frame = ttk.Frame(self)
        self.controls_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)

        ttk.Label(self.controls_frame, text="Range (NM):").pack(side=tk.LEFT)

        all_steps = [10, 20, 40, 80, 100, 160, 240, 320]
        valid_steps = sorted([s for s in all_steps if s <= self.max_range])
        if not valid_steps or self.max_range not in valid_steps:
            valid_steps.append(self.max_range)
            valid_steps.sort()

        self.range_var = tk.IntVar(value=self.max_range)
        self.range_selector = ttk.Combobox(
            self.controls_frame,
            textvariable=self.range_var,
            values=valid_steps,
            state="readonly",
            width=5,
        )
        self.range_selector.pack(side=tk.LEFT, padx=5)
        self.range_selector.bind("<<ComboboxSelected>>", self._on_range_selected)

    def _create_plot(self):
        """Initializes the Matplotlib polar plot."""
        fig = Figure(figsize=(5, 5), dpi=100, facecolor="#3E3E3E")
        fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.05)

        self.ax = fig.add_subplot(111, projection="polar", facecolor="#2E2E2E")

        self.ax.set_theta_zero_location("N")
        self.ax.set_theta_direction(-1)
        self.ax.set_rlabel_position(90)
        self.ax.set_ylim(0, self.range_var.get())

        angles = np.arange(0, 360, 30)
        # --- MODIFICATION START ---
        # Logic to format labels as 0, +30, ..., +/-180, ..., -30
        labels = []
        for angle in angles:
            if angle == 0:
                labels.append("0°")
            elif 0 < angle < 180:
                labels.append(f"+{angle}°")
            elif angle == 180:
                labels.append("±180°")
            else:  # angle > 180
                labels.append(f"-{360 - angle}°")
        # --- MODIFICATION END ---
        self.ax.set_thetagrids(angles, labels)

        self.ax.tick_params(axis="x", colors="white", labelsize=8)
        self.ax.tick_params(axis="y", colors="white", labelsize=8)
        self.ax.grid(color="white", linestyle="--", linewidth=0.5, alpha=0.5)
        self.ax.spines["polar"].set_color("white")
        self.ax.set_title("PPI Display", color="white")

        # --- Artists for drawing previews ---
        (self._path_plot,) = self.ax.plot([], [], "g--", linewidth=1.5, label="Path")
        (self._start_plot,) = self.ax.plot([], [], "go", markersize=8, label="Start")
        (self._waypoints_plot,) = self.ax.plot(
            [], [], "y+", markersize=10, mew=2, label="Waypoints"
        )
        self.preview_artists = [self._path_plot, self._start_plot, self._waypoints_plot]

        # --- Artists for scan lines ---
        limit_rad = np.deg2rad(self.scan_limit_deg)
        (self._scan_line_1,) = self.ax.plot(
            [limit_rad, limit_rad],
            [0, self.max_range],
            color="yellow",
            linestyle="--",
            linewidth=1,
        )
        (self._scan_line_2,) = self.ax.plot(
            [-limit_rad, -limit_rad],
            [0, self.max_range],
            color="yellow",
            linestyle="--",
            linewidth=1,
        )

        self.canvas = FigureCanvasTkAgg(fig, master=self)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self._update_scan_lines()

    def _update_scan_lines(self):
        """Updates the length and position of the scan sector lines."""
        current_range_max = self.ax.get_ylim()[1]
        limit_rad = np.deg2rad(self.scan_limit_deg)

        self._scan_line_1.set_data([limit_rad, limit_rad], [0, current_range_max])
        self._scan_line_2.set_data([-limit_rad, -limit_rad], [0, current_range_max])

    def _on_range_selected(self, event=None):
        """Handles the selection of a new range."""
        new_range = self.range_var.get()
        self.ax.set_ylim(0, new_range)
        self._update_scan_lines()
        self.canvas.draw_idle()

    def clear_previews(self):
        """Clears all preview-related artists from the plot."""
        for artist in self.preview_artists:
            artist.set_data([], [])
        self.canvas.draw_idle()

    def update_targets(self, targets: List[Target]):
        """Updates the display with the current state of active targets."""
        self.active_targets = [t for t in targets if t.active]

        # Clear previous target artists
        for artist in self.target_artists:
            artist.remove()
        self.target_artists.clear()

        vector_len_nm = self.range_var.get() / 20.0  # Length of heading vector

        for target in self.active_targets:
            # Target's polar coordinates
            r_nm = target.current_range_nm
            theta_rad = np.deg2rad(target.current_azimuth_deg)

            # Draw the target dot
            (dot,) = self.ax.plot(theta_rad, r_nm, "o", markersize=6, color="red")
            self.target_artists.append(dot)

            # Draw the heading vector
            heading_rad = np.deg2rad(target.current_heading_deg)

            # Convert to Cartesian to calculate endpoint, then back to polar
            x_nm = r_nm * np.sin(theta_rad)
            y_nm = r_nm * np.cos(theta_rad)
            dx_nm = vector_len_nm * np.sin(heading_rad)
            dy_nm = vector_len_nm * np.cos(heading_rad)

            r2_nm = math.sqrt((x_nm + dx_nm) ** 2 + (y_nm + dy_nm) ** 2)
            theta2_rad = math.atan2(x_nm + dx_nm, y_nm + dy_nm)

            (line,) = self.ax.plot(
                [theta_rad, theta2_rad], [r_nm, r2_nm], color="red", linewidth=1.2
            )
            self.target_artists.append(line)

        self.canvas.draw_idle()

    def draw_trajectory_preview(self, waypoints: List[Waypoint], use_spline: bool):
        """
        Simulates and draws a trajectory preview by leveraging the static path generator.
        """
        self.clear_previews()

        if not waypoints or waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
            return

        # Use the static method to get the path without creating a Target instance
        path, _ = Target.generate_path_from_waypoints(waypoints, use_spline)

        if not path:
            return

        # --- Draw the main path (splined or polygonal) ---
        path_thetas = []
        path_rs = []
        for point in path:
            _time, x_ft, y_ft, _z_ft = point
            r_ft = math.sqrt(x_ft**2 + y_ft**2)
            theta_rad = math.atan2(x_ft, y_ft)
            path_rs.append(r_ft / NM_TO_FT)
            path_thetas.append(theta_rad)

        self._path_plot.set_data(path_thetas, path_rs)

        # --- Draw waypoint markers (only for Fly to Point) ---
        wp_thetas = []
        wp_rs = []
        for wp in waypoints:
            if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
                r_nm = wp.target_range_nm or 0.0
                theta_rad = math.radians(wp.target_azimuth_deg or 0.0)
                wp_rs.append(r_nm)
                wp_thetas.append(theta_rad)

        self._waypoints_plot.set_data(wp_thetas, wp_rs)

        # --- Draw the start point ---
        start_wp = waypoints[0]
        start_r = start_wp.target_range_nm or 0.0
        start_theta = math.radians(start_wp.target_azimuth_deg or 0.0)
        self._start_plot.set_data([start_theta], [start_r])
        self.canvas.draw_idle()

    def reconfigure_radar(self, max_range_nm: int, scan_limit_deg: int):
        """
        Updates the radar parameters (range, scan limit) of an existing PPI display.
        """
        self.max_range = max_range_nm
        self.scan_limit_deg = scan_limit_deg

        # Update the range combobox values
        steps = [10, 20, 40, 80, 100, 160, 240, 320]
        valid_steps = sorted([s for s in steps if s <= self.max_range])
        if not valid_steps or self.max_range not in valid_steps:
            valid_steps.append(self.max_range)
            valid_steps.sort()

        current_range = self.range_var.get()
        self.range_selector["values"] = valid_steps

        # If the current range is still valid, keep it. Otherwise, reset to max.
        if current_range in valid_steps:
            self.range_var.set(current_range)
        else:
            self.range_var.set(self.max_range)

        # Apply the new range and redraw everything
        self._on_range_selected()
