use crate::breakout::{BreakoutGame, control_from_matrix};
use crate::config::{HandForceConfig, try_load_hand_force_config};
use crate::connection::{ConnectionManager, ConnectionState};
use crate::force::{ForceEstimatorState, HAND_FINGERTIP_COUNT, HudSpatialForce};
use crate::recording::Recorder;
use crate::render::{ActiveMode, FingerMode, HandGatewayMode};
use crate::style::{ONE_DARK_PRO, apply_fonts, apply_theme, layout};
use crate::ui::SerialMode;
use crate::{
    matrix::{MATRIX_COLS, MATRIX_ROWS},
    render::{
        BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, PressureSamples,
        WgpuBackgroundCallback,
    },
    ui::{
        ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
        draw_config_panel, draw_hand_force_panels, panel_restore_item,
    },
};
use eframe::{egui, egui_wgpu};
use std::collections::BTreeMap;
use std::{
    sync::Arc,
    time::{Duration, Instant},
};

const SUMMARY_POINTS_PER_SERIES: usize = 42;
const HAND_FORCE_PANEL_COUNT: usize = 7;
const HAND_FORCE_SEGMENT_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = [84, 84, 84, 84, 84, 70, 44];
const HAND_FORCE_DISPLAY_THRESHOLD_N: f32 = 0.05;
const HAND_IMAGE_SIZE_PX: [f32; 2] = [971.0, 1117.0];
const HAND_FINGERTIP_FORCE_ANCHORS_PX: [[f32; 2]; HAND_FINGERTIP_COUNT] = [
    [265.0, 495.0],
    [359.0, 260.0],
    [482.0, 228.0],
    [596.0, 265.0],
    [693.0, 370.0],
];
const RATE_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
const DATA_IDLE_PANEL_EXIT_DELAY: Duration = Duration::from_secs(3);

pub struct EskinDesktopApp {
    connect_panel: FloatingPanelState,
    connect_state: ConnectPanelState,
    connection: Arc<ConnectionManager>,
    pressure_matrix: PressureFrame,
    hand_pressure: PressureSamples,
    scene_panel: FloatingPanelState,
    config_panel: FloatingPanelState,
    config_state: ConfigPanelState,
    stats_panel: FloatingPanelState,
    // New: recording, export, matrix config, signal charts
    recorder: Recorder,
    export_panel: FloatingPanelState,
    export_path: String,
    matrix_config_panel: FloatingPanelState,
    matrix_config: MatrixConfigState,
    signal_history: Vec<f32>,
    hand_signal_histories: [Vec<f32>; HAND_FORCE_PANEL_COUNT],
    force_estimator: ForceEstimatorState,
    latest_spatial_force: Option<HudSpatialForce>,
    latest_hand_spatial_forces: [Option<HudSpatialForce>; HAND_FINGERTIP_COUNT],
    spatial_force_panel_force: Option<HudSpatialForce>,
    latest_raw_matrix: Vec<u32>,
    latest_matrix_rows: u32,
    latest_matrix_cols: u32,
    last_sample_at: Option<Instant>,
    last_color_debug_at: Option<Instant>,
    breakout_visible: bool,
    breakout_game: BreakoutGame,
    live_rates: LiveRateMeters,
    context_menu_open: bool,
    context_menu_pos: egui::Pos2,
    active_mode: ActiveMode,
    hand_force_config: Option<HandForceConfig>,
}

struct LiveRateMeters {
    window_started_at: Instant,
    window_rx_frames: u64,
    window_render_frames: u32,
    sample_rate_hz: f32,
    render_rate_hz: f32,
}

impl LiveRateMeters {
    fn new() -> Self {
        Self {
            window_started_at: Instant::now(),
            window_rx_frames: 0,
            window_render_frames: 0,
            sample_rate_hz: 0.0,
            render_rate_hz: 0.0,
        }
    }

    fn tick_render(&mut self, total_rx_frames: u64) {
        if total_rx_frames < self.window_rx_frames {
            self.window_rx_frames = total_rx_frames;
            self.sample_rate_hz = 0.0;
        }

        self.window_render_frames = self.window_render_frames.saturating_add(1);

        let now = Instant::now();
        let elapsed = now.duration_since(self.window_started_at);
        if elapsed < RATE_UPDATE_INTERVAL {
            return;
        }

        let seconds = elapsed.as_secs_f32().max(0.001);
        let rx_delta = total_rx_frames.saturating_sub(self.window_rx_frames);
        self.sample_rate_hz = rx_delta as f32 / seconds;
        self.render_rate_hz = self.window_render_frames as f32 / seconds;
        self.window_started_at = now;
        self.window_rx_frames = total_rx_frames;
        self.window_render_frames = 0;
    }
}

impl EskinDesktopApp {
    pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
        egui_extras::install_image_loaders(&cc.egui_ctx);
        apply_fonts(&cc.egui_ctx);
        apply_theme(&cc.egui_ctx, &ONE_DARK_PRO);

        let wgpu_state = cc
            .wgpu_render_state
            .as_ref()
            .expect("need open eframe wgpu renderer feature");

        let mut renderer = wgpu_state.renderer.write();
        renderer
            .callback_resources
            .insert(BackgroundRenderResources::new(
                &wgpu_state.device,
                &wgpu_state.queue,
                &wgpu_state.target_format,
                MATRIX_ROWS,
                MATRIX_COLS,
            ));

