use futures::SinkExt;
use iced::widget::{
    button, canvas, column, container, pane_grid, pick_list, row, text, text_editor, text_input,
};
use iced::widget::button::Style as ButtonStyle;
use iced::{Border, Color, Element, Length, Subscription, Task, Theme};
use std::fmt::Write;
use std::time::Duration;
use tokio::sync::mpsc;

use crate::native_menu::{MenuAction, NativeMenu};
use crate::protocol::{
    self, AmpPoint, ClPoint, ClResult, Electrode, EisMessage, EisPoint, LpRtia, LsvPoint,
    PhResult, Rcal, Rtia,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tab {
    Eis,
    Lsv,
    Amp,
    Chlorine,
    Ph,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PaneId {
    Plot,
    Data,
}

#[derive(Debug, Clone)]
pub enum Message {
    BleReady(mpsc::UnboundedSender<Vec<u8>>),
    BleStatus(String),
    BleData(EisMessage),
    TabSelected(Tab),
    PaneResized(pane_grid::ResizeEvent),
    DataAction(text_editor::Action),
    /* EIS */
    FreqStartChanged(String),
    FreqStopChanged(String),
    PpdChanged(String),
    RtiaSelected(Rtia),
    RcalSelected(Rcal),
    ElectrodeSelected(Electrode),
    ApplySettings,
    StartSweep,
    /* LSV */
    LsvStartVChanged(String),
    LsvStopVChanged(String),
    LsvScanRateChanged(String),
    LsvRtiaSelected(LpRtia),
    StartLsv,
    /* Amperometry */
    AmpVholdChanged(String),
    AmpIntervalChanged(String),
    AmpDurationChanged(String),
    AmpRtiaSelected(LpRtia),
    StartAmp,
    StopAmp,
    /* Chlorine */
    ClCondVChanged(String),
    ClCondTChanged(String),
    ClFreeVChanged(String),
    ClTotalVChanged(String),
    ClDepTChanged(String),
    ClMeasTChanged(String),
    ClRtiaSelected(LpRtia),
    StartCl,
    /* pH */
    PhStabilizeChanged(String),
    StartPh,
    /* Global */
    PollTemp,
    NativeMenuTick,
    CloseSysInfo,
    /* Reference baseline */
    SetReference,
    ClearReference,
    CollectRefs,
    GetRefs,
    ClearRefs,
    /* Clean */
    CleanVChanged(String),
    CleanDurChanged(String),
    StartClean,
    /* Misc */
    OpenMidiSetup,
    RefreshMidi,
}

pub struct App {
    tab: Tab,
    status: String,
    cmd_tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
    ble_connected: bool,
    panes: pane_grid::State<PaneId>,
    native_menu: NativeMenu,
    show_sysinfo: bool,

    /* EIS */
    eis_points: Vec<EisPoint>,
    sweep_total: u16,
    freq_start: String,
    freq_stop: String,
    ppd: String,
    rtia: Rtia,
    rcal: Rcal,
    electrode: Electrode,
    eis_data: text_editor::Content,

    /* LSV */
    lsv_points: Vec<LsvPoint>,
    lsv_total: u16,
    lsv_start_v: String,
    lsv_stop_v: String,
    lsv_scan_rate: String,
    lsv_rtia: LpRtia,
    lsv_data: text_editor::Content,

    /* Amp */
    amp_points: Vec<AmpPoint>,
    amp_total: u16,
    amp_running: bool,
    amp_v_hold: String,
    amp_interval: String,
    amp_duration: String,
    amp_rtia: LpRtia,
    amp_data: text_editor::Content,

    /* Chlorine */
    cl_points: Vec<ClPoint>,
    cl_result: Option<ClResult>,
    cl_total: u16,
    cl_cond_v: String,
    cl_cond_t: String,
    cl_free_v: String,
    cl_total_v: String,
    cl_dep_t: String,
    cl_meas_t: String,
    cl_rtia: LpRtia,
    cl_data: text_editor::Content,

    /* pH */
    ph_result: Option<PhResult>,
    ph_stabilize: String,

    /* Reference baselines */
    eis_ref: Option<Vec<EisPoint>>,
    lsv_ref: Option<Vec<LsvPoint>>,
    amp_ref: Option<Vec<AmpPoint>>,
    cl_ref: Option<(Vec<ClPoint>, ClResult)>,
    ph_ref: Option<PhResult>,

    /* Device reference collection */
    collecting_refs: bool,
    ref_mode: Option<u8>,
    ref_rtia: Option<u8>,
    has_device_refs: bool,
    eis_refs: [Option<Vec<EisPoint>>; 8],
    lsv_lp_range: Option<(u8, u8)>,
    amp_lp_range: Option<(u8, u8)>,
    cl_lp_range: Option<(u8, u8)>,

    /* Clean */
    clean_v: String,
    clean_dur: String,

    /* Global */
    temp_c: f32,
    midi_gen: u64,
}

/* ---- data table formatting ---- */

fn fmt_eis(pts: &[EisPoint]) -> String {
    let mut s = String::with_capacity(pts.len() * 130 + 130);
    writeln!(s, "{:>10}  {:>12}  {:>10}  {:>12}  {:>12}  {:>10}  {:>10}  {:>12}  {:>10}  {:>7}",
        "Freq (Hz)", "|Z| (Ohm)", "Phase (°)", "Re (Ohm)", "Im (Ohm)",
        "RTIA bef", "RTIA aft", "|Z| rev", "Ph rev", "Err%").unwrap();
    for pt in pts {
        writeln!(s, "{:>10.1}  {:>12.2}  {:>10.2}  {:>12.2}  {:>12.2}  {:>10.1}  {:>10.1}  {:>12.2}  {:>10.2}  {:>6.2}%",
            pt.freq_hz, pt.mag_ohms, pt.phase_deg, pt.z_real, pt.z_imag,
            pt.rtia_mag_before, pt.rtia_mag_after, pt.rev_mag, pt.rev_phase, pt.pct_err).unwrap();
    }
    s
}

fn fmt_lsv(pts: &[LsvPoint]) -> String {
    let mut s = String::with_capacity(pts.len() * 28 + 28);
    writeln!(s, "{:>10}  {:>12}", "V (mV)", "I (uA)").unwrap();
    for pt in pts {
        writeln!(s, "{:>10.1}  {:>12.3}", pt.v_mv, pt.i_ua).unwrap();
    }
    s
}

fn fmt_amp(pts: &[AmpPoint]) -> String {
    let mut s = String::with_capacity(pts.len() * 28 + 28);
    writeln!(s, "{:>10}  {:>12}", "t (ms)", "I (uA)").unwrap();
    for pt in pts {
        writeln!(s, "{:>10.1}  {:>12.3}", pt.t_ms, pt.i_ua).unwrap();
    }
    s
}

fn fmt_cl(pts: &[ClPoint]) -> String {
    let mut s = String::with_capacity(pts.len() * 36 + 36);
    writeln!(s, "{:>10}  {:>12}  {:>8}", "t (ms)", "I (uA)", "Phase").unwrap();
    for pt in pts {
        let phase = match pt.phase { 1 => "Free", 2 => "Total", _ => "Cond" };
        writeln!(s, "{:>10.1}  {:>12.3}  {:>8}", pt.t_ms, pt.i_ua, phase).unwrap();
    }
    s
}

const SQUIRCLE: f32 = 8.0;

fn btn_style(bg: Color, fg: Color) -> impl Fn(&Theme, button::Status) -> ButtonStyle {
    move |_theme, status| {
        let (bg, fg) = match status {
            button::Status::Hovered => (
                Color { a: bg.a * 0.85, ..bg },
                fg,
            ),
            button::Status::Pressed => (
                Color { a: bg.a * 0.7, ..bg },
                fg,
            ),
            button::Status::Disabled => (
                Color { a: 0.3, ..bg },
                Color { a: 0.5, ..fg },
            ),
            _ => (bg, fg),
        };
        ButtonStyle {
            background: Some(iced::Background::Color(bg)),
            text_color: fg,
            border: Border {
                radius: SQUIRCLE.into(),
                ..Border::default()
            },
            ..ButtonStyle::default()
        }
    }
}

fn style_action() -> impl Fn(&Theme, button::Status) -> ButtonStyle {
    btn_style(Color::from_rgb(0.20, 0.65, 0.35), Color::WHITE)
}

fn style_danger() -> impl Fn(&Theme, button::Status) -> ButtonStyle {
    btn_style(Color::from_rgb(0.80, 0.25, 0.25), Color::WHITE)
}

fn style_apply() -> impl Fn(&Theme, button::Status) -> ButtonStyle {
    btn_style(Color::from_rgb(0.25, 0.50, 0.85), Color::WHITE)
}

fn style_tab(active: bool) -> impl Fn(&Theme, button::Status) -> ButtonStyle {
    let bg = if active {
        Color::from_rgb(0.35, 0.35, 0.40)
    } else {
        Color::from_rgb(0.22, 0.22, 0.25)
    };
    btn_style(bg, Color::WHITE)
}

fn style_neutral() -> impl Fn(&Theme, button::Status) -> ButtonStyle {
    btn_style(Color::from_rgb(0.30, 0.30, 0.35), Color::WHITE)
}

impl App {
    pub fn new() -> (Self, Task<Message>) {
        (Self {
            tab: Tab::Eis,
            status: "Starting...".into(),
            cmd_tx: None,
            ble_connected: false,
            panes: pane_grid::State::with_configuration(pane_grid::Configuration::Split {
                axis: pane_grid::Axis::Horizontal,
                ratio: 0.55,
                a: Box::new(pane_grid::Configuration::Pane(PaneId::Plot)),
                b: Box::new(pane_grid::Configuration::Pane(PaneId::Data)),
            }),
            native_menu: NativeMenu::init(),
            show_sysinfo: false,

            eis_points: Vec::new(),
            sweep_total: 0,
            freq_start: "1000".into(),
            freq_stop: "200000".into(),
            ppd: "10".into(),
            rtia: Rtia::R5K,
            rcal: Rcal::R3K,
            electrode: Electrode::FourWire,
            eis_data: text_editor::Content::with_text(&fmt_eis(&[])),

            lsv_points: Vec::new(),
            lsv_total: 0,
            lsv_start_v: "0".into(),
            lsv_stop_v: "500".into(),
            lsv_scan_rate: "50".into(),
            lsv_rtia: LpRtia::R10K,
            lsv_data: text_editor::Content::with_text(&fmt_lsv(&[])),

            amp_points: Vec::new(),
            amp_total: 0,
            amp_running: false,
            amp_v_hold: "200".into(),
            amp_interval: "100".into(),
            amp_duration: "60".into(),
            amp_rtia: LpRtia::R10K,
            amp_data: text_editor::Content::with_text(&fmt_amp(&[])),

            cl_points: Vec::new(),
            cl_result: None,
            cl_total: 0,
            cl_cond_v: "800".into(),
            cl_cond_t: "2000".into(),
            cl_free_v: "100".into(),
            cl_total_v: "-200".into(),
            cl_dep_t: "5000".into(),
            cl_meas_t: "5000".into(),
            cl_rtia: LpRtia::R10K,
            cl_data: text_editor::Content::with_text(&fmt_cl(&[])),

            ph_result: None,
            ph_stabilize: "30".into(),

            eis_ref: None,
            lsv_ref: None,
            amp_ref: None,
            cl_ref: None,
            ph_ref: None,

            collecting_refs: false,
            ref_mode: None,
            ref_rtia: None,
            has_device_refs: false,
            eis_refs: Default::default(),
            lsv_lp_range: None,
            amp_lp_range: None,
            cl_lp_range: None,

            clean_v: "1200".into(),
            clean_dur: "30".into(),

            temp_c: 25.0,
            midi_gen: 0,
        }, Task::none())
    }

    pub fn title(&self) -> String { "Cue".into() }
    pub fn theme(&self) -> Theme { Theme::Dark }

    fn send_cmd(&self, sysex: &[u8]) {
        if let Some(tx) = &self.cmd_tx {
            let _ = tx.send(sysex.to_vec());
        }
    }

    pub fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::BleReady(tx) => {
                self.cmd_tx = Some(tx);
                self.ble_connected = true;
                self.send_cmd(&protocol::build_sysex_get_config());
            }
            Message::BleStatus(s) => {
                if s.contains("Reconnecting") || s.contains("Looking") {
                    self.ble_connected = false;
                    self.cmd_tx = None;
                }
                self.status = s;
            }
            Message::BleData(msg) => match msg {
                EisMessage::SweepStart { num_points, freq_start, freq_stop } => {
                    if self.collecting_refs {
                        /* ref collection: clear temp buffer */
                        self.eis_points.clear();
                        self.sweep_total = num_points;
                    } else {
                        self.eis_points.clear();
                        self.sweep_total = num_points;
                        self.eis_data = text_editor::Content::with_text(&fmt_eis(&self.eis_points));
                        self.status = format!("Sweep: {} pts, {:.0}--{:.0} Hz", num_points, freq_start, freq_stop);
                    }
                }
                EisMessage::DataPoint { point, .. } => {
                    if self.collecting_refs {
                        self.eis_points.push(point);
                    } else {
                        self.eis_points.push(point);
                        self.eis_data = text_editor::Content::with_text(&fmt_eis(&self.eis_points));
                        self.status = format!("Receiving: {}/{}", self.eis_points.len(), self.sweep_total);
                    }
                }
                EisMessage::SweepEnd => {
                    if self.collecting_refs {
                        if let Some(r) = self.ref_rtia {
                            if (r as usize) < 8 {
                                self.eis_refs[r as usize] = Some(self.eis_points.clone());
                            }
                        }
                        self.eis_points.clear();
                    } else {
                        self.status = format!("Sweep complete: {} points", self.eis_points.len());
                    }
                }
                EisMessage::Config(cfg) => {
                    self.freq_start = format!("{:.0}", cfg.freq_start);
                    self.freq_stop = format!("{:.0}", cfg.freq_stop);
                    self.ppd = format!("{}", cfg.ppd);
                    self.rtia = cfg.rtia;
                    self.rcal = cfg.rcal;
                    self.electrode = cfg.electrode;
                    self.status = "Config received".into();
                }
                EisMessage::LsvStart { num_points, v_start, v_stop } => {
                    self.lsv_points.clear();
                    self.lsv_total = num_points;
                    self.lsv_data = text_editor::Content::with_text(&fmt_lsv(&self.lsv_points));
                    self.status = format!("LSV: {} pts, {:.0}--{:.0} mV", num_points, v_start, v_stop);
                }
                EisMessage::LsvPoint { point, .. } => {
                    self.lsv_points.push(point);
                    self.lsv_data = text_editor::Content::with_text(&fmt_lsv(&self.lsv_points));
                    self.status = format!("LSV: {}/{}", self.lsv_points.len(), self.lsv_total);
                }
                EisMessage::LsvEnd => {
                    self.status = format!("LSV complete: {} points", self.lsv_points.len());
                }
                EisMessage::AmpStart { v_hold } => {
                    self.amp_points.clear();
                    self.amp_running = true;
                    self.amp_data = text_editor::Content::with_text(&fmt_amp(&self.amp_points));
                    self.status = format!("Amp: {:.0} mV", v_hold);
                }
                EisMessage::AmpPoint { _index, point } => {
                    self.amp_points.push(point);
                    self.amp_total = _index + 1;
                    self.amp_data = text_editor::Content::with_text(&fmt_amp(&self.amp_points));
                    self.status = format!("Amp: {} pts", self.amp_points.len());
                }
                EisMessage::AmpEnd => {
                    self.amp_running = false;
                    self.status = format!("Amp complete: {} points", self.amp_points.len());
                }
                EisMessage::ClStart { num_points } => {
                    self.cl_points.clear();
                    self.cl_result = None;
                    self.cl_total = num_points;
                    self.cl_data = text_editor::Content::with_text(&fmt_cl(&self.cl_points));
                    self.status = format!("Chlorine: {} pts", num_points);
                }
                EisMessage::ClPoint { point, .. } => {
                    self.cl_points.push(point);
                    self.cl_data = text_editor::Content::with_text(&fmt_cl(&self.cl_points));
                    self.status = format!("Chlorine: {}/{}", self.cl_points.len(), self.cl_total);
                }
                EisMessage::ClResult(r) => {
                    self.cl_result = Some(r);
                    self.status = format!("Chlorine: free={:.3} uA, total={:.3} uA",
                        self.cl_result.as_ref().unwrap().i_free_ua,
                        self.cl_result.as_ref().unwrap().i_total_ua);
                }
                EisMessage::ClEnd => {
                    self.status = format!("Chlorine complete: {} points", self.cl_points.len());
                }
                EisMessage::PhResult(r) => {
                    if self.collecting_refs {
                        self.ph_ref = Some(r);
                    } else {
                        self.status = format!("pH: {:.2} (OCP={:.1} mV, T={:.1}C)",
                            r.ph, r.v_ocp_mv, r.temp_c);
                        self.ph_result = Some(r);
                    }
                }
                EisMessage::Temperature(t) => {
                    self.temp_c = t;
                }
                EisMessage::RefFrame { mode, rtia_idx } => {
                    self.ref_mode = Some(mode);
                    self.ref_rtia = Some(rtia_idx);
                    let mode_name = match mode {
                        0 => "EIS", 1 => "LSV", 2 => "Amp", 3 => "Cl", 4 => "pH", _ => "?"
                    };
                    if mode == 0 {
                        self.status = format!("Ref: {} RTIA {}/8", mode_name, rtia_idx + 1);
                    } else {
                        self.status = format!("Ref: {} range search", mode_name);
                    }
                }
                EisMessage::RefLpRange { mode, low_idx, high_idx } => {
                    match mode {
                        1 => self.lsv_lp_range = Some((low_idx, high_idx)),
                        2 => self.amp_lp_range = Some((low_idx, high_idx)),
                        3 => self.cl_lp_range = Some((low_idx, high_idx)),
                        _ => {}
                    }
                }
                EisMessage::RefsDone => {
                    self.collecting_refs = false;
                    self.has_device_refs = true;
                    self.ref_mode = None;
                    self.ref_rtia = None;
                    /* populate eis_ref from current RTIA's ref if available */
                    let rtia_idx = self.rtia.as_byte() as usize;
                    if rtia_idx < 8 {
                        if let Some(pts) = &self.eis_refs[rtia_idx] {
                            self.eis_ref = Some(pts.clone());
                        }
                    }
                    self.status = "Reference collection complete".into();
                }
                EisMessage::RefStatus { has_refs } => {
                    self.has_device_refs = has_refs;
                    if !has_refs {
                        self.status = "No device refs".into();
                    }
                }
            },
            Message::TabSelected(t) => self.tab = t,
            Message::PaneResized(event) => {
                self.panes.resize(event.split, event.ratio);
            }
            Message::DataAction(action) => {
                if !matches!(action, text_editor::Action::Edit(_)) {
                    match self.tab {
                        Tab::Eis => self.eis_data.perform(action),
                        Tab::Lsv => self.lsv_data.perform(action),
                        Tab::Amp => self.amp_data.perform(action),
                        Tab::Chlorine => self.cl_data.perform(action),
                        Tab::Ph => {}
                    }
                }
            }
            /* EIS */
            Message::FreqStartChanged(s) => self.freq_start = s,
            Message::FreqStopChanged(s) => self.freq_stop = s,
            Message::PpdChanged(s) => self.ppd = s,
            Message::RtiaSelected(r) => {
                self.rtia = r;
                let idx = r.as_byte() as usize;
                if idx < 8 {
                    if let Some(pts) = &self.eis_refs[idx] {
                        self.eis_ref = Some(pts.clone());
                    }
                }
            }
            Message::RcalSelected(r) => self.rcal = r,
            Message::ElectrodeSelected(e) => self.electrode = e,
            Message::ApplySettings => {
                let fs = self.freq_start.parse::<f32>().unwrap_or(1000.0);
                let fe = self.freq_stop.parse::<f32>().unwrap_or(200000.0);
                let ppd = self.ppd.parse::<u16>().unwrap_or(10);
                self.send_cmd(&protocol::build_sysex_set_sweep(fs, fe, ppd));
                self.send_cmd(&protocol::build_sysex_set_rtia(self.rtia));
                self.send_cmd(&protocol::build_sysex_set_rcal(self.rcal));
                self.send_cmd(&protocol::build_sysex_set_electrode(self.electrode));
                self.send_cmd(&protocol::build_sysex_get_config());
            }
            Message::StartSweep => {
                self.send_cmd(&protocol::build_sysex_get_temp());
                self.send_cmd(&protocol::build_sysex_start_sweep());
            }
            /* LSV */
            Message::LsvStartVChanged(s) => self.lsv_start_v = s,
            Message::LsvStopVChanged(s) => self.lsv_stop_v = s,
            Message::LsvScanRateChanged(s) => self.lsv_scan_rate = s,
            Message::LsvRtiaSelected(r) => self.lsv_rtia = r,
            Message::StartLsv => {
                let vs = self.lsv_start_v.parse::<f32>().unwrap_or(0.0);
                let ve = self.lsv_stop_v.parse::<f32>().unwrap_or(500.0);
                let sr = self.lsv_scan_rate.parse::<f32>().unwrap_or(50.0);
                self.send_cmd(&protocol::build_sysex_get_temp());
                self.send_cmd(&protocol::build_sysex_start_lsv(vs, ve, sr, self.lsv_rtia));
            }
            /* Amp */
            Message::AmpVholdChanged(s) => self.amp_v_hold = s,
            Message::AmpIntervalChanged(s) => self.amp_interval = s,
            Message::AmpDurationChanged(s) => self.amp_duration = s,
            Message::AmpRtiaSelected(r) => self.amp_rtia = r,
            Message::StartAmp => {
                let vh = self.amp_v_hold.parse::<f32>().unwrap_or(200.0);
                let iv = self.amp_interval.parse::<f32>().unwrap_or(100.0);
                let dur = self.amp_duration.parse::<f32>().unwrap_or(60.0);
                self.send_cmd(&protocol::build_sysex_get_temp());
                self.send_cmd(&protocol::build_sysex_start_amp(vh, iv, dur, self.amp_rtia));
            }
            Message::StopAmp => {
                self.send_cmd(&protocol::build_sysex_stop_amp());
            }
            /* Chlorine */
            Message::ClCondVChanged(s) => self.cl_cond_v = s,
            Message::ClCondTChanged(s) => self.cl_cond_t = s,
            Message::ClFreeVChanged(s) => self.cl_free_v = s,
            Message::ClTotalVChanged(s) => self.cl_total_v = s,
            Message::ClDepTChanged(s) => self.cl_dep_t = s,
            Message::ClMeasTChanged(s) => self.cl_meas_t = s,
            Message::ClRtiaSelected(r) => self.cl_rtia = r,
            Message::StartCl => {
                let v_cond = self.cl_cond_v.parse::<f32>().unwrap_or(800.0);
                let t_cond = self.cl_cond_t.parse::<f32>().unwrap_or(2000.0);
                let v_free = self.cl_free_v.parse::<f32>().unwrap_or(100.0);
                let v_total = self.cl_total_v.parse::<f32>().unwrap_or(-200.0);
                let t_dep = self.cl_dep_t.parse::<f32>().unwrap_or(5000.0);
                let t_meas = self.cl_meas_t.parse::<f32>().unwrap_or(5000.0);
                self.send_cmd(&protocol::build_sysex_get_temp());
                self.send_cmd(&protocol::build_sysex_start_cl(
                    v_cond, t_cond, v_free, v_total, t_dep, t_meas, self.cl_rtia,
                ));
            }
            /* pH */
            Message::PhStabilizeChanged(s) => self.ph_stabilize = s,
            Message::StartPh => {
                let stab = self.ph_stabilize.parse::<f32>().unwrap_or(30.0);
                self.send_cmd(&protocol::build_sysex_get_temp());
                self.send_cmd(&protocol::build_sysex_start_ph(stab));
            }
            /* Reference baseline */
            Message::SetReference => {
                match self.tab {
                    Tab::Eis if !self.eis_points.is_empty() => {
                        self.eis_ref = Some(self.eis_points.clone());
                        self.status = format!("EIS reference set ({} pts)", self.eis_points.len());
                    }
                    Tab::Lsv if !self.lsv_points.is_empty() => {
                        self.lsv_ref = Some(self.lsv_points.clone());
                        self.status = format!("LSV reference set ({} pts)", self.lsv_points.len());
                    }
                    Tab::Amp if !self.amp_points.is_empty() => {
                        self.amp_ref = Some(self.amp_points.clone());
                        self.status = format!("Amp reference set ({} pts)", self.amp_points.len());
                    }
                    Tab::Chlorine if !self.cl_points.is_empty() => {
                        if let Some(r) = &self.cl_result {
                            self.cl_ref = Some((self.cl_points.clone(), r.clone()));
                            self.status = "Chlorine reference set".into();
                        }
                    }
                    Tab::Ph => {
                        if let Some(r) = &self.ph_result {
                            self.ph_ref = Some(r.clone());
                            self.status = format!("pH reference set ({:.2})", r.ph);
                        }
                    }
                    _ => {}
                }
            }
            Message::CollectRefs => {
                self.collecting_refs = true;
                self.eis_refs = Default::default();
                self.lsv_lp_range = None;
                self.amp_lp_range = None;
                self.cl_lp_range = None;
                self.status = "Starting reference collection...".into();
                self.send_cmd(&protocol::build_sysex_start_refs());
            }
            Message::GetRefs => {
                self.collecting_refs = true;
                self.eis_refs = Default::default();
                self.lsv_lp_range = None;
                self.amp_lp_range = None;
                self.cl_lp_range = None;
                self.send_cmd(&protocol::build_sysex_get_refs());
            }
            Message::ClearRefs => {
                self.collecting_refs = false;
                self.has_device_refs = false;
                self.eis_refs = Default::default();
                self.eis_ref = None;
                self.lsv_lp_range = None;
                self.amp_lp_range = None;
                self.cl_lp_range = None;
                self.ph_ref = None;
                self.send_cmd(&protocol::build_sysex_clear_refs());
                self.status = "Refs cleared".into();
            }
            Message::ClearReference => {
                match self.tab {
                    Tab::Eis => { self.eis_ref = None; self.status = "EIS reference cleared".into(); }
                    Tab::Lsv => { self.lsv_ref = None; self.status = "LSV reference cleared".into(); }
                    Tab::Amp => { self.amp_ref = None; self.status = "Amp reference cleared".into(); }
                    Tab::Chlorine => { self.cl_ref = None; self.status = "Chlorine reference cleared".into(); }
                    Tab::Ph => { self.ph_ref = None; self.status = "pH reference cleared".into(); }
                }
            }
            /* Global */
            Message::PollTemp => {
                self.send_cmd(&protocol::build_sysex_get_temp());
            }
            Message::NativeMenuTick => {
                for action in self.native_menu.poll_events() {
                    match action {
                        MenuAction::SystemInfo => self.show_sysinfo = !self.show_sysinfo,
                    }
                }
            }
            Message::CloseSysInfo => {
                self.show_sysinfo = false;
            }
            /* Clean */
            Message::CleanVChanged(s) => self.clean_v = s,
            Message::CleanDurChanged(s) => self.clean_dur = s,
            Message::StartClean => {
                let v = self.clean_v.parse::<f32>().unwrap_or(1200.0);
                let d = self.clean_dur.parse::<f32>().unwrap_or(30.0);
                self.send_cmd(&protocol::build_sysex_start_clean(v, d));
                self.status = format!("Cleaning: {:.0} mV for {:.0}s", v, d);
            }
            Message::OpenMidiSetup => {
                let _ = std::process::Command::new("open")
                    .arg("-a")
                    .arg("Audio MIDI Setup")
                    .spawn();
            }
            Message::RefreshMidi => {
                self.midi_gen += 1;
                self.cmd_tx = None;
                self.ble_connected = false;
                self.status = "Looking for MIDI device...".into();
            }
        }
        Task::none()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        let ble = Subscription::run_with_id(
            self.midi_gen,
            iced::stream::channel(100, |mut output| async move {
                loop {
                    let _ = output.send(Message::BleStatus("Looking for MIDI device...".into())).await;
                    match crate::ble::connect_and_stream(&mut output).await {
                        Ok(()) => eprintln!("BLE: session ended cleanly"),
                        Err(e) => eprintln!("BLE: session error: {e}"),
                    }
                    let _ = output.send(Message::BleStatus("Reconnecting...".into())).await;
                    tokio::time::sleep(Duration::from_millis(500)).await;
                }
            }),
        );

        let temp_poll = iced::time::every(Duration::from_millis(500))
            .map(|_| Message::PollTemp);
        let menu_tick = iced::time::every(Duration::from_millis(50))
            .map(|_| Message::NativeMenuTick);

        Subscription::batch([ble, temp_poll, menu_tick])
    }

    pub fn view(&self) -> Element<'_, Message> {
        let tab_btn = |label: &'static str, t: Tab, active: bool| -> Element<'_, Message> {
            let b = button(text(label).size(13))
                .style(style_tab(active))
                .padding([6, 14]);
            if active { b.into() } else { b.on_press(Message::TabSelected(t)).into() }
        };

        let tabs = row![
            tab_btn("EIS", Tab::Eis, self.tab == Tab::Eis),
            tab_btn("LSV", Tab::Lsv, self.tab == Tab::Lsv),
            tab_btn("Amperometry", Tab::Amp, self.tab == Tab::Amp),
            tab_btn("Chlorine", Tab::Chlorine, self.tab == Tab::Chlorine),
            tab_btn("pH", Tab::Ph, self.tab == Tab::Ph),
            button(text("MIDI Setup").size(13))
                .style(style_neutral())
                .padding([6, 14])
                .on_press(Message::OpenMidiSetup),
            iced::widget::horizontal_space(),
            text("Clean").size(12),
            text_input("mV", &self.clean_v).on_input(Message::CleanVChanged).width(60),
            text_input("s", &self.clean_dur).on_input(Message::CleanDurChanged).width(45),
            button(text("Clean").size(13))
                .style(btn_style(Color::from_rgb(0.65, 0.55, 0.15), Color::WHITE))
                .padding([6, 14])
                .on_press(Message::StartClean),
        ]
        .spacing(4)
        .align_y(iced::Alignment::Center);

        let has_ref = match self.tab {
            Tab::Eis => self.eis_ref.is_some(),
            Tab::Lsv => self.lsv_ref.is_some(),
            Tab::Amp => self.amp_ref.is_some(),
            Tab::Chlorine => self.cl_ref.is_some(),
            Tab::Ph => self.ph_ref.is_some(),
        };
        let has_data = match self.tab {
            Tab::Eis => !self.eis_points.is_empty(),
            Tab::Lsv => !self.lsv_points.is_empty(),
            Tab::Amp => !self.amp_points.is_empty(),
            Tab::Chlorine => self.cl_result.is_some(),
            Tab::Ph => self.ph_result.is_some(),
        };

        let mut ref_row = row![].spacing(4).align_y(iced::Alignment::Center);

        if !self.collecting_refs {
            ref_row = ref_row.push(
                button(text("Collect Refs").size(11))
                    .style(style_action())
                    .padding([4, 10])
                    .on_press(Message::CollectRefs),
            );
        } else {
            ref_row = ref_row.push(
                button(text("Collecting...").size(11))
                    .style(style_neutral())
                    .padding([4, 10]),
            );
        }
        if self.has_device_refs {
            ref_row = ref_row.push(
                button(text("Get Refs").size(11))
                    .style(style_neutral())
                    .padding([4, 10])
                    .on_press(Message::GetRefs),
            );
        }
        if self.has_device_refs || has_ref {
            ref_row = ref_row.push(
                button(text("Clear Refs").size(11))
                    .style(style_danger())
                    .padding([4, 10])
                    .on_press(Message::ClearRefs),
            );
        }
        if has_data {
            ref_row = ref_row.push(
                button(text("Set Ref").size(11))
                    .style(style_neutral())
                    .padding([4, 10])
                    .on_press(Message::SetReference),
            );
        }
        if has_ref {
            ref_row = ref_row.push(
                button(text("Clear Ref").size(11))
                    .style(style_danger())
                    .padding([4, 10])
                    .on_press(Message::ClearReference),
            );
            ref_row = ref_row.push(text("REF").size(11));
        }

        let connected = self.ble_connected;
        let mut status_row = row![text(&self.status).size(16)].spacing(6)
            .align_y(iced::Alignment::Center);
        if !connected {
            status_row = status_row.push(
                button(text("Refresh MIDI").size(11))
                    .style(style_apply())
                    .padding([4, 10])
                    .on_press(Message::RefreshMidi),
            );
        }
        status_row = status_row
            .push(iced::widget::horizontal_space())
            .push(ref_row)
            .push(text(format!("{:.1} C", self.temp_c)).size(14));

        let controls = self.view_controls();

        let body: Element<'_, Message> = if self.show_sysinfo {
            self.view_sysinfo()
        } else if self.tab == Tab::Ph {
            self.view_ph_body()
        } else {
            pane_grid::PaneGrid::new(&self.panes, |_pane, &pane_id, _maximized| {
                let el = match pane_id {
                    PaneId::Plot => self.view_plot_pane(),
                    PaneId::Data => self.view_data_pane(),
                };
                pane_grid::Content::new(el)
            })
            .on_resize(6, Message::PaneResized)
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
        };

        container(
            column![tabs, status_row, controls, body]
                .spacing(10)
                .padding(20)
                .width(Length::Fill)
                .height(Length::Fill),
        )
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }

    fn view_controls(&self) -> Element<'_, Message> {
        match self.tab {
            Tab::Eis => row![
                column![
                    text("Start Hz").size(12),
                    text_input("1000", &self.freq_start).on_input(Message::FreqStartChanged).width(90),
                ].spacing(2),
                column![
                    text("Stop Hz").size(12),
                    text_input("200000", &self.freq_stop).on_input(Message::FreqStopChanged).width(90),
                ].spacing(2),
                column![
                    text("PPD").size(12),
                    text_input("10", &self.ppd).on_input(Message::PpdChanged).width(50),
                ].spacing(2),
                column![
                    text("RTIA").size(12),
                    pick_list(Rtia::ALL, Some(self.rtia), Message::RtiaSelected).width(110),
                ].spacing(2),
                column![
                    text("RCAL").size(12),
                    pick_list(Rcal::ALL, Some(self.rcal), Message::RcalSelected).width(160),
                ].spacing(2),
                column![
                    text("Electrodes").size(12),
                    pick_list(Electrode::ALL, Some(self.electrode), Message::ElectrodeSelected).width(170),
                ].spacing(2),
                button(text("Apply").size(13))
                    .style(style_apply())
                    .padding([6, 16])
                    .on_press(Message::ApplySettings),
                button(text("Sweep").size(13))
                    .style(style_action())
                    .padding([6, 20])
                    .on_press(Message::StartSweep),
            ]
            .spacing(8)
            .align_y(iced::Alignment::End)
            .into(),

            Tab::Lsv => row![
                column![
                    text("Start mV").size(12),
                    text_input("0", &self.lsv_start_v).on_input(Message::LsvStartVChanged).width(80),
                ].spacing(2),
                column![
                    text("Stop mV").size(12),
                    text_input("500", &self.lsv_stop_v).on_input(Message::LsvStopVChanged).width(80),
                ].spacing(2),
                column![
                    text("Scan mV/s").size(12),
                    text_input("50", &self.lsv_scan_rate).on_input(Message::LsvScanRateChanged).width(80),
                ].spacing(2),
                column![
                    text("RTIA").size(12),
                    pick_list(LpRtia::ALL, Some(self.lsv_rtia), Message::LsvRtiaSelected).width(Length::Shrink),
                ].spacing(2),
                button(text("Start LSV").size(13))
                    .style(style_action())
                    .padding([6, 16])
                    .on_press(Message::StartLsv),
            ]
            .spacing(10)
            .align_y(iced::Alignment::End)
            .into(),

            Tab::Amp => row![
                column![
                    text("V hold mV").size(12),
                    text_input("200", &self.amp_v_hold).on_input(Message::AmpVholdChanged).width(80),
                ].spacing(2),
                column![
                    text("Interval ms").size(12),
                    text_input("100", &self.amp_interval).on_input(Message::AmpIntervalChanged).width(80),
                ].spacing(2),
                column![
                    text("Duration s").size(12),
                    text_input("60", &self.amp_duration).on_input(Message::AmpDurationChanged).width(80),
                ].spacing(2),
                column![
                    text("RTIA").size(12),
                    pick_list(LpRtia::ALL, Some(self.amp_rtia), Message::AmpRtiaSelected).width(Length::Shrink),
                ].spacing(2),
                if self.amp_running {
                    button(text("Stop").size(13))
                        .style(style_danger())
                        .padding([6, 16])
                        .on_press(Message::StopAmp)
                } else {
                    button(text("Start Amp").size(13))
                        .style(style_action())
                        .padding([6, 16])
                        .on_press(Message::StartAmp)
                },
            ]
            .spacing(10)
            .align_y(iced::Alignment::End)
            .into(),

            Tab::Chlorine => row![
                column![
                    text("Cond mV").size(12),
                    text_input("800", &self.cl_cond_v).on_input(Message::ClCondVChanged).width(70),
                ].spacing(2),
                column![
                    text("Cond ms").size(12),
                    text_input("2000", &self.cl_cond_t).on_input(Message::ClCondTChanged).width(70),
                ].spacing(2),
                column![
                    text("Free mV").size(12),
                    text_input("100", &self.cl_free_v).on_input(Message::ClFreeVChanged).width(70),
                ].spacing(2),
                column![
                    text("Total mV").size(12),
                    text_input("-200", &self.cl_total_v).on_input(Message::ClTotalVChanged).width(70),
                ].spacing(2),
                column![
                    text("Settle ms").size(12),
                    text_input("5000", &self.cl_dep_t).on_input(Message::ClDepTChanged).width(70),
                ].spacing(2),
                column![
                    text("Meas ms").size(12),
                    text_input("5000", &self.cl_meas_t).on_input(Message::ClMeasTChanged).width(70),
                ].spacing(2),
                column![
                    text("RTIA").size(12),
                    pick_list(LpRtia::ALL, Some(self.cl_rtia), Message::ClRtiaSelected).width(Length::Shrink),
                ].spacing(2),
                button(text("Measure").size(13))
                    .style(style_action())
                    .padding([6, 16])
                    .on_press(Message::StartCl),
            ]
            .spacing(8)
            .align_y(iced::Alignment::End)
            .into(),

            Tab::Ph => row![
                column![
                    text("Stabilize s").size(12),
                    text_input("30", &self.ph_stabilize).on_input(Message::PhStabilizeChanged).width(80),
                ].spacing(2),
                button(text("Measure pH").size(13))
                    .style(style_action())
                    .padding([6, 16])
                    .on_press(Message::StartPh),
            ]
            .spacing(10)
            .align_y(iced::Alignment::End)
            .into(),
        }
    }

    fn view_plot_pane(&self) -> Element<'_, Message> {
        match self.tab {
            Tab::Eis => {
                let bode = canvas(crate::plot::BodePlot {
                    points: &self.eis_points,
                    reference: self.eis_ref.as_deref(),
                })
                    .width(Length::FillPortion(3))
                    .height(Length::Fill);
                let nyquist = canvas(crate::plot::NyquistPlot {
                    points: &self.eis_points,
                    reference: self.eis_ref.as_deref(),
                })
                    .width(Length::FillPortion(2))
                    .height(Length::Fill);
                row![bode, nyquist].spacing(10).height(Length::Fill).into()
            }
            Tab::Lsv => canvas(crate::plot::VoltammogramPlot {
                points: &self.lsv_points,
                reference: self.lsv_ref.as_deref(),
            })
                .width(Length::Fill).height(Length::Fill).into(),
            Tab::Amp => canvas(crate::plot::AmperogramPlot {
                points: &self.amp_points,
                reference: self.amp_ref.as_deref(),
            })
                .width(Length::Fill).height(Length::Fill).into(),
            Tab::Chlorine => {
                let ref_pts = self.cl_ref.as_ref().map(|(pts, _)| pts.as_slice());
                let plot = canvas(crate::plot::ChlorinePlot {
                    points: &self.cl_points,
                    reference: ref_pts,
                })
                    .width(Length::Fill).height(Length::Fill);
                let mut result_parts: Vec<String> = Vec::new();
                if let Some(r) = &self.cl_result {
                    result_parts.push(format!(
                        "Free: {:.3} uA | Total: {:.3} uA | Combined: {:.3} uA",
                        r.i_free_ua, r.i_total_ua, r.i_total_ua - r.i_free_ua
                    ));
                    if let Some((_, ref_r)) = &self.cl_ref {
                        let df = r.i_free_ua - ref_r.i_free_ua;
                        let dt = r.i_total_ua - ref_r.i_total_ua;
                        result_parts.push(format!(
                            "vs Ref: dFree={:.3} uA, dTotal={:.3} uA",
                            df, dt
                        ));
                    }
                }
                if result_parts.is_empty() {
                    plot.into()
                } else {
                    let result_text = text(result_parts.join("  ")).size(14);
                    column![result_text, plot].spacing(4).height(Length::Fill).into()
                }
            }
            Tab::Ph => text("").into(),
        }
    }

    fn view_data_pane(&self) -> Element<'_, Message> {
        let content = match self.tab {
            Tab::Eis => &self.eis_data,
            Tab::Lsv => &self.lsv_data,
            Tab::Amp => &self.amp_data,
            Tab::Chlorine => &self.cl_data,
            Tab::Ph => return text("").into(),
        };
        text_editor(content)
            .on_action(Message::DataAction)
            .font(iced::Font::MONOSPACE)
            .size(13)
            .height(Length::Fill)
            .padding(4)
            .into()
    }

    fn view_ph_body(&self) -> Element<'_, Message> {
        if let Some(r) = &self.ph_result {
            let nernst_slope = 0.1984 * (r.temp_c as f64 + 273.15);
            let mut col = column![
                text(format!("pH: {:.2}", r.ph)).size(28),
                text(format!("OCP: {:.1} mV  |  Nernst slope: {:.2} mV/pH  |  Temp: {:.1} C",
                    r.v_ocp_mv, nernst_slope, r.temp_c)).size(14),
            ].spacing(4);
            if let Some(ref_r) = &self.ph_ref {
                let d_ph = r.ph - ref_r.ph;
                let d_v = r.v_ocp_mv - ref_r.v_ocp_mv;
                col = col.push(text(format!(
                    "vs Ref: dpH={:+.3}  dOCP={:+.1} mV  (ref pH={:.2})",
                    d_ph, d_v, ref_r.ph
                )).size(14));
            }
            col.into()
        } else {
            column![
                text("No measurement yet").size(16),
                text("OCP method: V(SE0) - V(RE0) with Nernst correction").size(12),
            ].spacing(4).into()
        }
    }

    fn view_sysinfo(&self) -> Element<'_, Message> {
        container(
            column![
                text("System Info").size(20),
                iced::widget::horizontal_rule(1),
                text(format!("Temperature: {:.1} C", self.temp_c)).size(14),
                text("Syslog: not yet implemented").size(14),
                text("0 / 4,194,304 bytes used").size(14),
                iced::widget::vertical_space().height(20),
                button(text("Close").size(14)).on_press(Message::CloseSysInfo),
            ]
            .spacing(8)
            .width(350)
            .padding(20),
        )
        .center(Length::Fill)
        .into()
    }
}
