use eframe::egui;

use crate::{
    connection::{ConnectionManager, ConnectionState},
    force::HudSpatialForce,
    recording::Recorder,
    serial_core::serial::{SerialIoStats, SerialProtocol},
    style::{
        self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, METRICS, ONE_DARK_PRO,
        dim_text, group_frame, layout, panel_frame, rich_tag_button, tag_button,
    },
    utils::serial_enum,
};

pub struct FloatingPanelState {
    pub visible: bool,
    default_pos: egui::Pos2,
    tag_pos: egui::Pos2,
    center_anim: f32,
    center_anim_target: bool,
    center_anim_last_time: Option<f64>,
}

pub struct ConfigPanelState {
    pub mode: SerialMode,
    pub port: String,
    pub available_ports: Vec<String>,
    pub baud_rate: u32,
    pub data_bits: u8,
    pub stop_bits: u8,
    pub parity: Parity,
    pub timeout_ms: u32,
    pub module_addr: u8,
    pub connected: bool,
    pub auto_reconnect: bool,
    pub manual_tx: String,
    pub model_path: String,
}

pub struct ConnectPanelState {
    pub mode: SerialMode,
    pub port: Vec<String>,
    pub selected_port: String,
    pub duration: u8,
    pub manual: bool,
    pub rows: u8,
    pub cols: u8,
    pub connection: bool,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SerialMode {
    Finger,
    Hand,
}

impl SerialMode {
    pub fn baud_rate(self) -> u32 {
        match self {
            SerialMode::Finger => 921_600,
            SerialMode::Hand => 1_152_000,
        }
    }

    pub fn protocol(self) -> SerialProtocol {
        match self {
            SerialMode::Finger => SerialProtocol::TactileA,
            SerialMode::Hand => SerialProtocol::HandGateway,
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Parity {
    None,
    Odd,
    Even,
}

pub enum IconButtonIcon<'a> {
    Font(&'a str),
    Png(egui::ImageSource<'a>),
}

impl FloatingPanelState {
    pub fn new(default_pos: [f32; 2], tag_pos: [f32; 2]) -> Self {
        Self {
            visible: true,
            default_pos: egui::pos2(default_pos[0], default_pos[1]),
            tag_pos: egui::pos2(tag_pos[0], tag_pos[1]),
            center_anim: 1.0,
            center_anim_target: true,
            center_anim_last_time: None,
        }
    }
}

impl Default for ConfigPanelState {
    fn default() -> Self {
        let available_ports = serial_enum().unwrap_or_default();
        let port = available_ports
            .first()
            .cloned()
            .unwrap_or_else(|| "COM3".to_owned());

        Self {
            mode: SerialMode::Hand,
            port,
            available_ports,
            baud_rate: SerialMode::Hand.baud_rate(),
            data_bits: 8,
            stop_bits: 1,
            parity: Parity::None,
            timeout_ms: 1000,
            module_addr: 1,
            connected: false,
            auto_reconnect: false,
            manual_tx: "01 03 00 00 00 02".to_owned(),
            model_path: "model/default.eskin".to_owned(),
        }
    }
}

impl Default for ConnectPanelState {
    fn default() -> Self {
        let port = serial_enum().unwrap_or_default();
        let selected_port = port.first().cloned().unwrap_or_default();
        Self {
            mode: SerialMode::Hand,
            port,
            selected_port,
            duration: 10,
            manual: false,
            rows: 12,
            cols: 7,
            connection: false,
        }
    }
}

const PANEL_VIEWPORT_MARGIN: f32 = 12.0;

fn viewport_panel_rect(ctx: &egui::Context) -> egui::Rect {
    ctx.content_rect().shrink(PANEL_VIEWPORT_MARGIN)
}

fn responsive_panel_width(ctx: &egui::Context, preferred: f32, min_width: f32) -> f32 {
    let screen = ctx.content_rect();
    let available = (screen.width() - PANEL_VIEWPORT_MARGIN * 2.0).max(240.0);
    let side_cap = (screen.width() * 0.44).max(min_width.min(available));

    preferred
        .min(side_cap)
        .min(available)
        .max(min_width.min(available))
}

fn responsive_panel_height(ctx: &egui::Context, preferred: f32, min_height: f32) -> f32 {
    let screen = ctx.content_rect();
    let available = (screen.height() - layout::TITLE_BAR_HEIGHT - PANEL_VIEWPORT_MARGIN * 2.0)
        .max(min_height.min(screen.height()));
    let height_cap = (screen.height() * 0.72).max(min_height.min(available));

    preferred
        .min(height_cap)
        .min(available)
        .max(min_height.min(available))
}

fn responsive_panel_pos(
    ctx: &egui::Context,
    preferred: egui::Pos2,
    panel_width: f32,
) -> egui::Pos2 {
    let rect = viewport_panel_rect(ctx);
    let min_y = rect.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN;
    let max_x = (rect.right() - panel_width).max(rect.left());
    let preferred_right_side = preferred.x > rect.center().x;
    let x = if preferred_right_side {
        max_x
    } else {
        preferred.x.clamp(rect.left(), max_x)
    };
    let y = preferred.y.clamp(min_y, rect.bottom());

    egui::pos2(x, y)
}

fn narrow_panel(ui: &egui::Ui) -> bool {
    ui.available_width() < 420.0
}

pub fn draw_scene_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
    draw_floating_panel(ctx, panel, "场景", "scene_panel", 320.0, 260.0, |ui| {
        ui.horizontal(|ui| {
            ui.colored_label(dim_text(), "视图");
            let _ = ui.selectable_label(true, "簇");
            let _ = ui.selectable_label(false, "三角形");
        });
        ui.separator();
        group_frame().show(ui, |ui| {
            ui.label("模型 / 材质 / 灯光");
            ui.label("目标任务   64");
            ui.label("缓存命中 100.0%");
        });
    });
}

pub fn draw_connect_panel(
    ctx: &egui::Context,
    panel: &mut FloatingPanelState,
    config: &mut ConnectPanelState,
    connection: &ConnectionManager,
    recorder: &Recorder,
    export_path: &mut String,
) {
    let conn_state = connection.state();
    let is_connected = matches!(
        conn_state,
        ConnectionState::Connected | ConnectionState::Streaming
    );

    draw_center_floating_panel(
        ctx,
        panel,
        "connect_center_panel",
        layout::CENTER_PANEL_TOP,
        "连接",
        |ui| {
            ui.vertical(|ui| {
                // centered_mode_row(ui, &mut config.mode);
                ui.add_space(METRICS.row_gap);

                group_frame().show(ui, |ui| {
                    ui.horizontal(|ui| {
                        ui.add(
                            egui::Image::new(egui::include_image!("../static/cpu.png"))
                                .fit_to_exact_size(egui::vec2(64.0, 64.0)),
                        );
                        ui.add_space(METRICS.item_gap);

                        ui.vertical(|ui| {
                            draw_connect_port_row(ui, config);
                            ui.add_space(6.0);
                            draw_connect_matrix_row(ui, config);
                        });
                    });
                });

                ui.add_space(METRICS.row_gap);
                draw_connect_action_row(ui, conn_state, is_connected, config, connection, recorder);

                if is_connected {
                    draw_recording_toolbar(ui, recorder, export_path);
                }
            });
        },
    );
}

// fn centered_mode_row(ui: &mut egui::Ui, mode: &mut SerialMode) {
//     let button_width = 96.0;
//     let gap = METRICS.item_gap;
//     let total_width = button_width * 3.0 + gap * 2.0;
//     let left_space = ((ui.available_width() - total_width) * 0.5).max(0.0);

//     ui.horizontal(|ui| {
//         ui.add_space(left_space);
//         mode_button(ui, mode, SerialMode::Finger, "指尖模块");
//         ui.add_space(gap);
//         mode_button(ui, mode, SerialMode::Hand, "手掌模块");
//         // ui.add_space(gap);
//         // mode_button(ui, mode, SerialMode::Model, "模型");
//     });
// }

fn draw_connect_port_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
    ui.horizontal_wrapped(|ui| {
        ui.label(style::field_label("串口"));
        egui::ComboBox::from_id_salt("connect_ports")
            .width(150.0)
            .selected_text(if config.selected_port.is_empty() {
                "无可用串口".to_owned()
            } else {
                config.selected_port.clone()
            })
            .show_ui(ui, |ui| {
                for port in &config.port {
                    ui.selectable_value(&mut config.selected_port, port.clone(), port.as_str());
                }
            });

        if ui
            .add(style::icon_button(
                egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0),
                METRICS.icon_button,
            ))
            .on_hover_text("刷新串口")
            .clicked()
        {
            if let Ok(ports) = serial_enum() {
                if !ports.contains(&config.selected_port) {
                    config.selected_port = ports.first().cloned().unwrap_or_default();
                }
                config.port = ports;
            }
        }

        ui.add_space(METRICS.item_gap);
        ui.label(style::field_label("频率"));
        ui.add_sized(
            egui::vec2(72.0, METRICS.field_height),
            egui::DragValue::new(&mut config.duration).range(1..=120),
        );
    });
}

fn draw_connect_matrix_row(ui: &mut egui::Ui, config: &mut ConnectPanelState) {
    ui.horizontal_wrapped(|ui| {
        ui.checkbox(&mut config.manual, "手动矩阵");

        ui.add_enabled_ui(config.manual, |ui| {
            ui.add_space(METRICS.item_gap);
            ui.label(style::field_label("行"));
            ui.add_sized(
                egui::vec2(56.0, METRICS.field_height),
                egui::DragValue::new(&mut config.rows).range(1..=64),
            );
            ui.label(style::field_label("列"));
            ui.add_sized(
                egui::vec2(56.0, METRICS.field_height),
                egui::DragValue::new(&mut config.cols).range(1..=64),
            );
        });
    });
}

fn draw_connect_action_row(
    ui: &mut egui::Ui,
    conn_state: ConnectionState,
    is_connected: bool,
    config: &ConnectPanelState,
    connection: &ConnectionManager,
    recorder: &Recorder,
) {
    ui.horizontal_wrapped(|ui| {
        let status_text = match conn_state {
            ConnectionState::Disconnected => "未连接",
            ConnectionState::Connecting => "连接中...",
            ConnectionState::Connected => "已连接",
            ConnectionState::Streaming => "采集中",
            ConnectionState::Error => "连接错误",
        };
        let status_color = match conn_state {
            ConnectionState::Disconnected => ONE_DARK_PRO.text_dim,
            ConnectionState::Connecting => style::status_color_warn(),
            ConnectionState::Connected => style::status_color_ok(),
            ConnectionState::Streaming => style::status_color_info(),
            ConnectionState::Error => style::status_color_error(),
        };

        ui.colored_label(status_color, status_text);
        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
            let button = if is_connected {
                style::danger_button("断开")
            } else {
                style::primary_button("连接")
            };

            if ui.add(button).clicked() {
                if is_connected {
                    connection.disconnect();
                } else if !config.selected_port.is_empty() {
                    connection.connect(
                        &config.selected_port,
                        config.rows as u32,
                        config.cols as u32,
                        config.mode.baud_rate(),
                        config.mode.protocol(),
                        recorder.clone(),
                    );
                }
            }
        });
    });
}