        let hand_force_config = try_load_hand_force_config();
        Self {
            connect_panel: FloatingPanelState::new([0.0, 0.0], [layout::RIGHT_TAG_X, 48.0]),
            connect_state: ConnectPanelState::default(),
            connection: Arc::new(ConnectionManager::new()),
            pressure_matrix: [[0.0, 0.0]; PRESSURE_CELL_COUNT],
            hand_pressure: Vec::new(),
            scene_panel: FloatingPanelState::new(
                [layout::LEFT_X, layout::TOP_Y],
                [layout::LEFT_TAG_X, layout::TOP_Y],
            ),
            config_panel: FloatingPanelState::new(
                [layout::RIGHT_X, layout::TOP_Y],
                [layout::RIGHT_TAG_X, layout::TOP_Y],
            ),
            config_state: ConfigPanelState::default(),
            stats_panel: FloatingPanelState::new(
                [layout::LEFT_X, layout::LEFT_SECOND_Y],
                [layout::LEFT_TAG_X, layout::LEFT_SECOND_Y],
            ),
            recorder: Recorder::full(),
            export_panel: FloatingPanelState::new(
                [layout::LEFT_X, layout::TOP_Y],
                [layout::LEFT_TAG_X, layout::TOP_Y],
            ),
            export_path: String::new(),
            matrix_config_panel: FloatingPanelState::new(
                [layout::RIGHT_X, layout::RIGHT_SECOND_Y],
                [layout::RIGHT_TAG_X, layout::RIGHT_SECOND_Y],
            ),
            matrix_config: MatrixConfigState::default(),
            signal_history: Vec::with_capacity(128),
            hand_signal_histories: std::array::from_fn(|_| Vec::with_capacity(128)),
            force_estimator: ForceEstimatorState::new(),
            latest_spatial_force: None,
            latest_hand_spatial_forces: [None; HAND_FINGERTIP_COUNT],
            spatial_force_panel_force: None,
            latest_raw_matrix: Vec::new(),
            latest_matrix_rows: MATRIX_ROWS,
            latest_matrix_cols: MATRIX_COLS,
            last_sample_at: None,
            last_color_debug_at: None,
            breakout_visible: false,
            breakout_game: BreakoutGame::default(),
            live_rates: LiveRateMeters::new(),
            context_menu_open: false,
            context_menu_pos: egui::pos2(24.0, 48.0),
            active_mode: ActiveMode::Hand(HandGatewayMode { range: 0..7000 }),
            hand_force_config,
        }
    }

    fn draw_wgpu_background_rect(&self, ui: &mut egui::Ui, rect: egui::Rect) {
        let width = rect.width().max(1.0);
        let height = rect.height().max(1.0);

        ui.painter().add(egui_wgpu::Callback::new_paint_callback(
            rect,
            WgpuBackgroundCallback {
                width,
                height,
                pressure: self.pressure_matrix,
                hand_pressure: self.hand_pressure.clone(),
                active_mode: self.active_mode.clone(),
            },
        ));
        self.paint_spatial_force_overlay(ui, rect);
    }

    fn paint_spatial_force_overlay(&self, ui: &egui::Ui, rect: egui::Rect) {
        if !self.recent_sample_is_fresh() {
            return;
        }

        if matches!(self.active_mode, ActiveMode::Hand(_)) {
            self.paint_hand_spatial_force_overlay(ui, rect);
            return;
        }

        let Some(force) = self.latest_spatial_force else {
            return;
        };

        let center = rect.center();
        paint_spatial_force_arrow(ui.painter(), center, force, 34.0, 54.0);
    }

    fn paint_hand_spatial_force_overlay(&self, ui: &egui::Ui, rect: egui::Rect) {
        for (force, anchor_px) in self
            .latest_hand_spatial_forces
            .iter()
            .zip(HAND_FINGERTIP_FORCE_ANCHORS_PX)
        {
            let Some(force) = force else {
                continue;
            };

            let center = hand_image_pixel_to_screen(rect, anchor_px);
            paint_spatial_force_arrow(ui.painter(), center, *force, 20.0, 34.0);
        }
    }

    fn strongest_hand_spatial_force(&self) -> Option<HudSpatialForce> {
        self.latest_hand_spatial_forces
            .iter()
            .flatten()
            .copied()
            .max_by(|a, b| a.magnitude.total_cmp(&b.magnitude))
    }

    fn analyze_spatial_force(&mut self, values: &[u32]) {
        if matches!(self.config_state.mode, SerialMode::Hand) {
            let enabled = std::array::from_fn(|tip_index| {
                hand_segment_force(values, tip_index, self.hand_force_config.as_ref())
                    > HAND_FORCE_DISPLAY_THRESHOLD_N
            });
            self.latest_hand_spatial_forces = self
                .force_estimator
                .analyze_fingertips_masked(values, &enabled);
            self.latest_spatial_force = self.strongest_hand_spatial_force();
        } else {
            self.latest_spatial_force = self.force_estimator.analyze(values);
            self.latest_hand_spatial_forces = [None; HAND_FINGERTIP_COUNT];
        }
    }

    fn connection_has_live_data(&self) -> bool {
        matches!(
            self.connection.state(),
            ConnectionState::Connected | ConnectionState::Streaming
        )
    }

    fn recent_sample_is_fresh(&self) -> bool {
        self.last_sample_at
            .is_some_and(|sample_at| sample_at.elapsed() <= DATA_IDLE_PANEL_EXIT_DELAY)
    }

    fn hand_force_panels_visible(&self) -> bool {
        self.connection_has_live_data() && self.recent_sample_is_fresh()
    }

    fn sync_disconnected_live_state(&mut self) {
        if self.connection_has_live_data() {
            return;
        }

        self.last_sample_at = None;
        self.latest_spatial_force = None;
        self.latest_hand_spatial_forces = [None; HAND_FINGERTIP_COUNT];
        self.spatial_force_panel_force = None;
    }

    fn draw_workspace(&mut self, ui: &mut egui::Ui) {
        let screen = ui.max_rect();
        let workspace = egui::Rect::from_min_max(
            screen.left_top()
                + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT + layout::CONFIG_BAR_HEIGHT),
            screen.right_bottom(),
        );
        self.draw_wgpu_background_rect(ui, workspace);
    }

    fn draw_split_workspace(&mut self, ui: &mut egui::Ui) {
        let screen = ui.max_rect();
        let content = egui::Rect::from_min_max(
            screen.left_top() + egui::vec2(0.0, layout::TITLE_BAR_HEIGHT),
            screen.right_bottom(),
        );
        let gutter = 10.0;
        let half_width = (content.width() - gutter).max(1.0) * 0.5;
        let left = egui::Rect::from_min_size(content.min, egui::vec2(half_width, content.height()));
        let right = egui::Rect::from_min_max(
            egui::pos2(left.right() + gutter, content.top()),
            content.right_bottom(),
        );

        ui.painter()
            .rect_filled(screen, egui::CornerRadius::ZERO, ONE_DARK_PRO.bg);
        paint_split_viewport_shell(ui, left, "DOTMATRIX", "压力矩阵 / WGPU");
        paint_split_viewport_shell(ui, right, "BREAKOUT", "压力控制打砖块");

        let left_body = split_viewport_body(left);
        let right_body = split_viewport_body(right);
        self.draw_wgpu_background_rect(ui, left_body);

        let breakout_control = control_from_matrix(
            &self.latest_raw_matrix,
            self.latest_matrix_rows,
            self.latest_matrix_cols,
        );
        self.breakout_game.draw_viewport(
            ui.ctx(),
            right_body,
            &mut self.breakout_visible,
            breakout_control,
        );
    }

    fn update_pressure_matrix(&mut self) {
        if let Some(sample) = self.connection.take_latest_sample() {
            self.last_sample_at = Some(Instant::now());
            normalize_pressure_sample(
                &sample.matrix,
                sample.rows,
                sample.cols,
                &mut self.pressure_matrix,
            );
            self.hand_pressure = normalize_pressure_samples(&sample.matrix);
            self.latest_raw_matrix.clear();
            self.latest_raw_matrix.extend_from_slice(&sample.matrix);
            self.latest_matrix_rows = sample.rows;
            self.latest_matrix_cols = sample.cols;
            self.log_hand_color_mapping_debug(&sample.matrix);

            self.analyze_spatial_force(&sample.matrix);

            let force = update_hand_signal_histories(
                &mut self.hand_signal_histories,
                &sample.matrix,
                self.hand_force_config.as_ref(),
            )
            .min(25.6);
            self.signal_history.push(force);
            if self.signal_history.len() > SUMMARY_POINTS_PER_SERIES {
                self.signal_history.remove(0);
            }
        }
    }

    fn draw_title_bar(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        let title_bar_height = layout::TITLE_BAR_HEIGHT;
        let title_bar_rect = ui
            .allocate_space(egui::vec2(ui.available_width(), title_bar_height))
            .1;

        // Paint background
        ui.painter().rect_filled(
            title_bar_rect,
            egui::CornerRadius::ZERO,
            ONE_DARK_PRO.panel_deep,
        );
        ui.painter().line_segment(
            [title_bar_rect.left_bottom(), title_bar_rect.right_bottom()],
            egui::Stroke::new(1.0, ONE_DARK_PRO.border),
        );

        // Drag-to-move: double-click to maximize, drag to move
        let title_bar_response = ui.interact(
            title_bar_rect,
            egui::Id::new("title_bar"),
            egui::Sense::click_and_drag(),
        );
        if title_bar_response.drag_started() {
            ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
        }
        if title_bar_response.double_clicked() {
            let is_maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false));
            ui.ctx()
                .send_viewport_cmd(egui::ViewportCommand::Maximized(!is_maximized));
        }

        // macOS traffic light buttons (close / minimize / maximize)
        let btn_size = 14.0;
        let btn_spacing = 8.0;
        let btns_start = title_bar_rect.left_center() + egui::vec2(12.0, 0.0);

        let btn_close_center = btns_start;
        let btn_min_center = btns_start + egui::vec2(btn_size + btn_spacing, 0.0);
        let btn_max_center = btns_start + egui::vec2((btn_size + btn_spacing) * 2.0, 0.0);

        // Close (red)
        let close_rect =
            egui::Rect::from_center_size(btn_close_center, egui::vec2(btn_size, btn_size));
        let close_resp = ui.interact(close_rect, egui::Id::new("btn_close"), egui::Sense::click());
        let close_color = egui::Color32::from_rgb(255, 95, 86);
        ui.painter()
            .circle_filled(btn_close_center, btn_size / 2.0, close_color);
        // Draw × when hovered
        if close_resp.hovered() {
            ui.painter().line_segment(
                [
                    btn_close_center + egui::vec2(-3.0, -3.0),
                    btn_close_center + egui::vec2(3.0, 3.0),
                ],
                egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 0, 0)),
            );
            ui.painter().line_segment(
                [
                    btn_close_center + egui::vec2(3.0, -3.0),
                    btn_close_center + egui::vec2(-3.0, 3.0),
                ],
                egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 0, 0)),
            );
        }
        if close_resp.clicked() {
            ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
        }

        // Minimize (yellow)
        let min_rect = egui::Rect::from_center_size(btn_min_center, egui::vec2(btn_size, btn_size));
        let min_resp = ui.interact(min_rect, egui::Id::new("btn_min"), egui::Sense::click());
        let min_color = egui::Color32::from_rgb(255, 189, 46);
        ui.painter()
            .circle_filled(btn_min_center, btn_size / 2.0, min_color);
        if min_resp.hovered() {
            ui.painter().line_segment(
                [
                    btn_min_center + egui::vec2(-3.0, 0.0),
                    btn_min_center + egui::vec2(3.0, 0.0),
                ],
                egui::Stroke::new(1.5, egui::Color32::from_rgb(120, 80, 0)),
            );
        }
        if min_resp.clicked() {
            ui.ctx()
                .send_viewport_cmd(egui::ViewportCommand::Minimized(true));
        }

        // Maximize (green)
        let max_rect = egui::Rect::from_center_size(btn_max_center, egui::vec2(btn_size, btn_size));
        let max_resp = ui.interact(max_rect, egui::Id::new("btn_max"), egui::Sense::click());
        let max_color = egui::Color32::from_rgb(39, 201, 63);
        ui.painter()
            .circle_filled(btn_max_center, btn_size / 2.0, max_color);
        if max_resp.hovered() {
            let s = 3.0;
            ui.painter().rect_stroke(
                egui::Rect::from_center_size(btn_max_center, egui::vec2(s * 2.0, s * 2.0)),
                egui::CornerRadius::same(1),
                egui::Stroke::new(1.5, egui::Color32::from_rgb(0, 80, 10)),
                egui::StrokeKind::Outside,
            );
        }
        if max_resp.clicked() {
            let is_maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false));
            ui.ctx()
                .send_viewport_cmd(egui::ViewportCommand::Maximized(!is_maximized));
        }
    }

    fn draw_floating_panels(
        &mut self,
        ctx: &egui::Context,
        stats: crate::serial_core::serial::SerialIoStats,
    ) {
        self.scene_panel.visible = false;
        self.connect_panel.visible = false;
        self.export_panel.visible = false;
        self.matrix_config_panel.visible = false;
        self.context_menu_open = false;

        if let Some(next_mode) = draw_config_panel(
            ctx,
            &mut self.config_panel,
            &mut self.config_state,
            &self.connection,
            &self.recorder,
            stats,
            self.live_rates.sample_rate_hz,
            self.live_rates.render_rate_hz,
        ) {
            self.switch_mode(next_mode);
        }

        self.stats_panel.visible = false;
        draw_hand_force_panels(
            ctx,
            self.hand_force_panels_visible(),
            &self.hand_signal_histories,
        );
    }

    fn draw_panel_context_menu(&mut self, ctx: &egui::Context) {
        let (secondary_clicked, pointer_pos, escape_pressed) = ctx.input(|input| {
            (
                input.pointer.secondary_clicked(),
                input.pointer.interact_pos(),
                input.key_pressed(egui::Key::Escape),
            )
        });

        if secondary_clicked {
            self.context_menu_open = true;
            self.context_menu_pos = pointer_pos.unwrap_or(self.context_menu_pos);
        }
        if escape_pressed {
            self.context_menu_open = false;
        }
        if !self.context_menu_open {
            return;
        }

        let mut close_menu = false;
        let response = egui::Area::new(egui::Id::new("workspace_context_menu_area"))
            .fixed_pos(self.context_menu_pos)
            .order(egui::Order::Foreground)
            .show(ctx, |ui| {
                egui::Frame::popup(ui.style()).show(ui, |ui| {
                    ui.set_min_width(132.0);
                    ui.label("显示面板");
                    ui.separator();

                    panel_restore_item(ui, "配置", &mut self.config_panel);
                    panel_restore_item(ui, "录制", &mut self.export_panel);
                    panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel);
                    if self.stats_panel.visible {
                        if ui
                            .add_sized(
                                egui::vec2(ui.available_width(), 0.0),
                                egui::Button::new("隐藏 统计"),
                            )
                            .clicked()
                        {
                            self.stats_panel.visible = false;
                            close_menu = true;
                        }
                    } else {
                        panel_restore_item(ui, "统计", &mut self.stats_panel);
                    }
                    ui.separator();

                    if ui
                        .add_sized(
                            egui::vec2(ui.available_width(), 0.0),
                            egui::Button::new("全部显示"),
                        )
                        .clicked()
                    {
                        self.config_panel.visible = true;
                        self.export_panel.visible = true;
                        self.matrix_config_panel.visible = true;
                        self.stats_panel.visible = true;
                        close_menu = true;
                    }
                });
            });

        let clicked_outside = ctx.input(|input| {
            input.pointer.primary_clicked()
                && input
                    .pointer
                    .interact_pos()
                    .is_some_and(|pos| !response.response.rect.contains(pos))
        });
        if close_menu || clicked_outside {
            self.context_menu_open = false;
        }
    }

    fn switch_mode(&mut self, next: SerialMode) {
        self.connect_state.mode = next;
        self.config_state.mode = next;
        self.config_state.baud_rate = next.baud_rate();
        self.connection.disconnect();
        self.pressure_matrix.fill([0.0, 0.0]);
        self.hand_pressure.clear();
        self.last_sample_at = None;
        self.last_color_debug_at = None;
        self.force_estimator.reset();
        self.latest_spatial_force = None;
        self.latest_hand_spatial_forces = [None; HAND_FINGERTIP_COUNT];
        self.spatial_force_panel_force = None;
        self.signal_history.clear();
        self.hand_signal_histories
            .iter_mut()
            .for_each(|history| history.clear());

        self.active_mode = match next {
            SerialMode::Finger => ActiveMode::Finger(FingerMode {
                rows: self.matrix_config.rows,
                cols: self.matrix_config.cols,
                range: 0..7000,
                dot: true,
            }),
            SerialMode::Hand => ActiveMode::Hand(HandGatewayMode { range: 0..7000 }),
        }
    }

    fn log_hand_color_mapping_debug(&mut self, raw: &[u32]) {
        let now = Instant::now();
        if self
            .last_color_debug_at
            .is_some_and(|last| now.duration_since(last) < Duration::from_secs(1))
        {
            return;
        }
        self.last_color_debug_at = Some(now);

        let mut parts = Vec::with_capacity(HAND_FORCE_PANEL_COUNT);
        let mut start = 0;
        for (panel_index, (range, cell_count)) in HAND_SENSOR_PANEL_COLOR_RANGES
            .iter()
            .copied()
            .zip(HAND_SENSOR_PANEL_CELL_COUNTS)
            .enumerate()
        {
            let end = start + cell_count;
            let peak = raw
                .get(start..end)
                .unwrap_or(&[])
                .iter()
                .copied()
                .max()
                .unwrap_or(0);
            let cell_range = range.per_cell(cell_count);
            let [intensity, _] = cell_range.normalize(peak);
            parts.push(format!(
                "{} raw_peak={} range_total_max={:.0} cell_range_max={:.0} intensity={:.3}",
                match panel_index {
                    0..=4 => format!("F{}", panel_index + 1),
                    5 => "Palm-H".to_owned(),
                    _ => "Palm-V".to_owned(),
                },
                peak,
                range.max,
                cell_range.max,
                intensity
            ));
            start = end;
        }
    }
}

