use crate::app::WriteType;
use crate::compat::Instant;
use crate::config::Column;
use crate::custom::{CustomOp, CustomRepr, EnumEntry};
use crate::modbus::{DataBits, DeviceIdAccess, Parity, StopBits, WordOrder};
use crate::num_ops::{cycle, wrap_index};
use crate::register::{RegisterCell, RegisterType};
use serde::{Deserialize, Serialize};
use std::time::Duration;

macro_rules! field_enum {
    ( $(#[$meta:meta])* $vis:vis enum $name:ident { $( $(#[$vmeta:meta])* $variant:ident ),+ $(,)? } ) => {
        $(#[$meta])*
        $vis enum $name { $( $(#[$vmeta])* $variant ),+ }
        impl $name {
            pub const ALL: [$name; field_enum!(@count $($variant)+)] = [$($name::$variant),+];
        }
    };
    (@count) => (0usize);
    (@count $head:ident $($tail:ident)*) => (1usize + field_enum!(@count $($tail)*));
}

pub trait PopupPayload: Sized {
    fn from_popup(popup: &Popup) -> Option<&Self>;
    fn from_popup_mut(popup: &mut Popup) -> Option<&mut Self>;
}

macro_rules! popups {
    (@accessor $variant:ident ( $payload:ty )) => {
        impl PopupPayload for $payload {
            fn from_popup(popup: &Popup) -> Option<&Self> {
                match popup {
                    Popup::$variant(inner) => Some(inner),
                    _ => None,
                }
            }
            fn from_popup_mut(popup: &mut Popup) -> Option<&mut Self> {
                match popup {
                    Popup::$variant(inner) => Some(inner),
                    _ => None,
                }
            }
        }
    };
    (@accessor $variant:ident) => {};

    ( $( $variant:ident $( ( $payload:ty ) )? ),+ $(,)? ) => {
        #[derive(Debug, PartialEq)]
        pub enum Popup {
            $( $variant $( ( $payload ) )? ),+
        }

        #[derive(Clone, Copy, PartialEq, Eq)]
        pub enum PopupKind {
            $( $variant ),+
        }

        impl Popup {
            pub fn kind(&self) -> PopupKind {
                match self {
                    $( Popup::$variant { .. } => PopupKind::$variant ),+
                }
            }
        }

        $( popups!(@accessor $variant $( ( $payload ) )? ); )+
    };
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterfaceKind {
    Mock,
    Wired,
    Network,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveryField {
    Interface,
    Port,
    Baud,
    DataBits,
    Parity,
    StopBits,
    Ip,
    NetPort,
    SlaveId,
    ConnectTimeout,
    CommandTimeout,
    BetweenCommands,
    WordOrder,
    ScanNetwork,
    Connect,
}

#[derive(Debug, PartialEq)]
pub struct DiscoveryParams {
    pub interface: InterfaceKind,
    pub selected: u16,
    pub ports: Vec<String>,
    pub port_index: u16,
    pub baud_rate: u32,
    pub data_bits: DataBits,
    pub parity: Parity,
    pub stop_bits: StopBits,
    pub ip: String,
    pub net_port: u16,
    pub slave_id: u8,
    pub connect_timeout_ms: u64,
    pub command_timeout_ms: u64,
    pub between_commands_ms: u64,
    pub word_order: WordOrder,
    pub found: Vec<String>,
    pub scan_open: bool,
    pub scan_selected: u16,
    pub status: Option<StatusMessage>,
}

#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn local_subnet_prefix() -> Option<String> {
    match local_ip_address::local_ip().ok()? {
        std::net::IpAddr::V4(ip) if !ip.is_loopback() => {
            let [a, b, c, _] = ip.octets();
            Some(format!("{a}.{b}.{c}."))
        }
        _ => None,
    }
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn local_subnet_prefix() -> Option<String> {
    None
}

impl Default for DiscoveryParams {
    fn default() -> Self {
        Self {
            interface: InterfaceKind::Mock,
            selected: 0,
            ports: Vec::new(),
            port_index: 0,
            baud_rate: 9600,
            data_bits: DataBits::Eight,
            parity: Parity::None,
            stop_bits: StopBits::One,
            ip: local_subnet_prefix().unwrap_or_else(|| "127.0.0.1".to_string()),
            net_port: 502,
            slave_id: 1,
            connect_timeout_ms: 1000,
            command_timeout_ms: 2000,
            between_commands_ms: 3,
            word_order: WordOrder::default(),
            found: Vec::new(),
            scan_open: false,
            scan_selected: 0,
            status: None,
        }
    }
}

impl DiscoveryParams {
    pub fn fields(&self) -> Vec<DiscoveryField> {
        use DiscoveryField::*;
        let mut fields = vec![Interface];
        match self.interface {
            InterfaceKind::Mock => {}
            InterfaceKind::Wired => fields.extend([Port, Baud, DataBits, Parity, StopBits]),
            InterfaceKind::Network => fields.extend([Ip, NetPort, ScanNetwork]),
        }
        fields.extend([
            SlaveId,
            ConnectTimeout,
            CommandTimeout,
            BetweenCommands,
            WordOrder,
            Connect,
        ]);
        fields
    }

    pub fn current_field(&self) -> DiscoveryField {
        let fields = self.fields();
        let i = (self.selected as usize).min(fields.len() - 1);
        fields[i]
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct WriteParams {
    pub position: u16,
    pub result: Option<StatusMessage>,
    pub value: Option<i64>,
    pub write_type: WriteType,
    pub bit_cursor: u16,
    pub force_multiple: bool,
}

#[derive(Debug, Default, PartialEq)]
pub struct LabelParams {
    pub position: u16,
    pub register_type: RegisterType,
    pub text: String,
}

#[derive(Debug, Default, PartialEq)]
pub struct DumpParams {
    pub result: Option<StatusMessage>,
}

#[derive(Debug, Default, PartialEq)]
pub struct ImportParams {
    pub pins: usize,
    pub labels: usize,
    pub rules: usize,
}

#[derive(Debug, Default, PartialEq)]
pub struct DeviceIdParams {
    pub access: DeviceIdAccess,
    pub objects: Vec<(u8, String)>,
    pub status: Option<StatusMessage>,
    pub loading: bool,
}

field_enum! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum RawField {
        Code,
        Data,
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct RawParams {
    pub code: String,
    pub data: String,
    pub selected: u16,
    pub response: Option<String>,
    pub status: Option<StatusMessage>,
}

fn clamp_pick<const N: usize, T: Copy>(selected: u16, all: &[T; N]) -> T {
    all[(selected as usize).min(N - 1)]
}

impl RawParams {
    pub fn current_field(&self) -> RawField {
        clamp_pick(self.selected, &RawField::ALL)
    }
}

field_enum! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum CustomField {
        Repr,
        Ops,
        Enum,
        Decimals,
        Prefix,
        Suffix,
        Save,
        Remove,
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct CustomParams {
    pub address: u16,
    pub register_type: RegisterType,
    pub repr: CustomRepr,
    pub ops: Vec<CustomOp>,
    pub enum_map: Vec<EnumEntry>,
    pub decimals: String,
    pub prefix: String,
    pub suffix: String,
    pub op_buffer: String,
    pub enum_buffer: String,
    pub selected: u16,
    pub existed: bool,
    pub error: Option<String>,
}

impl CustomParams {
    pub fn current_field(&self) -> CustomField {
        clamp_pick(self.selected, &CustomField::ALL)
    }
}

field_enum! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum SweepField {
        From,
        To,
        Mode,
        Action,
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SweepConfigParams {
    pub from: u16,
    pub to: u16,
    pub continuous: bool,
    pub selected: u16,
}

impl SweepConfigParams {
    pub fn current_field(&self) -> SweepField {
        clamp_pick(self.selected, &SweepField::ALL)
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct SearchParams {
    pub query: String,
    pub matches: Vec<(RegisterCell, String)>,
    pub selected: u16,
    pub top: u16,
}

impl SearchParams {
    pub fn scroll(&mut self, rows: u16) {
        let len = self.matches.len() as u16;
        scroll_window(&mut self.selected, &mut self.top, rows, len);
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct HelpParams {
    pub query: String,
    pub selected: u16,
}

#[derive(Debug, Default, PartialEq)]
pub struct ColumnsParams {
    pub query: String,
    pub selected: u16,
}

fn scroll_window(cursor: &mut u16, top: &mut u16, rows: u16, len: u16) {
    let rows = rows.max(1);
    if len == 0 {
        *cursor = 0;
        *top = 0;
        return;
    }
    *cursor = (*cursor).min(len - 1);
    if *cursor < *top {
        *top = *cursor;
    } else if *cursor >= top.saturating_add(rows) {
        *top = cursor.saturating_sub(rows - 1);
    }
    if *top >= len {
        *top = len.saturating_sub(rows).min(*cursor);
    }
}

field_enum! {
    #[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
    pub enum ReadPanel {
        #[default]
        Main,
        Pinned,
        Labeled,
        Custom,
        Matrix,
    }
}

impl ReadPanel {
    pub fn name(self) -> &'static str {
        match self {
            ReadPanel::Main => "Main",
            ReadPanel::Pinned => "Pinned",
            ReadPanel::Labeled => "Labeled",
            ReadPanel::Custom => "Custom",
            ReadPanel::Matrix => "Matrix",
        }
    }
}

field_enum! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum SettingsField {
        Name,
        RegistersBatch,
        AutoUpdate,
        HistoryCap,
        MatrixCols,
        ReadOnly,
        LogWrites,
        ApiPort,
        ApiSlaveOverride,
        StartupPanel,
        CycleHoldings,
        CycleInputs,
        CycleCoils,
        CycleDiscretes,
        IgnoreDirty,
        ClearPins,
        ClearLabels,
        ClearCustom,
        ShowContinuation,
        ShowClock,
        ShowFrameTime,
        Save,
        LoadConfig,
    }
}

impl SettingsField {
    pub fn is_text_input(self) -> bool {
        matches!(self, SettingsField::Name | SettingsField::LoadConfig)
    }

    pub fn is_toggle(self) -> bool {
        matches!(
            self,
            SettingsField::ReadOnly
                | SettingsField::ApiSlaveOverride
                | SettingsField::LogWrites
                | SettingsField::ShowContinuation
                | SettingsField::ShowClock
                | SettingsField::ShowFrameTime
                | SettingsField::StartupPanel
                | SettingsField::IgnoreDirty
                | SettingsField::CycleHoldings
                | SettingsField::CycleInputs
                | SettingsField::CycleCoils
                | SettingsField::CycleDiscretes
        )
    }

    pub fn cycle_register_type(self) -> Option<RegisterType> {
        Some(match self {
            SettingsField::CycleHoldings => RegisterType::Holding,
            SettingsField::CycleInputs => RegisterType::Input,
            SettingsField::CycleCoils => RegisterType::Coil,
            SettingsField::CycleDiscretes => RegisterType::Discrete,
            _ => return None,
        })
    }
}

field_enum! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum SettingsCategory {
        General,
        Data,
        Api,
        Display,
        Cycle,
        Clear,
        Keybinds,
        Config,
    }
}

impl SettingsCategory {
    pub fn label(self) -> &'static str {
        match self {
            SettingsCategory::General => "General",
            SettingsCategory::Data => "Data",
            SettingsCategory::Api => "API",
            SettingsCategory::Display => "Display",
            SettingsCategory::Cycle => "Cycle types",
            SettingsCategory::Clear => "Clear data",
            SettingsCategory::Keybinds => "Keybinds",
            SettingsCategory::Config => "Config",
        }
    }

    pub fn fields(self) -> &'static [SettingsField] {
        use SettingsField::*;
        match self {
            SettingsCategory::General => &[Name, StartupPanel, IgnoreDirty],
            SettingsCategory::Data => {
                &[RegistersBatch, AutoUpdate, ReadOnly, HistoryCap, MatrixCols]
            }
            SettingsCategory::Api => &[ApiPort, ApiSlaveOverride, LogWrites],
            SettingsCategory::Display => &[ShowClock, ShowFrameTime, ShowContinuation],
            SettingsCategory::Cycle => &[CycleHoldings, CycleInputs, CycleCoils, CycleDiscretes],
            SettingsCategory::Clear => &[ClearPins, ClearLabels, ClearCustom],
            SettingsCategory::Keybinds => &[],
            SettingsCategory::Config => &[Save, LoadConfig],
        }
    }

    pub fn is_keybinds(self) -> bool {
        matches!(self, SettingsCategory::Keybinds)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SettingsFocus {
    #[default]
    Categories,
    Fields,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageKind {
    Ok,
    Warn,
    Err,
    Info,
}

#[derive(Debug, Clone, PartialEq)]
pub struct StatusMessage {
    pub text: String,
    pub kind: MessageKind,
}

impl StatusMessage {
    pub fn ok(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            kind: MessageKind::Ok,
        }
    }

    pub fn warn(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            kind: MessageKind::Warn,
        }
    }

    pub fn err(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            kind: MessageKind::Err,
        }
    }

    pub fn info(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            kind: MessageKind::Info,
        }
    }
}

pub type Outcome = Result<String, String>;

impl From<Outcome> for StatusMessage {
    fn from(result: Outcome) -> Self {
        match result {
            Ok(text) => Self::ok(text),
            Err(text) => Self::err(text),
        }
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct SettingsParams {
    pub category: u16,
    pub field: u16,
    pub focus: SettingsFocus,
    pub status: Option<StatusMessage>,
    pub load_path: String,
    pub previous: ReadParams,
    pub kb_selected: u16,
    pub kb_top: u16,
    pub kb_capturing: bool,
}

impl SettingsParams {
    pub const KB_VISIBLE: u16 = 14;

    pub fn current_category(&self) -> SettingsCategory {
        SettingsCategory::ALL[self.category as usize]
    }

    pub fn current_fields(&self) -> &'static [SettingsField] {
        self.current_category().fields()
    }

    pub fn current_field(&self) -> Option<SettingsField> {
        self.current_fields().get(self.field as usize).copied()
    }

    pub fn enter_category(&mut self) {
        self.focus = SettingsFocus::Fields;
        self.field = 0;
        if self.current_category().is_keybinds() {
            self.kb_selected = 0;
            self.kb_top = 0;
            self.kb_capturing = false;
        }
    }

    pub fn kb_move(&mut self, up: bool, count: u16) {
        if count == 0 {
            return;
        }
        self.kb_selected = wrap_index(self.kb_selected, count, !up);
        self.kb_scroll_into_view(count);
    }

    pub fn kb_page(&mut self, up: bool, count: u16) {
        if count == 0 {
            return;
        }
        self.kb_selected = if up {
            self.kb_selected.saturating_sub(Self::KB_VISIBLE)
        } else {
            (self.kb_selected + Self::KB_VISIBLE).min(count - 1)
        };
        self.kb_scroll_into_view(count);
    }

    fn kb_scroll_into_view(&mut self, count: u16) {
        scroll_window(
            &mut self.kb_selected,
            &mut self.kb_top,
            Self::KB_VISIBLE,
            count,
        );
    }
}

#[derive(Debug, Default, PartialEq)]
pub struct LogsParams {
    pub path: String,
    pub lines: Vec<String>,
    pub top: u16,
}

impl LogsParams {
    pub const VISIBLE: u16 = 16;

    pub fn scroll(&mut self, delta: i32) {
        let len = self.lines.len() as i32;
        let max_top = (len - Self::VISIBLE as i32).max(0);
        self.top = (self.top as i32 + delta).clamp(0, max_top) as u16;
    }

    pub fn scroll_to_bottom(&mut self) {
        self.scroll(i32::MAX);
    }
}

field_enum! {
    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
    pub enum InspectMode {
        #[default]
        Now,
        Min,
        Max,
        Avg,
    }
}

impl InspectMode {
    pub fn name(self) -> &'static str {
        match self {
            InspectMode::Now => "now",
            InspectMode::Min => "min",
            InspectMode::Max => "max",
            InspectMode::Avg => "avg",
        }
    }
}

popups! {
    Discovery(DiscoveryParams),
    Help(HelpParams),
    About,
    Dump(DumpParams),
    Search(SearchParams),
    Label(LabelParams),
    Custom(CustomParams),
    Columns(ColumnsParams),
    Write(WriteParams),
    Slave(u16),
    Logs(LogsParams),
    SweepConfig(SweepConfigParams),
    Inspect(InspectMode),
    Stats,
    DeviceId(DeviceIdParams),
    Raw(RawParams),
    Import(ImportParams),
    Quit,
}

#[derive(Debug, PartialEq)]
pub struct ReadParams {
    pub position: u16,
    pub window_start: u16,
    pub col_offset: u16,
    pub panel: ReadPanel,
    pub pinned_index: u16,
    pub pinned_top: u16,
    pub popup: Option<Popup>,
    pub graph: bool,
    pub graph_column: Column,
    pub refresh_timer: Instant,
    pub register_type: RegisterType,
    pub read_duration: Option<Duration>,
    pub loading: bool,
    pub read_error: Option<String>,
    pub status: Option<StatusMessage>,
    pub status_at: Instant,
}

const STATUS_TTL: Duration = Duration::from_secs(4);

impl Default for ReadParams {
    fn default() -> Self {
        Self {
            position: 0,
            window_start: 0,
            col_offset: 0,
            panel: ReadPanel::Main,
            pinned_index: 0,
            pinned_top: 0,
            popup: None,
            graph: false,
            graph_column: Column::U16,
            refresh_timer: Instant::now(),
            register_type: Default::default(),
            read_duration: None,
            loading: false,
            read_error: None,
            status: None,
            status_at: Instant::now(),
        }
    }
}

impl ReadParams {
    pub fn active_status(&self) -> Option<&StatusMessage> {
        self.status
            .as_ref()
            .filter(|_| self.status_at.elapsed() < STATUS_TTL)
    }

    pub fn scroll_to_cursor(&mut self, rows: u16, matrix_cols: u16) {
        let rows = rows.max(1);
        if self.panel == ReadPanel::Matrix {
            let cols = matrix_cols.max(1);
            let last_row = u16::MAX / cols;
            let max_start_row = last_row.saturating_sub(rows - 1);
            let row = self.position / cols;
            let start_row = row.saturating_sub(rows / 2).min(max_start_row);
            self.window_start = start_row.saturating_mul(cols);
            return;
        }
        let max_start = u16::MAX - (rows - 1);
        self.window_start = self.position.saturating_sub(rows / 2).min(max_start);
    }

    pub fn toggle_panel(&mut self) {
        self.panel = cycle(&ReadPanel::ALL, self.panel, true);
    }

    pub fn scroll_pinned(&mut self, rows: u16, len: u16) {
        scroll_window(&mut self.pinned_index, &mut self.pinned_top, rows, len);
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub enum ConnectionStatus {
    #[default]
    Unknown,
    Reading,
    Connected,
    Reconnecting,
    Error(String),
}

impl ConnectionStatus {
    pub fn code(&self) -> u8 {
        match self {
            ConnectionStatus::Unknown => 0,
            ConnectionStatus::Reading => 1,
            ConnectionStatus::Connected => 2,
            ConnectionStatus::Reconnecting => 3,
            ConnectionStatus::Error(_) => 4,
        }
    }

    pub fn label_from_code(code: u8) -> &'static str {
        match code {
            1 => "reading",
            2 => "connected",
            3 => "reconnecting",
            4 => "error",
            _ => "unknown",
        }
    }

    pub fn code_serving(code: u8) -> bool {
        matches!(code, 0..=2)
    }
}

#[derive(Debug, PartialEq)]
pub struct LogViewParams {
    pub top: u16,
    pub follow: bool,
    pub previous: ReadParams,
}

#[derive(Debug, PartialEq)]
pub enum State {
    Read(ReadParams),
    Settings(SettingsParams),
    Logs(LogViewParams),
}