pub fn draw_config_panel(
    ctx: &egui::Context,
    _panel: &mut FloatingPanelState,
    config: &mut ConfigPanelState,
    connection: &ConnectionManager,
    recorder: &Recorder,
    _stats: SerialIoStats,
    sample_rate_hz: f32,
    render_rate_hz: f32,
) -> Option<SerialMode> {
    let changed_mode = None;
    let conn_state = connection.state();
    config.connected = matches!(
        conn_state,
        ConnectionState::Connected | ConnectionState::Streaming
    );

    let screen = ctx.content_rect();
    let bar_rect = egui::Rect::from_min_size(
        egui::pos2(screen.left(), screen.top() + layout::TITLE_BAR_HEIGHT),
        egui::vec2(screen.width(), layout::CONFIG_BAR_HEIGHT),
    );

    egui::Area::new(egui::Id::new("config_top_bar"))
        .fixed_pos(bar_rect.min)
        .order(egui::Order::Foreground)
        .show(ctx, |ui| {
            ui.set_min_size(bar_rect.size());
            ui.painter().rect_filled(
                ui.max_rect(),
                egui::CornerRadius::ZERO,
                ONE_DARK_PRO.panel_deep,
            );
            ui.painter().line_segment(
                [ui.max_rect().left_top(), ui.max_rect().right_top()],
                egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
            );
            ui.painter().line_segment(
                [ui.max_rect().left_bottom(), ui.max_rect().right_bottom()],
                egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
            );

            ui.add_space(5.0);
            ui.horizontal(|ui| {
                ui.add_space(8.0);
                ui.spacing_mut().item_spacing = egui::vec2(8.0, 0.0);
                config.mode = SerialMode::Hand;
                config.baud_rate = SerialMode::Hand.baud_rate();

                ui.colored_label(dim_text(), "配置面板");
                ui.add_space(8.0);
                ui.colored_label(dim_text(), "模式");
                ui.add_sized(
                    egui::vec2(102.0, 28.0),
                    style::mode_button("手掌模块", true),
                );

                ui.add_space(12.0);
                ui.label(style::field_label("端口"));
                egui::ComboBox::from_id_salt("config_top_bar_ports")
                    .width(180.0)
                    .selected_text(if config.port.is_empty() {
                        "无可用串口".to_owned()
                    } else {
                        config.port.clone()
                    })
                    .show_ui(ui, |ui| {
                        for port in &config.available_ports {
                            ui.selectable_value(&mut config.port, port.clone(), port.as_str());
                        }
                    });

                if ui
                    .add(style::icon_button(
                        egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0),
                        METRICS.icon_button,
                    ))
                    .on_hover_text("刷新串口")
                    .clicked()
                {
                    refresh_config_ports(config);
                }

                ui.add_space(10.0);
                ui.label(style::value_text(format!("波特率  {}", config.baud_rate)));
                ui.add_space(10.0);
                ui.colored_label(dim_text(), "状态");
                ui.colored_label(
                    connection_status_color(conn_state),
                    connection_status_text(conn_state),
                );

                ui.add_space(8.0);
                let is_connected = matches!(
                    conn_state,
                    ConnectionState::Connected | ConnectionState::Streaming
                );
                let button_text = if is_connected { "断开" } else { "连接" };
                if ui
                    .add_sized(
                        egui::vec2(118.0, 28.0),
                        if is_connected {
                            style::danger_button(button_text)
                        } else {
                            style::primary_button(button_text)
                        },
                    )
                    .clicked()
                {
                    if is_connected {
                        connection.disconnect();
                    } else if !config.port.is_empty() {
                        connection.connect(
                            &config.port,
                            12,
                            7,
                            config.mode.baud_rate(),
                            config.mode.protocol(),
                            recorder.clone(),
                        );
                    }
                }

                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                    ui.label(style::value_text(format!("{render_rate_hz:.1} Hz")));
                    ui.colored_label(dim_text(), "画面刷新率");
                    ui.add_space(18.0);
                    ui.label(style::value_text(format!("{sample_rate_hz:.1} Hz")));
                    ui.colored_label(dim_text(), "采样率");
                });
            });
        });

    changed_mode
}

pub fn draw_spatial_force_panel(
    ctx: &egui::Context,
    force: Option<HudSpatialForce>,
    force_history: &[f32],
) {
    let Some(force) = force else {
        return;
    };

    let target_visible = has_recent_resultant_force(force_history);
    let anim = ctx.animate_bool(egui::Id::new("spatial_force_panel_enter"), target_visible);
    if anim <= 0.01 {
        return;
    }

    let target_anim = if target_visible { 1.0 } else { 0.0 };
    if (anim - target_anim).abs() > 0.001 {
        ctx.request_repaint();
    }

    let alpha = (anim * 242.0).round() as u8;
    let panel_width = responsive_panel_width(ctx, 380.0, 260.0);
    let panel_height = responsive_panel_height(ctx, 340.0, 220.0);
    let viewport = viewport_panel_rect(ctx);
    let pos = egui::pos2(
        viewport.right() - panel_width,
        viewport.bottom() - panel_height - PANEL_VIEWPORT_MARGIN,
    );

    egui::Area::new(egui::Id::new("spatial_force_panel"))
        .fixed_pos(pos)
        .constrain_to(viewport)
        .order(egui::Order::Foreground)
        .show(ctx, |ui| {
            faded_panel_frame(ctx, alpha).show(ui, |ui| {
                ui.set_min_width(panel_width);
                ui.set_max_width(panel_width);
                ui.set_min_height(panel_height);

                let text_dim = color_alpha(ONE_DARK_PRO.text_dim, alpha);
                let accent = color_alpha(ACCENT_ORANGE, alpha);
                let green = color_alpha(ACCENT_GREEN, alpha);

                ui.horizontal(|ui| {
                    ui.colored_label(accent, "3D FORCE");
                    ui.label(egui::RichText::new("三维力方向").color(text_dim).size(17.0));
                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                        ui.colored_label(
                            green,
                            egui::RichText::new("LIVE").color(green).size(12.0),
                        );
                    });
                });
                ui.separator();

                ui.vertical_centered(|ui| {
                    let gauge_size = (panel_height - 52.0).min(panel_width - 24.0).max(168.0);
                    let (rect, _) = ui.allocate_exact_size(
                        egui::vec2(gauge_size, gauge_size),
                        egui::Sense::hover(),
                    );
                    paint_spatial_force_gauge(ui.painter(), rect, force, alpha);
                });
            });
        });
}