fn hand_image_pixel_to_screen(rect: egui::Rect, point_px: [f32; 2]) -> egui::Pos2 {
    let image_uv = egui::vec2(
        point_px[0] / HAND_IMAGE_SIZE_PX[0].max(1.0),
        point_px[1] / HAND_IMAGE_SIZE_PX[1].max(1.0),
    );
    let viewport_aspect = rect.width() / rect.height().max(1.0);
    let image_aspect = HAND_IMAGE_SIZE_PX[0] / HAND_IMAGE_SIZE_PX[1].max(1.0);
    let mut screen_uv = image_uv;

    if viewport_aspect > image_aspect {
        let image_width = image_aspect / viewport_aspect;
        screen_uv.x = image_uv.x * image_width + (1.0 - image_width) * 0.5;
    } else {
        let image_height = viewport_aspect / image_aspect;
        screen_uv.y = image_uv.y * image_height + (1.0 - image_height) * 0.5;
    }

    egui::pos2(
        rect.left() + screen_uv.x * rect.width(),
        rect.top() + screen_uv.y * rect.height(),
    )
}

fn paint_spatial_force_arrow(
    painter: &egui::Painter,
    center: egui::Pos2,
    force: HudSpatialForce,
    base_length: f32,
    magnitude_scale: f32,
) {
    let magnitude = force.magnitude.clamp(0.0, 1.8);
    let length = base_length + magnitude * magnitude_scale;
    let angle = force.angle_deg.to_radians();
    let direction = egui::vec2(angle.cos(), angle.sin());
    let end = center + direction * length;
    let side = egui::vec2(-direction.y, direction.x);
    let head_length = (length * 0.24).clamp(8.0, 14.0);
    let head_width = head_length * 0.5;
    let color = egui::Color32::from_rgb(255, 196, 54);
    let glow = egui::Color32::from_rgba_unmultiplied(255, 196, 54, 58);

    painter.line_segment([center, end], egui::Stroke::new(8.0, glow));
    painter.line_segment([center, end], egui::Stroke::new(2.4, color));
    painter.line_segment(
        [end, end - direction * head_length + side * head_width],
        egui::Stroke::new(2.4, color),
    );
    painter.line_segment(
        [end, end - direction * head_length - side * head_width],
        egui::Stroke::new(2.4, color),
    );
    painter.circle_filled(center, 3.8, color);
}

fn update_hand_signal_histories(
    histories: &mut [Vec<f32>; HAND_FORCE_PANEL_COUNT],
    raw_values: &[u32],
    config: Option<&HandForceConfig>,
) -> f32 {
    let mut total_force = 0.0;
    for (index, history) in histories.iter_mut().enumerate() {
        let force = hand_segment_force(raw_values, index, config);
        let force = if force <= HAND_FORCE_DISPLAY_THRESHOLD_N {
            0.0
        } else {
            force
        };
        total_force += force;
        history.push(force);
        if history.len() > SUMMARY_POINTS_PER_SERIES {
            history.remove(0);
        }
    }

    total_force
}

fn hand_segment_force(raw_values: &[u32], index: usize, config: Option<&HandForceConfig>) -> f32 {
    let offset: usize = HAND_FORCE_SEGMENT_COUNTS[..index.min(HAND_FORCE_PANEL_COUNT)]
        .iter()
        .sum();
    let sample_count = HAND_FORCE_SEGMENT_COUNTS
        .get(index)
        .copied()
        .unwrap_or_default();
    let raw_total = raw_values
        .get(offset..offset + sample_count)
        .unwrap_or(&[])
        .iter()
        .fold(0_u64, |sum, value| sum + *value as u64)
        .min(u32::MAX as u64) as u32;

    raw_to_hand_segment_g(index, raw_total, config).min(20.6)
}