fn draw_mode_row(ui: &mut egui::Ui, config: &mut ConfigPanelState) -> Option<SerialMode> {
    config.mode = SerialMode::Hand;
    config.baud_rate = SerialMode::Hand.baud_rate();
    ui.horizontal_wrapped(|ui| {
        ui.colored_label(dim_text(), "Mode");
        ui.add_space(12.0);
        ui.label(style::panel_title("Hand Gateway"));
    });
    None
}

fn draw_connection_row(
    ui: &mut egui::Ui,
    config: &mut ConfigPanelState,
    connection: &ConnectionManager,
    recorder: &Recorder,
    conn_state: ConnectionState,
) {
    let is_narrow = narrow_panel(ui);
    let draw_port_status = |ui: &mut egui::Ui, config: &mut ConfigPanelState| {
        ui.horizontal_wrapped(|ui| {
            ui.label(style::field_label("端口"));
            egui::ComboBox::from_id_salt("config_ports")
                .width(ui.available_width().min(150.0).max(104.0))
                .selected_text(if config.port.is_empty() {
                    "无可用串口".to_owned()
                } else {
                    config.port.clone()
                })
                .show_ui(ui, |ui| {
                    for port in &config.available_ports {
                        ui.selectable_value(&mut config.port, port.clone(), port.as_str());
                    }
                });

            if ui
                .add(style::icon_button(
                    egui::RichText::new("⟳").color(ONE_DARK_PRO.text).size(16.0),
                    METRICS.icon_button,
                ))
                .on_hover_text("刷新串口")
                .clicked()
            {
                refresh_config_ports(config);
            }
        });
        ui.label(style::value_text(format!("波特率  {}", config.baud_rate)));
        ui.colored_label(
            connection_status_color(conn_state),
            connection_status_text(conn_state),
        );
    };

    let draw_connect_button = |ui: &mut egui::Ui, config: &ConfigPanelState| {
        let is_connected = matches!(
            conn_state,
            ConnectionState::Connected | ConnectionState::Streaming
        );
        let button_text = if is_connected { "断开" } else { "连接" };
        if ui
            .add(if is_connected {
                style::danger_button(button_text)
            } else {
                style::primary_button(button_text)
            })
            .clicked()
        {
            if is_connected {
                connection.disconnect();
            } else if !config.port.is_empty() {
                connection.connect(
                    &config.port,
                    12,
                    7,
                    config.mode.baud_rate(),
                    config.mode.protocol(),
                    recorder.clone(),
                );
            }
        }
    };

    if is_narrow {
        ui.vertical(|ui| {
            draw_port_status(ui, config);
            ui.add_space(METRICS.item_gap);
            draw_connect_button(ui, config);
        });
    } else {
        ui.horizontal(|ui| {
            ui.add(
                egui::Image::new(egui::include_image!("../static/cpu.png"))
                    .fit_to_exact_size(egui::vec2(72.0, 72.0)),
            );

            ui.add_space(METRICS.item_gap);

            ui.vertical(|ui| {
                draw_port_status(ui, config);
            });

            ui.add_space(22.0);
            draw_connect_button(ui, config);
        });
    }

    // Legacy reconnect/link-protection indicator:
    // ui.add_space(18.0);
    // ui.colored_label(
    //     if config.auto_reconnect {
    //         style::status_color_ok()
    //     } else {
    //         ONE_DARK_PRO.text_dim
    //     },
    //     if config.auto_reconnect {
    //         "链路保护 开"
    //     } else {
    //         "链路保护 关"
    //     },
    // );
}

fn draw_serial_grid(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
    group_frame().show(ui, |ui| {
        egui::Grid::new("serial_config_grid")
            .num_columns(4)
            .spacing(egui::vec2(14.0, 7.0))
            .striped(true)
            .show(ui, |ui| {
                ui.label(style::field_label("端口"));
                ui.add_sized(
                    egui::vec2(110.0, METRICS.field_height),
                    egui::TextEdit::singleline(&mut config.port),
                );

                ui.label(style::field_label("波特率"));
                baud_combo(ui, config);
                ui.end_row();

                ui.label(style::field_label("数据位"));
                ui.add_sized(
                    egui::vec2(70.0, METRICS.field_height),
                    egui::DragValue::new(&mut config.data_bits).range(5..=8),
                );

                ui.label(style::field_label("校验"));
                parity_combo(ui, config);
                ui.end_row();

                ui.label(style::field_label("停止位"));
                ui.add_sized(
                    egui::vec2(70.0, METRICS.field_height),
                    egui::DragValue::new(&mut config.stop_bits).range(1..=2),
                );

                ui.label(style::field_label("超时"));
                ui.horizontal(|ui| {
                    ui.add_sized(
                        egui::vec2(84.0, METRICS.field_height),
                        egui::DragValue::new(&mut config.timeout_ms)
                            .range(50..=30_000)
                            .speed(50),
                    );
                    ui.colored_label(dim_text(), "毫秒");
                });
                ui.end_row();
            });
    });
}

fn draw_mode_body(
    ui: &mut egui::Ui,
    config: &mut ConfigPanelState,
    conn_state: ConnectionState,
    stats: SerialIoStats,
    sample_rate_hz: f32,
    render_rate_hz: f32,
) {
    let _ = (config.mode, conn_state, stats);
    draw_realtime_rate_row(ui, sample_rate_hz, render_rate_hz);
}

fn draw_realtime_rate_row(ui: &mut egui::Ui, sample_rate_hz: f32, render_rate_hz: f32) {
    group_frame().show(ui, |ui| {
        ui.horizontal_wrapped(|ui| {
            ui.colored_label(dim_text(), "实时采样率");
            ui.label(style::value_text(format!("{sample_rate_hz:.1} Hz")));
            ui.add_space(18.0);
            ui.colored_label(dim_text(), "画面刷新率");
            ui.label(style::value_text(format!("{render_rate_hz:.1} Hz")));
        });
    });
}

fn draw_status_bytes_row(ui: &mut egui::Ui, conn_state: ConnectionState, stats: SerialIoStats) {
    ui.horizontal_wrapped(|ui| {
        ui.colored_label(dim_text(), "状态");
        ui.label(connection_status_text(conn_state));
        ui.colored_label(dim_text(), "接收");
        ui.label(format!("{} 字节", stats.rx_bytes));
        ui.colored_label(dim_text(), "发送");
        ui.label(format!("{} 字节", stats.tx_bytes));
    });
}

fn refresh_config_ports(config: &mut ConfigPanelState) {
    if let Ok(ports) = serial_enum() {
        if !ports.contains(&config.port) {
            config.port = ports.first().cloned().unwrap_or_default();
        }
        config.available_ports = ports;
    }
}

fn connection_status_text(state: ConnectionState) -> &'static str {
    match state {
        ConnectionState::Disconnected => "未连接",
        ConnectionState::Connecting => "连接中...",
        ConnectionState::Connected => "已连接",
        ConnectionState::Streaming => "采集中",
        ConnectionState::Error => "连接错误",
    }
}

fn connection_status_color(state: ConnectionState) -> egui::Color32 {
    match state {
        ConnectionState::Disconnected => style::status_color_error(),
        ConnectionState::Connecting => style::status_color_warn(),
        ConnectionState::Connected | ConnectionState::Streaming => style::status_color_ok(),
        ConnectionState::Error => style::status_color_error(),
    }
}

pub fn icon_button<'a>(
    ui: &mut egui::Ui,
    icon: IconButtonIcon<'a>,
    tooltip: impl Into<egui::WidgetText>,
) -> egui::Response {
    icon_button_sized(ui, icon, tooltip, egui::vec2(28.0, 24.0))
}

pub fn icon_button_sized<'a>(
    ui: &mut egui::Ui,
    icon: IconButtonIcon<'a>,
    tooltip: impl Into<egui::WidgetText>,
    size: egui::Vec2,
) -> egui::Response {
    let button = match icon {
        IconButtonIcon::Font(icon) => egui::Button::new(
            egui::RichText::new(icon)
                .color(egui::Color32::WHITE)
                .size(size.y - 8.0),
        ),
        IconButtonIcon::Png(source) => egui::Button::image(
            egui::Image::new(source).fit_to_exact_size(egui::vec2(size.y - 8.0, size.y - 8.0)),
        ),
    }
    .fill(ONE_DARK_PRO.panel_strong)
    .stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
    .corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
    .min_size(size);

    ui.add(button).on_hover_text(tooltip)
}