fn raw_to_hand_segment_g(index: usize, raw: u32, config: Option<&HandForceConfig>) -> f32 {
    match index {
        0 => raw_to_g1(raw, config),
        1 => raw_to_g2(raw, config),
        2 => raw_to_g3(raw, config),
        3 => raw_to_g4(raw, config),
        4 => raw_to_g5(raw, config),
        5 => raw_to_g6(raw),
        6 => raw_to_g7(raw),
        _ => raw_to_gd(raw),
    }
}

fn to_force_table(values: &BTreeMap<String, u32>) -> Vec<(u32, f32)> {
    let mut table = values
        .iter()
        .filter_map(|(force_n, raw)| force_n.parse::<f32>().ok().map(|force| (*raw, force)))
        .collect::<Vec<_>>();
    table.sort_by(|a, b| a.1.total_cmp(&b.1));
    table.insert(0, (0, 0.0));
    table
}

fn interpolate_force(raw: u32, table: &[(u32, f32)]) -> f32 {
    let Some(&(first_raw, first_force)) = table.first() else {
        return 0.0;
    };
    let Some(&(last_raw, last_force)) = table.last() else {
        return 0.0;
    };

    if raw <= first_raw {
        return first_force;
    }
    if raw >= last_raw {
        return last_force;
    }

    let right = table.partition_point(|(raw_point, _)| *raw_point <= raw);
    let (raw_left, force_left) = table[right - 1];
    let (raw_right, force_right) = table[right];
    let ratio = (raw - raw_left) as f32 / (raw_right - raw_left).max(1) as f32;
    force_left + ratio * (force_right - force_left)
}

fn force_from_config_or_fallback(
    raw: u32,
    config_values: Option<&BTreeMap<String, u32>>,
    fallback_raw: &[u32],
    fallback_force_centi_n: &[f32],
) -> f32 {
    if let Some(config_values) = config_values {
        let table = to_force_table(config_values);
        if table.len() >= 2 && table.windows(2).all(|pair| pair[0].0 <= pair[1].0) {
            return interpolate_force(raw, &table);
        }
        log::warn!(
            "Hand force config raw values are not monotonically increasing; using fallback table"
        );
    }

    let fallback = fallback_raw
        .iter()
        .copied()
        .zip(fallback_force_centi_n.iter().map(|force| force / 100.0))
        .collect::<Vec<_>>();
    interpolate_force(raw, &fallback)
}

fn raw_to_gd(raw: u32) -> f32 {
    const RAW: [u32; 11] = [
        0, 486, 5310, 8130, 10005, 12883, 14700, 18141, 19877, 26270, 32648,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    if raw <= RAW[0] {
        return FORCE_CENTI_N[0] / 100.0;
    }

    if raw >= RAW[RAW.len() - 1] {
        return FORCE_CENTI_N[FORCE_CENTI_N.len() - 1] / 100.0;
    }

    let mut left = 0usize;
    let mut right = RAW.len() - 1;
    while left + 1 < right {
        let mid = (left + right) / 2;
        if RAW[mid] <= raw {
            left = mid;
        } else {
            right = mid;
        }
    }

    let raw_left = RAW[left] as f32;
    let raw_right = RAW[right] as f32;
    let span = (raw_right - raw_left).max(1.0);
    let ratio = (raw as f32 - raw_left) / span;

    (FORCE_CENTI_N[left] + ratio * (FORCE_CENTI_N[right] - FORCE_CENTI_N[left])) / 100.0
}

fn raw_to_g1(raw: u32, config: Option<&HandForceConfig>) -> f32 {
    const RAW: [u32; 11] = [
        0, 35694, 226640, 319868, 398021, 450448, 510506, 592075, 655760, 771671, 813967,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    force_from_config_or_fallback(raw, config.map(|cfg| &cfg.thumb), &RAW, &FORCE_CENTI_N)
}

fn raw_to_g2(raw: u32, config: Option<&HandForceConfig>) -> f32 {
    const RAW: [u32; 11] = [
        0, 45490, 267579, 352066, 431681, 457605, 550875, 624327, 732735, 883397, 899338,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    force_from_config_or_fallback(raw, config.map(|cfg| &cfg.index), &RAW, &FORCE_CENTI_N)
}

fn raw_to_g3(raw: u32, config: Option<&HandForceConfig>) -> f32 {
    const RAW: [u32; 11] = [
        0, 51562, 252840, 326531, 386571, 415819, 514679, 631745, 678945, 810847, 900422,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    force_from_config_or_fallback(raw, config.map(|cfg| &cfg.middle), &RAW, &FORCE_CENTI_N)
}

fn raw_to_g4(raw: u32, config: Option<&HandForceConfig>) -> f32 {
    const RAW: [u32; 11] = [
        0, 33213, 220611, 307383, 369637, 430462, 489659, 584789, 664960, 802749, 926907,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    force_from_config_or_fallback(raw, config.map(|cfg| &cfg.ring), &RAW, &FORCE_CENTI_N)
}

fn raw_to_g5(raw: u32, config: Option<&HandForceConfig>) -> f32 {
    const RAW: [u32; 11] = [
        0, 44291, 216657, 301950, 366186, 384791, 496521, 566189, 701459, 845608, 940796,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    force_from_config_or_fallback(raw, config.map(|cfg| &cfg.little), &RAW, &FORCE_CENTI_N)
}

fn raw_to_g6(raw: u32) -> f32 {
    const RAW: [u32; 11] = [
        0, 0, 2532, 5122, 7256, 11525, 13544, 16044, 18504, 29778, 41845,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    if raw <= RAW[0] {
        return FORCE_CENTI_N[0] / 100.0;
    }

    if raw >= RAW[RAW.len() - 1] {
        return FORCE_CENTI_N[FORCE_CENTI_N.len() - 1] / 100.0;
    }

    let mut left = 0usize;
    let mut right = RAW.len() - 1;
    while left + 1 < right {
        let mid = (left + right) / 2;
        if RAW[mid] <= raw {
            left = mid;
        } else {
            right = mid;
        }
    }

    let raw_left = RAW[left] as f32;
    let raw_right = RAW[right] as f32;
    let span = (raw_right - raw_left).max(1.0);
    let ratio = (raw as f32 - raw_left) / span;

    (FORCE_CENTI_N[left] + ratio * (FORCE_CENTI_N[right] - FORCE_CENTI_N[left])) / 100.0
}

fn raw_to_g7(raw: u32) -> f32 {
    const RAW: [u32; 11] = [
        0, 233, 9000, 12067, 14311, 16297, 18113, 27918, 32559, 50861, 65857,
    ];
    const FORCE_CENTI_N: [f32; 11] = [
        0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
    ];

    if raw <= RAW[0] {
        return FORCE_CENTI_N[0] / 100.0;
    }

    if raw >= RAW[RAW.len() - 1] {
        return FORCE_CENTI_N[FORCE_CENTI_N.len() - 1] / 100.0;
    }

    let mut left = 0usize;
    let mut right = RAW.len() - 1;
    while left + 1 < right {
        let mid = (left + right) / 2;
        if RAW[mid] <= raw {
            left = mid;
        } else {
            right = mid;
        }
    }

    let raw_left = RAW[left] as f32;
    let raw_right = RAW[right] as f32;
    let span = (raw_right - raw_left).max(1.0);
    let ratio = (raw as f32 - raw_left) / span;

    (FORCE_CENTI_N[left] + ratio * (FORCE_CENTI_N[right] - FORCE_CENTI_N[left])) / 100.0
}

fn paint_split_viewport_shell(
    ui: &egui::Ui,
    rect: egui::Rect,
    title: &'static str,
    subtitle: &'static str,
) {
    let painter = ui.painter();
    painter.rect_filled(
        rect,
        egui::CornerRadius::same(8),
        egui::Color32::from_rgb(11, 17, 23),
    );
    painter.rect_stroke(
        rect,
        egui::CornerRadius::same(8),
        egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
        egui::StrokeKind::Outside,
    );
    painter.line_segment(
        [
            rect.left_top() + egui::vec2(12.0, 34.0),
            rect.right_top() + egui::vec2(-12.0, 34.0),
        ],
        egui::Stroke::new(1.0, ONE_DARK_PRO.border_soft),
    );
    painter.text(
        rect.left_top() + egui::vec2(14.0, 16.0),
        egui::Align2::LEFT_CENTER,
        title,
        egui::FontId::monospace(13.0),
        ONE_DARK_PRO.accent,
    );
    painter.text(
        rect.left_top() + egui::vec2(122.0, 16.0),
        egui::Align2::LEFT_CENTER,
        subtitle,
        egui::FontId::proportional(12.0),
        ONE_DARK_PRO.text_dim,
    );
}

fn split_viewport_body(rect: egui::Rect) -> egui::Rect {
    egui::Rect::from_min_max(
        rect.left_top() + egui::vec2(10.0, 44.0),
        rect.right_bottom() - egui::vec2(10.0, 10.0),
    )
}

#[derive(Clone, Copy)]
struct PressureColorRange {
    min: f32,
    max: f32,
    gamma: f32,
}

// Controls the pressure-to-color response curve. Higher values reserve warmer
// colors for larger fractions of each panel's configured raw range.
const PRESSURE_COLOR_GAMMA: f32 = 1.5;

impl PressureColorRange {
    const fn new(min: u32, max: u32) -> Self {
        Self {
            min: min as f32,
            max: max as f32,
            gamma: PRESSURE_COLOR_GAMMA,
        }
    }

    fn normalize(self, value: u32) -> [f32; 2] {
        let raw = value as f32;
        let span = (self.max - self.min).max(1.0);
        let mapped = ((raw - self.min) / span).clamp(0.0, 1.0);

        let intensity = if raw <= self.min + 4.0 {
            0.0
        } else {
            mapped.powf(self.gamma)
        };

        let display_value = if raw <= self.min + 4.0 {
            0.0
        } else {
            raw.round().min(9999.0)
        };

        [intensity, display_value]
    }

    fn per_cell(self, cell_count: usize) -> Self {
        let cells = cell_count.max(1) as f32;
        Self {
            min: self.min / cells,
            max: self.max / cells,
            gamma: self.gamma,
        }
    }
}

const DEFAULT_COLOR_RANGE: PressureColorRange = PressureColorRange::new(0, 7000);

const HAND_SENSOR_PANEL_COLOR_RANGES: [PressureColorRange; HAND_FORCE_PANEL_COUNT] = [
    PressureColorRange::new(0, 813967),
    PressureColorRange::new(0, 899338),
    PressureColorRange::new(0, 900422),
    PressureColorRange::new(0, 926907),
    PressureColorRange::new(0, 940796),
    PressureColorRange::new(0, 41845),
    PressureColorRange::new(0, 65857),
];

const HAND_FINGER_SENSOR_CELLS: usize = 12 * 7;
const HAND_SENSOR_PANEL_CELL_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = [
    HAND_FINGER_SENSOR_CELLS,
    HAND_FINGER_SENSOR_CELLS,
    HAND_FINGER_SENSOR_CELLS,
    HAND_FINGER_SENSOR_CELLS,
    HAND_FINGER_SENSOR_CELLS,
    5 * 14,
    11 * 4,
];

fn normalize_pressure_sample(raw: &[u32], rows: u32, cols: u32, normalized: &mut PressureFrame) {
    normalized.fill([0.0, 0.0]);

    let src_cols = cols.max(1);
    let copy_rows = MATRIX_ROWS.min(rows);
    let copy_cols = MATRIX_COLS.min(cols);

    for row in 0..copy_rows {
        for col in 0..copy_cols {
            let src_index = (row * src_cols + col) as usize;
            let dst_index = (row * MATRIX_COLS + col) as usize;
            if let Some(value) = raw.get(src_index) {
                normalized[dst_index] = normalize_pressure_value(*value);
            }
        }
    }
}

fn normalize_pressure_samples(raw: &[u32]) -> PressureSamples {
    raw.iter()
        .copied()
        .enumerate()
        .map(|(index, value)| {
            hand_sensor_panel_for_index(index)
                .map(|(range, cell_count)| range.per_cell(cell_count).normalize(value))
                .unwrap_or_else(|| DEFAULT_COLOR_RANGE.normalize(value))
        })
        .collect()
}

fn hand_sensor_panel_for_index(index: usize) -> Option<(PressureColorRange, usize)> {
    let mut start = 0;

    for (range, cell_count) in HAND_SENSOR_PANEL_COLOR_RANGES
        .iter()
        .copied()
        .zip(HAND_SENSOR_PANEL_CELL_COUNTS)
    {
        let end = start + cell_count;
        if index < end {
            return Some((range, cell_count));
        }
        start = end;
    }

    None
}

fn normalize_pressure_value(value: u32) -> [f32; 2] {
    DEFAULT_COLOR_RANGE.normalize(value)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn raw_to_g1_uses_loaded_thumb_config() {
        let config: HandForceConfig = toml::from_str(
            r#"
                [thumb]
                "0.57" = 10
                "2.57" = 20

                [index]
                "0.57" = 1
                [middle]
                "0.57" = 1
                [ring]
                "0.57" = 1
                [little]
                "0.57" = 1
            "#,
        )
        .expect("test config should parse");

        assert_eq!(raw_to_g1(20, Some(&config)), 2.57);
        assert_ne!(raw_to_g1(20, None), 2.57);
    }

    #[test]
    fn pressure_normalization_makes_low_hand_values_visible() {
        let [zero_intensity, zero_label] = normalize_pressure_value(4);
        assert_eq!(zero_intensity, 0.0);
        assert_eq!(zero_label, 0.0);

        let [low_intensity, low_label] = normalize_pressure_value(100);
        assert!(
            low_intensity > 0.0,
            "low hand-gateway values should retain a non-zero response"
        );
        assert_eq!(low_label, 100.0);

        let [max_intensity, max_label] = normalize_pressure_value(7000);
        assert_eq!(max_intensity, 1.0);
        assert_eq!(max_label, 7000.0);
    }

    #[test]
    fn pressure_normalization_applies_configured_gamma() {
        let range = PressureColorRange::new(0, 10_000);
        let [intensity, _] = range.normalize(8_000);
        let expected = 0.8_f32.powf(PRESSURE_COLOR_GAMMA);

        assert!(
            (intensity - expected).abs() < 0.002,
            "the normalized response should follow the configured gamma"
        );
    }

    #[test]
    fn hand_panel_ranges_cover_both_palm_boards() {
        let total_cells: usize = HAND_SENSOR_PANEL_CELL_COUNTS.iter().sum();
        let mut raw = vec![0; total_cells];
        let mut start = 0;

        for (range, cell_count) in HAND_SENSOR_PANEL_COLOR_RANGES
            .iter()
            .copied()
            .zip(HAND_SENSOR_PANEL_CELL_COUNTS)
        {
            raw[start] = (range.max / cell_count as f32).ceil() as u32;
            start += cell_count;
        }

        let normalized = normalize_pressure_samples(&raw);
        let palm_horizontal_start = HAND_SENSOR_PANEL_CELL_COUNTS[..5].iter().sum::<usize>();
        let palm_vertical_start = palm_horizontal_start + HAND_SENSOR_PANEL_CELL_COUNTS[5];

        assert_eq!(normalized[palm_horizontal_start][0], 1.0);
        assert_eq!(normalized[palm_vertical_start][0], 1.0);
    }
}

impl eframe::App for EskinDesktopApp {
    fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
        let ctx = ui.ctx().clone();
        let stats = self.connection.stats();
        self.live_rates.tick_render(stats.rx_frames);

        self.update_pressure_matrix();
        self.sync_disconnected_live_state();
        self.draw_workspace(ui);
        self.draw_title_bar(ui, frame);
        self.draw_floating_panels(&ctx, stats);

        // Keep repainting while the wgpu background is a realtime viewport.
        ctx.request_repaint();
    }
}