fn mode_button(
    ui: &mut egui::Ui,
    mode: &mut SerialMode,
    value: SerialMode,
    label: &'static str,
) -> bool {
    let clicked = ui.add(style::mode_button(label, *mode == value)).clicked();

    if clicked && *mode != value {
        *mode = value;
        true
    } else {
        false
    }
}

fn baud_combo(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
    egui::ComboBox::from_id_salt("serial_baud_rate")
        .width(110.0)
        .selected_text(config.baud_rate.to_string())
        .show_ui(ui, |ui| {
            for baud_rate in [
                9_600, 19_200, 38_400, 57_600, 115_200, 230_400, 460_800, 921_600, 1_152_000,
            ] {
                ui.selectable_value(&mut config.baud_rate, baud_rate, baud_rate.to_string());
            }
        });
}

fn parity_combo(ui: &mut egui::Ui, config: &mut ConfigPanelState) {
    egui::ComboBox::from_id_salt("serial_parity")
        .width(110.0)
        .selected_text(match config.parity {
            Parity::None => "无",
            Parity::Odd => "奇",
            Parity::Even => "偶",
        })
        .show_ui(ui, |ui| {
            ui.selectable_value(&mut config.parity, Parity::None, "无");
            ui.selectable_value(&mut config.parity, Parity::Odd, "奇");
            ui.selectable_value(&mut config.parity, Parity::Even, "偶");
        });
}

pub fn draw_stats_panel(
    ctx: &egui::Context,
    panel: &mut FloatingPanelState,
    force_history: &[f32],
    _spatial_force: Option<HudSpatialForce>,
) {
    const PREFERRED_PANEL_WIDTH: f32 = 380.0;
    const PREFERRED_PANEL_HEIGHT: f32 = 340.0;
    const PANEL_OUTSIDE_GAP: f32 = 18.0;

    let force_active = has_recent_resultant_force(force_history);
    let target_visible = panel.visible && force_active;
    let anim = ctx.animate_bool(egui::Id::new("stats_panel_force_enter"), target_visible);

    if anim <= 0.01 {
        return;
    }

    let target_anim = if target_visible { 1.0 } else { 0.0 };
    if (anim - target_anim).abs() > 0.001 {
        ctx.request_repaint();
    }

    let eased = ease_in_out(anim);
    let screen = ctx.content_rect();
    let panel_width = responsive_panel_width(ctx, PREFERRED_PANEL_WIDTH, 260.0);
    let panel_height = responsive_panel_height(ctx, PREFERRED_PANEL_HEIGHT, 220.0);
    let viewport = viewport_panel_rect(ctx);
    let target_pos = egui::pos2(
        viewport.left(),
        (viewport.bottom() - panel_height - PANEL_VIEWPORT_MARGIN * 2.0)
            .max(viewport.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN),
    );
    let target_x = target_pos.x;
    let hidden_x = if target_x < screen.center().x {
        screen.left() - panel_width - PANEL_OUTSIDE_GAP
    } else {
        screen.right() + PANEL_OUTSIDE_GAP
    };
    let x = egui::lerp(hidden_x..=target_x, eased);
    let y = target_pos.y;

    egui::Area::new(egui::Id::new("stats_panel"))
        .fixed_pos(egui::pos2(x, y))
        .constrain_to(viewport_panel_rect(ctx))
        .order(egui::Order::Foreground)
        .show(ctx, |ui| {
            panel_frame(ctx).show(ui, |ui| {
                ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
                ui.set_min_width(panel_width);
                ui.set_max_width(panel_width);
                ui.set_min_height(panel_height);
                draw_force_chart_panel_contents(ui, "RF", "合力", force_history, panel_height);
            });
        });
}

const FORCE_CHART_MAX_N: f32 = 25.6;
const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1;
const FORCE_PANEL_ACTIVE_TAIL: usize = 8;
const FORCE_PANEL_EXIT_DELAY_SECONDS: f64 = 3.0;

const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 160.0;
const HAND_FORCE_PANEL_MAX_WIDTH: f32 = 560.0;
const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 72.0;
const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 230.0;
const HAND_FORCE_PANEL_GAP: f32 = 20.0;
const HAND_FORCE_PANEL_SIDE_MARGIN: f32 = 24.0;
const HAND_FORCE_PANEL_VERTICAL_MARGIN: f32 = 28.0;
const HAND_FORCE_CENTER_MIN_WIDTH: f32 = 420.0;
const HAND_FORCE_CENTER_MAX_WIDTH: f32 = 820.0;
const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [
    ("T1", "拇指"),
    ("T2", "食指"),
    ("T3", "中指"),
    ("T4", "无名指"),
    ("T5", "小指"),
    ("P1", "掌心横区"),
    ("P2", "掌心纵区"),
];

fn has_recent_resultant_force(values: &[f32]) -> bool {
    values
        .iter()
        .rev()
        .take(FORCE_PANEL_ACTIVE_TAIL)
        .any(|value| *value > 0.0)
}

pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[Vec<f32>]) {
    let screen = ctx.content_rect();
    let side_margin = HAND_FORCE_PANEL_SIDE_MARGIN.min((screen.width() * 0.025).max(10.0));
    let vertical_margin = HAND_FORCE_PANEL_VERTICAL_MARGIN.min((screen.height() * 0.04).max(10.0));
    let gap = HAND_FORCE_PANEL_GAP.min((screen.height() * 0.024).max(12.0));
    let reserved_center_width = (screen.width() * 0.42)
        .clamp(HAND_FORCE_CENTER_MIN_WIDTH, HAND_FORCE_CENTER_MAX_WIDTH)
        .min((screen.width() - side_margin * 2.0).max(0.0));
    let side_width = ((screen.width() - reserved_center_width - side_margin * 2.0) * 0.5).max(0.0);
    let panel_width = side_width
        .min(HAND_FORCE_PANEL_MAX_WIDTH)
        .max(HAND_FORCE_PANEL_MIN_WIDTH.min(side_width));
    let left_x = screen.left() + side_margin;
    let right_x = screen.right() - side_margin - panel_width;
    let left_count = 4usize;
    let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count;
    let chrome_height = layout::TITLE_BAR_HEIGHT + layout::CONFIG_BAR_HEIGHT;
    let available_height = (screen.height() - chrome_height - vertical_margin * 2.0).max(0.0);
    let panel_height = ((available_height - (left_count - 1) as f32 * gap) / left_count as f32)
        .max(0.0)
        .min(HAND_FORCE_PANEL_MAX_HEIGHT)
        .max(HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height / left_count as f32));
    let left_height = left_count as f32 * panel_height + (left_count - 1) as f32 * gap;
    let right_height =
        right_count as f32 * panel_height + (right_count.saturating_sub(1)) as f32 * gap;
    let min_top = screen.top() + chrome_height + vertical_margin;
    let left_top = (screen.center().y - left_height * 0.5).max(min_top);
    let right_top = (screen.center().y - right_height * 0.5).max(min_top);

    for index in 0..HAND_FORCE_PANEL_TITLES.len() {
        let history = histories.get(index).map(Vec::as_slice).unwrap_or(&[]);
        let (code, title) = HAND_FORCE_PANEL_TITLES[index];
        let (target_x, target_y) = if index < left_count {
            (left_x, left_top + index as f32 * (panel_height + gap))
        } else {
            let right_index = index - left_count;
            (
                right_x,
                right_top + right_index as f32 * (panel_height + gap),
            )
        };

        draw_force_chart_area(
            ctx,
            egui::Id::new(format!("hand_force_panel_{index}")),
            egui::pos2(target_x, target_y),
            egui::vec2(panel_width, panel_height),
            index < left_count,
            visible,
            code,
            title,
            history,
        );
    }
}

fn draw_force_chart_area(
    ctx: &egui::Context,
    id: egui::Id,
    target_pos: egui::Pos2,
    size: egui::Vec2,
    from_left: bool,
    target_visible: bool,
    code: &'static str,
    title: &'static str,
    values: &[f32],
) {
    let now = ctx.input(|input| input.time);
    let active = target_visible && has_recent_resultant_force(values);
    let state_id = id.with("active_state");
    let mut last_active_at = ctx.data_mut(|data| data.get_temp::<f64>(state_id));
    if active {
        last_active_at = Some(now);
        ctx.data_mut(|data| data.insert_temp(state_id, now));
    } else if !target_visible {
        last_active_at = None;
        ctx.data_mut(|data| data.remove::<f64>(state_id));
    }

    let target_visible =
        last_active_at.is_some_and(|active_at| now - active_at <= FORCE_PANEL_EXIT_DELAY_SECONDS);
    let anim_id = id.with("enter");
    let anim_seeded_id = id.with("enter_seeded");
    let anim_seeded = ctx.data_mut(|data| data.get_temp::<bool>(anim_seeded_id).unwrap_or(false));
    if target_visible && !anim_seeded {
        ctx.animate_bool(anim_id, false);
        ctx.data_mut(|data| data.insert_temp(anim_seeded_id, true));
    }

    let anim = ctx.animate_bool(anim_id, target_visible);

    if anim <= 0.01 {
        return;
    }

    let target_anim = if target_visible { 1.0 } else { 0.0 };
    if (anim - target_anim).abs() > 0.001 {
        ctx.request_repaint();
    }

    let eased = ease_in_out(anim);
    let hidden_x = if from_left {
        ctx.content_rect().left() - size.x - HAND_FORCE_PANEL_GAP
    } else {
        ctx.content_rect().right() + HAND_FORCE_PANEL_GAP
    };
    let x = egui::lerp(hidden_x..=target_pos.x, eased);

    egui::Area::new(id)
        .fixed_pos(egui::pos2(x, target_pos.y))
        .constrain_to(viewport_panel_rect(ctx))
        .order(egui::Order::Foreground)
        .show(ctx, |ui| {
            panel_frame(ctx).show(ui, |ui| {
                let padding = METRICS.panel_padding as f32;
                let inner_width = (size.x - padding * 2.0).max(80.0);
                let inner_height = (size.y - padding * 2.0).max(48.0);
                ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
                ui.set_min_width(inner_width);
                ui.set_max_width(inner_width);
                ui.set_min_height(inner_height);
                draw_force_chart_panel_contents(ui, code, title, values, inner_height);
            });
        });
}

fn draw_force_chart_panel_contents(
    ui: &mut egui::Ui,
    code: &'static str,
    title: &'static str,
    values: &[f32],
    content_height: f32,
) {
    let latest = values.last().copied().unwrap_or(0.0);

    ui.horizontal(|ui| {
        ui.colored_label(ACCENT_BLUE, code);
        ui.label(style::panel_title(title));
        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
            ui.colored_label(ACCENT_GREEN, format!("{latest:.1} N"));
        });
    });

    ui.add_space(6.0);
    let chart_height = (content_height - 32.0).max(36.0);
    paint_resultant_force_chart(ui, values, chart_height);
}

fn paint_resultant_force_chart(ui: &mut egui::Ui, values: &[f32], chart_height: f32) {
    const CHART_POINTS: usize = 42;
    const CHART_WINDOW_SECONDS: f32 = CHART_POINTS as f32 * FORCE_CHART_SAMPLE_SECONDS;

    let width = ui.available_width().max(80.0);
    let (rect, _) = ui.allocate_exact_size(egui::vec2(width, chart_height), egui::Sense::hover());
    let painter = ui.painter_at(rect);
    let radius = egui::CornerRadius::same(5);
    let time = ui.ctx().input(|input| input.time) as f32;

    painter.rect_filled(rect, radius, egui::Color32::from_rgb(10, 18, 22));
    painter.rect_stroke(
        rect,
        radius,
        egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.accent, 95)),
        egui::StrokeKind::Outside,
    );

    let chart_rect = egui::Rect::from_min_max(
        rect.left_top() + egui::vec2(34.0, 12.0),
        rect.right_bottom() - egui::vec2(10.0, 24.0),
    );

    paint_force_grid(&painter, rect, chart_rect, time, CHART_WINDOW_SECONDS);

    if values.is_empty() {
        painter.text(
            chart_rect.center(),
            egui::Align2::CENTER_CENTER,
            "等待合力数据",
            egui::FontId::proportional(12.0),
            ONE_DARK_PRO.text_subtle,
        );
        return;
    }

    ui.ctx().request_repaint();

    let enter = ui
        .ctx()
        .animate_bool(egui::Id::new("resultant_force_chart_enter"), true);
    let slide_offset = (1.0 - enter) * -chart_rect.width() * 0.65;
    let plot_rect = chart_rect.translate(egui::vec2(slide_offset, 0.0));

    let mut points = Vec::with_capacity(values.len());
    let start_slot = CHART_POINTS.saturating_sub(values.len());
    for (index, value) in values.iter().enumerate() {
        let denom = (CHART_POINTS - 1).max(1) as f32;
        let slot = start_slot + index;
        let x = plot_rect.left() + (slot as f32 / denom) * plot_rect.width();
        let normalized = (*value / FORCE_CHART_MAX_N).clamp(0.0, 1.0);
        let y = plot_rect.bottom() - normalized * plot_rect.height();
        points.push(egui::pos2(x, y));
    }

    let plot_painter = painter.with_clip_rect(chart_rect.expand2(egui::vec2(1.0, 1.0)));
    paint_force_area(&plot_painter, &points, plot_rect);

    if points.len() >= 2 {
        plot_painter.add(egui::Shape::line(
            points.clone(),
            egui::Stroke::new(4.0, color_alpha(ONE_DARK_PRO.accent, 45)),
        ));
        plot_painter.add(egui::Shape::line(
            points.clone(),
            egui::Stroke::new(1.7, ONE_DARK_PRO.accent),
        ));
    }

    if let Some(last) = points.last().copied() {
        let pulse = (time * 1.8).fract();
        plot_painter.circle_stroke(
            last,
            5.0 + pulse * 12.0,
            egui::Stroke::new(
                1.0,
                color_alpha(ACCENT_GREEN, ((1.0 - pulse) * 120.0) as u8),
            ),
        );
        plot_painter.circle_filled(last, 3.6, ACCENT_GREEN);
        plot_painter.circle_filled(last, 1.6, egui::Color32::WHITE);
    }
}

fn paint_force_grid(
    painter: &egui::Painter,
    rect: egui::Rect,
    chart_rect: egui::Rect,
    time: f32,
    window_seconds: f32,
) {
    for tick in [25.0, 20.0, 15.0, 10.0, 5.0, 0.0] {
        let normalized = (tick / FORCE_CHART_MAX_N).clamp(0.0, 1.0);
        let y = chart_rect.bottom() - normalized * chart_rect.height();
        painter.line_segment(
            [
                egui::pos2(chart_rect.left(), y),
                egui::pos2(chart_rect.right(), y),
            ],
            egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border, 78)),
        );
        painter.text(
            egui::pos2(rect.left() + 8.0, y),
            egui::Align2::LEFT_CENTER,
            format!("{tick:.0}"),
            egui::FontId::monospace(9.0),
            ONE_DARK_PRO.text_dim,
        );
    }

    for index in 0..=5 {
        let x = chart_rect.left() + index as f32 / 5.0 * chart_rect.width();
        painter.line_segment(
            [
                egui::pos2(x, chart_rect.top()),
                egui::pos2(x, chart_rect.bottom()),
            ],
            egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, 42)),
        );
    }

    let start_time = (time - window_seconds).max(0.0);
    for index in 0..=5 {
        let fraction = index as f32 / 5.0;
        let x = chart_rect.left() + fraction * chart_rect.width();
        let label_time = start_time + fraction * (time - start_time);
        let align = match index {
            0 => egui::Align2::LEFT_CENTER,
            5 => egui::Align2::RIGHT_CENTER,
            _ => egui::Align2::CENTER_CENTER,
        };

        painter.text(
            egui::pos2(x, rect.bottom() - 11.0),
            align,
            format!("{label_time:.1}s"),
            egui::FontId::monospace(9.0),
            ONE_DARK_PRO.text_subtle,
        );
    }
}

fn paint_force_area(painter: &egui::Painter, points: &[egui::Pos2], plot_rect: egui::Rect) {
    if points.len() < 2 {
        return;
    }

    for pair in points.windows(2) {
        let left = pair[0];
        let right = pair[1];
        let area = vec![
            egui::pos2(left.x, plot_rect.bottom()),
            left,
            right,
            egui::pos2(right.x, plot_rect.bottom()),
        ];

        painter.add(egui::Shape::convex_polygon(
            area,
            color_alpha(ONE_DARK_PRO.accent, 32),
            egui::Stroke::NONE,
        ));
    }
}

fn color_alpha(color: egui::Color32, alpha: u8) -> egui::Color32 {
    egui::Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), alpha)
}

fn faded_panel_frame(ctx: &egui::Context, alpha: u8) -> egui::Frame {
    let style = ctx.global_style();
    egui::Frame::window(&style)
        .fill(color_alpha(ONE_DARK_PRO.panel, alpha))
        .stroke(egui::Stroke::new(
            1.0,
            color_alpha(ONE_DARK_PRO.border, alpha),
        ))
        .corner_radius(egui::CornerRadius::same(ONE_DARK_PRO.radius))
        .inner_margin(egui::Margin::same(METRICS.panel_padding))
        .shadow(egui::epaint::Shadow {
            offset: [0, 14],
            blur: 28,
            spread: 0,
            color: egui::Color32::from_black_alpha((alpha as f32 * 0.45) as u8),
        })
}

fn paint_spatial_force_gauge(
    painter: &egui::Painter,
    rect: egui::Rect,
    force: HudSpatialForce,
    alpha: u8,
) {
    let center = rect.center();
    let radius = rect.width().min(rect.height()) * 0.42;
    let angle = force.angle_deg.to_radians();
    let direction = egui::vec2(angle.cos(), angle.sin());
    let magnitude = force.magnitude.clamp(0.0, 1.0);
    let end = center + direction * (radius * (0.35 + magnitude * 0.65));
    let side = egui::vec2(-direction.y, direction.x);

    painter.circle_stroke(
        center,
        radius,
        egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border, alpha)),
    );
    painter.circle_stroke(
        center,
        radius * 0.55,
        egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, alpha / 2)),
    );
    painter.line_segment(
        [
            center + egui::vec2(-radius, 0.0),
            center + egui::vec2(radius, 0.0),
        ],
        egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, alpha / 3)),
    );
    painter.line_segment(
        [
            center + egui::vec2(0.0, -radius),
            center + egui::vec2(0.0, radius),
        ],
        egui::Stroke::new(1.0, color_alpha(ONE_DARK_PRO.border_soft, alpha / 3)),
    );
    painter.circle_filled(
        end,
        12.0,
        color_alpha(ACCENT_ORANGE, (alpha as f32 * 0.16) as u8),
    );
    painter.line_segment(
        [center, end],
        egui::Stroke::new(4.0, color_alpha(ACCENT_ORANGE, alpha)),
    );
    painter.line_segment(
        [end, end - direction * 13.0 + side * 7.0],
        egui::Stroke::new(3.0, color_alpha(ACCENT_ORANGE, alpha)),
    );
    painter.line_segment(
        [end, end - direction * 13.0 - side * 7.0],
        egui::Stroke::new(3.0, color_alpha(ACCENT_ORANGE, alpha)),
    );
    painter.circle_filled(center, 4.5, color_alpha(ONE_DARK_PRO.text_dim, alpha));
    painter.circle_filled(end, 5.5, color_alpha(ACCENT_GREEN, alpha));
    painter.circle_filled(end, 2.2, color_alpha(egui::Color32::WHITE, alpha));
}

fn draw_pinned_panel(
    ctx: &egui::Context,
    _panel: &mut FloatingPanelState,
    title: &'static str,
    id: &'static str,
    preferred_width: f32,
    min_width: f32,
    add_contents: impl FnOnce(&mut egui::Ui),
) {
    let panel_width = responsive_panel_width(ctx, preferred_width, min_width);
    let max_height = responsive_panel_height(ctx, ctx.content_rect().height(), 180.0);
    let viewport = viewport_panel_rect(ctx);
    let x = (viewport.center().x - panel_width * 0.5).clamp(
        viewport.left(),
        (viewport.right() - panel_width).max(viewport.left()),
    );
    let y = viewport.top() + layout::TITLE_BAR_HEIGHT + PANEL_VIEWPORT_MARGIN;
    let pos = egui::pos2(x, y);

    egui::Window::new(title)
        .id(egui::Id::new(id))
        .current_pos(pos)
        .max_width(panel_width)
        .max_height(max_height)
        .constrain_to(viewport_panel_rect(ctx))
        .title_bar(false)
        .resizable(false)
        .frame(panel_frame(ctx))
        .show(ctx, |ui| {
            ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
            ui.set_min_width(panel_width);
            ui.set_max_width(panel_width);
            ui.horizontal(|ui| {
                ui.label(style::panel_title(title));
            });
            ui.separator();
            let content_max_height = (max_height - 60.0).max(120.0);
            egui::ScrollArea::vertical()
                .max_height(content_max_height)
                .auto_shrink([false, true])
                .show(ui, |ui| {
                    ui.set_min_width(panel_width);
                    ui.set_max_width(panel_width);
                    add_contents(ui);
                });
        });
}

fn draw_floating_panel(
    ctx: &egui::Context,
    panel: &mut FloatingPanelState,
    title: &'static str,
    id: &'static str,
    preferred_width: f32,
    min_width: f32,
    add_contents: impl FnOnce(&mut egui::Ui),
) {
    if panel.visible {
        let mut open = true;
        let mut hide_requested = false;
        let mut window_rect = None;
        let panel_width = responsive_panel_width(ctx, preferred_width, min_width);
        let max_height = responsive_panel_height(ctx, ctx.content_rect().height(), 180.0);
        let default_pos = responsive_panel_pos(ctx, panel.default_pos, panel_width);

        let window_response = egui::Window::new(title)
            .id(egui::Id::new(id))
            .open(&mut open)
            .default_pos(default_pos)
            .max_width(panel_width)
            .max_height(max_height)
            .constrain_to(viewport_panel_rect(ctx))
            .title_bar(false)
            .resizable(true)
            .frame(panel_frame(ctx))
            .show(ctx, |ui| {
                ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
                ui.set_min_width(panel_width);
                ui.set_max_width(panel_width);
                ui.horizontal(|ui| {
                    if ui
                        .add(rich_tag_button("隐藏", style::ONE_DARK_PRO.text_dim))
                        .clicked()
                    {
                        hide_requested = true;
                    }
                    ui.add_space(6.0);
                    ui.label(style::panel_title(title));
                });
                ui.separator();
                let content_max_height = (max_height - 60.0).max(120.0);
                egui::ScrollArea::vertical()
                    .max_height(content_max_height)
                    .auto_shrink([false, true])
                    .show(ui, |ui| {
                        ui.set_min_width(panel_width);
                        ui.set_max_width(panel_width);
                        add_contents(ui);
                    });
            });

        if let Some(response) = window_response {
            window_rect = Some(response.response.rect);
        }

        if hide_requested {
            if let Some(rect) = window_rect {
                let screen = ctx.content_rect();
                let tag_size = egui::vec2(86.0, 22.0);

                let distance_to_left = rect.left();
                let distance_to_right = screen.right() - rect.right();

                let x = if distance_to_left <= distance_to_right {
                    screen.left()
                } else {
                    screen.right() - tag_size.x
                };

                let y = rect.top().clamp(screen.top(), screen.bottom() - tag_size.y);

                panel.tag_pos = egui::pos2(x, y);
            }
        }
        panel.visible = open && !hide_requested;
    }
    // else {
    //     let response = egui::Area::new(egui::Id::new(format!("{id}_tag")))
    //         .current_pos(panel.tag_pos)
    //         .movable(true)
    //         .order(egui::Order::Foreground)
    //         .show(ctx, |ui| {
    //             ui.set_min_width(86.0);
    //             if ui
    //                 .add(tag_button(format!("▸ {title}")).min_size(egui::vec2(86.0, 22.0)))
    //                 .clicked()
    //             {
    //                 panel.visible = true;
    //             }
    //         });

    //     panel.tag_pos = response.response.rect.min;
    // }
}

fn draw_center_floating_panel(
    ctx: &egui::Context,
    panel: &mut FloatingPanelState,
    id: &'static str,
    top_offset: f32,
    collapsed_label: &'static str,
    add_contents: impl FnOnce(&mut egui::Ui),
) {
    const PREFERRED_PANEL_WIDTH: f32 = 520.0;
    const SLIDE_DISTANCE: f32 = 18.0;

    let panel_width = responsive_panel_width(ctx, PREFERRED_PANEL_WIDTH, 320.0);
    let anim = advance_center_panel_anim(ctx, panel);
    let eased = ease_in_out(anim);

    if !panel.visible || anim < 0.28 {
        let handle_y = top_offset - (1.0 - eased) * 2.0;
        egui::Area::new(egui::Id::new(format!("{id}_handle")))
            .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, handle_y))
            .order(egui::Order::Tooltip)
            .show(ctx, |ui| {
                let handle_rect = ui
                    .allocate_exact_size(egui::vec2(112.0, 24.0), egui::Sense::hover())
                    .0;
                if integrated_handle(ui, handle_rect, collapsed_label, false, "打开连接面板")
                    .clicked()
                {
                    panel.visible = true;
                }
            });
    }

    if anim <= 0.02 {
        return;
    }

    egui::Area::new(egui::Id::new(id))
        .anchor(
            egui::Align2::CENTER_TOP,
            egui::vec2(0.0, top_offset - (1.0 - eased) * SLIDE_DISTANCE),
        )
        .order(egui::Order::Tooltip)
        .show(ctx, |ui| {
            let response = center_panel_shell(ui, panel_width, |ui| {
                ui.set_width(panel_width);
                add_contents(ui);
                ui.add_space(6.0);
                let handle_rect = allocate_center_panel_handle(ui);
                if integrated_handle(ui, handle_rect, "", true, "收起连接面板").clicked() {
                    panel.visible = false;
                }
                handle_rect
            });
            paint_integrated_center_panel(ui, response.response.rect, response.inner, true);
        });
}

fn advance_center_panel_anim(ctx: &egui::Context, panel: &mut FloatingPanelState) -> f32 {
    let target = if panel.visible { 1.0 } else { 0.0 };
    let now = ctx.input(|input| input.time);
    let delta_time = panel
        .center_anim_last_time
        .map(|last_time| (now - last_time) as f32)
        .unwrap_or(1.0 / 60.0)
        .clamp(0.0, 0.05);

    panel.center_anim_last_time = Some(now);
    panel.center_anim_target = panel.visible;

    let speed = 7.5;
    let step = speed * delta_time;
    if panel.center_anim < target {
        panel.center_anim = (panel.center_anim + step).min(target);
    } else if panel.center_anim > target {
        panel.center_anim = (panel.center_anim - step).max(target);
    }

    if (panel.center_anim - target).abs() > 0.001 {
        ctx.request_repaint();
    }

    panel.center_anim
}

fn ease_in_out(t: f32) -> f32 {
    let t = t.clamp(0.0, 1.0);
    t * t * (3.0 - 2.0 * t)
}

fn center_panel_shell<R>(
    ui: &mut egui::Ui,
    width: f32,
    add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
    style::center_panel_frame().show(ui, |ui| {
        ui.set_width(width);
        add_contents(ui)
    })
}

fn allocate_center_panel_handle(ui: &mut egui::Ui) -> egui::Rect {
    let handle_width = 112.0;
    let handle_height = 24.0;
    let left_space = ((ui.available_width() - handle_width) * 0.5).max(0.0);
    let mut handle_rect = egui::Rect::NOTHING;

    ui.horizontal(|ui| {
        ui.add_space(left_space);
        let (rect, _) = ui.allocate_exact_size(
            egui::vec2(handle_width, handle_height),
            egui::Sense::hover(),
        );
        handle_rect = rect;
    });

    handle_rect
}

fn integrated_handle(
    ui: &mut egui::Ui,
    rect: egui::Rect,
    label: &str,
    expanded: bool,
    tooltip: &'static str,
) -> egui::Response {
    let response = ui
        .interact(
            rect.expand2(egui::vec2(12.0, 5.0)),
            ui.id().with(("center_panel_handle", expanded)),
            egui::Sense::click(),
        )
        .on_hover_text(tooltip);

    paint_integrated_handle(ui, rect, label, expanded, response.hovered());
    response
}

fn paint_integrated_handle(
    ui: &mut egui::Ui,
    rect: egui::Rect,
    label: &str,
    expanded: bool,
    hovered: bool,
) {
    let center = rect.center();
    if !expanded {
        let fill = if hovered {
            ONE_DARK_PRO.panel_strong
        } else {
            egui::Color32::from_rgb(31, 41, 52)
        };
        let points = vec![
            egui::pos2(rect.left() - 12.0, rect.top() - 1.0),
            egui::pos2(rect.right() + 12.0, rect.top() - 1.0),
            egui::pos2(rect.right() + 3.0, rect.bottom() + 8.0),
            egui::pos2(rect.left() - 3.0, rect.bottom() + 8.0),
        ];
        ui.painter().add(egui::Shape::convex_polygon(
            points.clone(),
            fill,
            egui::Stroke::new(1.0, ONE_DARK_PRO.border),
        ));
        ui.painter().line_segment(
            [points[2], points[3]],
            egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
        );
    }

    let arrow = if expanded { -1.0 } else { 1.0 };
    let arrow_center_x = if label.is_empty() {
        center.x
    } else {
        rect.right() - 18.0
    };
    let arrow_center = egui::pos2(arrow_center_x, center.y);
    let arrow_color = if hovered {
        ONE_DARK_PRO.accent_hot
    } else {
        ONE_DARK_PRO.text_dim
    };
    ui.painter().line_segment(
        [
            arrow_center + egui::vec2(-6.0, -2.0 * arrow),
            arrow_center + egui::vec2(0.0, 4.0 * arrow),
        ],
        egui::Stroke::new(1.7, arrow_color),
    );
    ui.painter().line_segment(
        [
            arrow_center + egui::vec2(6.0, -2.0 * arrow),
            arrow_center + egui::vec2(0.0, 4.0 * arrow),
        ],
        egui::Stroke::new(1.7, arrow_color),
    );

    if !label.is_empty() {
        let galley = ui.fonts_mut(|fonts| {
            fonts.layout_no_wrap(
                label.to_owned(),
                egui::FontId::proportional(12.0),
                ONE_DARK_PRO.text,
            )
        });
        ui.painter().galley(
            egui::pos2(rect.left() + 14.0, center.y - galley.size().y * 0.5 - 1.0),
            galley,
            ONE_DARK_PRO.text,
        );
    }
}

fn paint_integrated_center_panel(
    ui: &egui::Ui,
    rect: egui::Rect,
    handle_rect: egui::Rect,
    handle_on_bottom: bool,
) {
    let painter = ui.painter();
    let stroke = egui::Stroke::new(1.0, ONE_DARK_PRO.border);
    let accent_stroke = egui::Stroke::new(1.2, ONE_DARK_PRO.border_soft);
    let top = rect.top();
    let left = rect.left();
    let right = rect.right();
    let bottom = rect.bottom();
    let radius = ONE_DARK_PRO.radius as f32;
    let tab_left = handle_rect.left() - 18.0;
    let tab_right = handle_rect.right() + 18.0;
    let tab_recess = if handle_on_bottom {
        handle_rect.top() - 2.0
    } else {
        handle_rect.bottom() + 2.0
    };

    if handle_on_bottom {
        painter.line_segment(
            [
                egui::pos2(left + radius, top),
                egui::pos2(right - radius, top),
            ],
            stroke,
        );
    } else {
        painter.line_segment(
            [egui::pos2(left + radius, top), egui::pos2(tab_left, top)],
            stroke,
        );
        painter.line_segment(
            [egui::pos2(tab_right, top), egui::pos2(right - radius, top)],
            stroke,
        );
    }
    painter.line_segment(
        [
            egui::pos2(left, top + radius),
            egui::pos2(left, bottom - radius),
        ],
        stroke,
    );
    painter.line_segment(
        [
            egui::pos2(right, top + radius),
            egui::pos2(right, bottom - radius),
        ],
        stroke,
    );
    if handle_on_bottom {
        painter.line_segment(
            [
                egui::pos2(left + radius, bottom),
                egui::pos2(tab_left, bottom),
            ],
            stroke,
        );
        painter.line_segment(
            [
                egui::pos2(tab_right, bottom),
                egui::pos2(right - radius, bottom),
            ],
            stroke,
        );
    } else {
        painter.line_segment(
            [
                egui::pos2(left + radius, bottom),
                egui::pos2(right - radius, bottom),
            ],
            stroke,
        );
    }

    let tab_points = if handle_on_bottom {
        vec![
            egui::pos2(tab_left, bottom),
            egui::pos2(handle_rect.left() - 8.0, tab_recess),
            egui::pos2(handle_rect.right() + 8.0, tab_recess),
            egui::pos2(tab_right, bottom),
        ]
    } else {
        vec![
            egui::pos2(tab_left, top),
            egui::pos2(handle_rect.left() - 8.0, tab_recess),
            egui::pos2(handle_rect.right() + 8.0, tab_recess),
            egui::pos2(tab_right, top),
        ]
    };
    painter.add(egui::Shape::line(tab_points, accent_stroke));
}

// ── Signal Chart (sparkline) ───────────────────────────────────────────

/// Draw a mini sparkline chart with current/min/max labels.
pub fn draw_signal_chart(ui: &mut egui::Ui, values: &[f32], label: &str, color: egui::Color32) {
    let chart_height = 48.0;
    let chart_width = ui.available_width().min(200.0);

    group_frame().show(ui, |ui| {
        ui.horizontal(|ui| {
            ui.colored_label(color, label);
            if let (Some(&last), Some(&min), Some(&max)) = (
                values.last(),
                values
                    .iter()
                    .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
                values
                    .iter()
                    .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
            ) {
                ui.colored_label(ONE_DARK_PRO.text, format!("{:.0}", last));
                ui.colored_label(ONE_DARK_PRO.text_dim, format!("↓{:.0} ↑{:.0}", min, max));
            }
        });

        let (rect, _response) =
            ui.allocate_exact_size(egui::vec2(chart_width, chart_height), egui::Sense::hover());

        if values.len() < 2 {
            return;
        }

        let painter = ui.painter_at(rect);
        painter.rect_filled(rect, egui::CornerRadius::same(2), ONE_DARK_PRO.panel_deep);

        let min_val = values
            .iter()
            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
            .copied()
            .unwrap_or(0.0);
        let max_val = values
            .iter()
            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
            .copied()
            .unwrap_or(1.0);
        let range = (max_val - min_val).max(1.0);

        let points: Vec<egui::Pos2> = values
            .iter()
            .enumerate()
            .map(|(i, &v)| {
                let x = rect.left() + (i as f32 / (values.len() - 1) as f32) * rect.width();
                let y = rect.bottom() - ((v - min_val) / range) * rect.height();
                egui::pos2(x, y)
            })
            .collect();

        painter.add(egui::Shape::line(points, egui::Stroke::new(1.5, color)));
    });
}

// ── Recording Toolbar ──────────────────────────────────────────────────

/// Draw recording controls inside the connect panel.
pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_path: &mut String) {
    let is_recording = recorder.is_recording();
    let frame_count = recorder.frame_count();
    let duration_ms = recorder.duration_ms();

    ui.separator();
    ui.horizontal_wrapped(|ui| {
        ui.colored_label(ACCENT_RED, "● REC");
        ui.colored_label(
            ONE_DARK_PRO.text,
            format!("{} 帧 | {:.1}s", frame_count, duration_ms as f64 / 1000.0),
        );
    });

    ui.horizontal_wrapped(|ui| {
        // Full recording
        let rec_btn = if is_recording {
            tag_button("⏹ 停止")
        } else {
            style::accent_button(
                egui::RichText::new("● 全量录制").color(egui::Color32::WHITE),
                ACCENT_RED,
            )
        };
        if ui.add(rec_btn).clicked() {
            if is_recording {
                let _ = recorder.stop_recording();
            } else {
                let _ = recorder.start_full_recording();
            }
        }

        ui.add_space(4.0);

        // Snapshot recording
        let snap_btn = if is_recording {
            tag_button("⏹ 快照停止")
        } else {
            style::accent_button(
                egui::RichText::new("✂ 快照录制").color(egui::Color32::WHITE),
                ACCENT_ORANGE,
            )
        };
        if ui.add(snap_btn).clicked() {
            if is_recording {
                let _ = recorder.stop_recording();
            } else {
                let _ = recorder.start_snapshot_recording();
            }
        }

        ui.add_space(4.0);

        // Pause/Resume
        if is_recording {
            if ui.add(tag_button("⏸ 暂停")).clicked() {
                let _ = recorder.pause_recording();
            }
        }

        ui.add_space(8.0);

        // Export
        if ui
            .add(style::accent_button(
                egui::RichText::new("📤 导出CSV").color(egui::Color32::WHITE),
                ACCENT_BLUE,
            ))
            .clicked()
        {
            let path = if export_path.is_empty() {
                let ts = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap_or_default()
                    .as_secs();
                format!("eskin_export_{}.csv", ts)
            } else {
                export_path.clone()
            };
            if let Err(e) = recorder.export_csv(&path) {
                eprintln!("[export] error: {e}");
            }
        }

        // Import
        if ui
            .add(style::accent_button(
                egui::RichText::new("📥 导入CSV").color(egui::Color32::WHITE),
                ACCENT_GREEN,
            ))
            .clicked()
        {
            let import_path = export_path.clone();
            if let Err(e) = recorder.import_csv(&import_path) {
                eprintln!("[import] error: {e}");
            }
        }
    });

    // Export path
    if narrow_panel(ui) {
        ui.vertical(|ui| {
            ui.colored_label(dim_text(), "导出路径");
            ui.add_sized(
                egui::vec2(ui.available_width().max(180.0), METRICS.field_height),
                egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
            );
        });
    } else {
        ui.horizontal(|ui| {
            ui.colored_label(dim_text(), "导出路径");
            ui.add_sized(
                egui::vec2(
                    (ui.available_width() - 80.0).max(160.0),
                    METRICS.field_height,
                ),
                egui::TextEdit::singleline(export_path).hint_text("eskin_export_*.csv"),
            );
        });
    }
}

// ── Export Panel (floating) ────────────────────────────────────────────

/// Draw the export/recording floating panel.
pub fn draw_export_panel(
    ctx: &egui::Context,
    panel: &mut FloatingPanelState,
    recorder: &Recorder,
    export_path: &mut String,
) {
    let panel_width = responsive_panel_width(ctx, 340.0, 280.0);
    draw_floating_panel(
        ctx,
        panel,
        "录制导出",
        "export_panel",
        340.0,
        280.0,
        |ui| {
            ui.set_min_width(panel_width);
            ui.set_max_width(panel_width);
            draw_recording_toolbar(ui, recorder, export_path);
        },
    );
}

// ── Matrix Config Panel ────────────────────────────────────────────────

pub struct MatrixConfigState {
    pub rows: u32,
    pub cols: u32,
    pub color_min: f32,
    pub color_max: f32,
}

impl Default for MatrixConfigState {
    fn default() -> Self {
        Self {
            rows: 12,
            cols: 7,
            color_min: 0.0,
            color_max: 7000.0,
        }
    }
}

/// Draw matrix configuration floating panel.
pub fn draw_matrix_config_panel(
    ctx: &egui::Context,
    panel: &mut FloatingPanelState,
    config: &mut MatrixConfigState,
) {
    let panel_width = responsive_panel_width(ctx, 380.0, 280.0);
    draw_floating_panel(
        ctx,
        panel,
        "矩阵配置",
        "matrix_config_panel",
        380.0,
        280.0,
        |ui| {
            ui.set_min_width(panel_width);
            ui.set_max_width(panel_width);

            ui.horizontal_wrapped(|ui| {
                ui.label(style::field_label("矩阵尺寸"));
                ui.add_space(METRICS.item_gap);
                ui.label(style::value_text("行"));
                ui.add_sized(
                    egui::vec2(56.0, METRICS.field_height),
                    egui::DragValue::new(&mut config.rows).range(1..=128),
                );
                ui.label(style::value_text("列"));
                ui.add_sized(
                    egui::vec2(56.0, METRICS.field_height),
                    egui::DragValue::new(&mut config.cols).range(1..=128),
                );
            });

            ui.add_space(METRICS.item_gap);

            ui.horizontal_wrapped(|ui| {
                ui.label(style::field_label("预设"));
                for (r, c, name) in &[
                    (12, 7, "12×7"),
                    (24, 12, "24×12"),
                    (32, 16, "32×16"),
                    (48, 24, "48×24"),
                    (64, 32, "64×32"),
                ] {
                    if ui
                        .add(rich_tag_button(*name, style::ONE_DARK_PRO.text_dim))
                        .clicked()
                    {
                        config.rows = *r;
                        config.cols = *c;
                    }
                }
            });

            ui.add_space(METRICS.item_gap);

            ui.horizontal_wrapped(|ui| {
                ui.label(style::field_label("色域范围"));
                ui.add_space(METRICS.item_gap);
                ui.label(style::value_text("最小"));
                ui.add_sized(
                    egui::vec2(72.0, METRICS.field_height),
                    egui::DragValue::new(&mut config.color_min)
                        .range(0.0..=10000.0)
                        .speed(100.0),
                );
                ui.label(style::value_text("最大"));
                ui.add_sized(
                    egui::vec2(72.0, METRICS.field_height),
                    egui::DragValue::new(&mut config.color_max)
                        .range(1.0..=10000.0)
                        .speed(100.0),
                );
            });

            ui.add_space(METRICS.row_gap);

            if ui
                .add(rich_tag_button("重置默认", style::ONE_DARK_PRO.text_dim))
                .clicked()
            {
                *config = MatrixConfigState::default();
            }
        },
    );
}

pub fn panel_restore_item(ui: &mut egui::Ui, title: &'static str, panel: &mut FloatingPanelState) {
    let button_size = egui::vec2(ui.available_width(), 0.0);
    if panel.visible {
        ui.add_enabled_ui(false, |ui| {
            ui.add_sized(button_size, egui::Button::new(format!("{title} 已显示")));
        });
    } else if ui
        .add_sized(button_size, egui::Button::new(format!("显示 {title}")))
        .clicked()
    {
        panel.visible = true;
        ui.close();
    }
}
