use crate::I18N;
use crate::app::{
    components::{
        info_row::{InfoRow, InfoRowExt},
        info_row_level::InfoRowLevel,
        page_section::PageSection,
    },
    utils::{
        ext::FlowBoxExt,
        formatting::{self, Mono},
    },
};
use gtk::pango::AttrList;
use gtk::prelude::{BoxExt, OrientableExt, PopoverExt as _, WidgetExt};
use i18n_embed_fl::fl;
use lact_schema::{DeviceInfo, DeviceStats, PowerStates, PowerStats};
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt as _};
use std::collections::HashSet;
use std::str::FromStr as _;
use std::sync::Arc;

pub struct GpuStatsSection {
    stats_to_show: HashSet<GpuStat>,
    stats: Arc<DeviceStats>,
    vram_clock_ratio: f64,
    max_gpu_clock: Option<u64>,
    max_vram_clock: Option<u64>,
    min_gpu_clock: Option<u64>,
    min_vram_clock: Option<u64>,
}

#[derive(Debug)]
pub enum GpuStatsSectionMsg {
    Info(Arc<DeviceInfo>),
    Stats(Arc<DeviceStats>),
    PowerStates(Arc<PowerStates>),
}

#[relm4::component(pub)]
impl relm4::SimpleComponent for GpuStatsSection {
    type Input = GpuStatsSectionMsg;
    type Output = ();
    type Init = GpuStatsSectionConfig;

    view! {
        gtk::Box {
            add_css_class: "gpu-stats-section",
            set_orientation: gtk::Orientation::Vertical,
            set_spacing: 10,

            PageSection {
                #[watch]
                set_visible: model.visible_count(GpuStatDisplay::Digits) > 0,

                append_child = &gtk::FlowBox {
                    set_orientation: gtk::Orientation::Horizontal,
                    set_column_spacing: 10,
                    set_row_spacing: 10,
                    set_homogeneous: true,

                    #[watch]
                    set_max_children_per_line: model.columns(GpuStatDisplay::Digits),
                    set_selection_mode: gtk::SelectionMode::None,

                    append_child = &InfoRow {
                        set_name: fl!(I18N, "throttling"),
                        #[watch]
                        set_value: formatting::fmt_throttling_text(&model.stats),
                    } -> throttling_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::Throttling),
                    },

                    append_child = &InfoRow {
                        set_name: fl!(I18N, "gpu-clock-target"),
                        #[watch]
                        set_value: format_current_gfxclk(model.stats.clockspeed.target_gpu_clockspeed),
                    } -> clockspeed_target_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::GpuClockTarget),
                    },

                    append_child = &InfoRow {
                        set_name: fl!(I18N, "gpu-voltage"),
                        #[watch]
                        set_value: format!("{} V", Mono::float(model.stats.voltage.gpu.unwrap_or(0) as f64 / 1000f64, 3)),
                    } -> gpu_voltage_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::GpuVoltage),
                    },


                    append_child = &InfoRow {
                        set_name: fl!(I18N, "gpu-temp"),
                        #[watch]
                        set_value: if primary_temperatures.is_empty() {
                            fl!(I18N, "no-sensors-found")
                        } else {
                            primary_temperatures.join(", ")
                        },
                    } -> basic_temps_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::Temperature) && secondary_temperatures.is_empty(),
                    },

                    append_child = &InfoRow {
                        set_name: fl!(I18N, "gpu-temp"),
                        #[watch]
                        set_value: if primary_temperatures.is_empty() {
                            "N/A".to_owned()
                        } else {
                            primary_temperatures.join(", ")
                        },

                        set_icon: "go-down-symbolic".to_string(),

                        #[name = "secondary_temps_popover"]
                        set_popover = &gtk::Popover {
                            gtk::Label {
                                set_margin_all: 10,
                                set_selectable: false,
                                set_use_markup: true,
                                set_attributes: Some(&AttrList::from_str("0 -1 weight bold").unwrap()),

                                #[watch]
                                set_label: &secondary_temperatures.join("\n"),
                            },
                        },

                        connect_clicked[secondary_temps_popover] => move |_| {
                            secondary_temps_popover.popup();
                        },
                    } -> full_temps_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::Temperature) && !secondary_temperatures.is_empty(),
                    },

                    append_child = &InfoRow {
                        set_name: fl!(I18N, "extra-clocks"),
                        set_max_value_width_chars: 40,
                        #[watch]
                        set_value: extra_clocks.join(", "),

                        set_icon: "go-down-symbolic".to_string(),

                        #[name = "secondary_clocks_popover"]
                        set_popover = &gtk::Popover {
                            gtk::Label {
                                set_margin_all: 10,
                                set_selectable: false,
                                set_use_markup: true,
                                set_attributes: Some(&AttrList::from_str("0 -1 weight bold").unwrap()),

                                #[watch]
                                set_label: &extra_clocks.join("\n"),
                            },
                        },

                        connect_clicked[secondary_clocks_popover] => move |_| {
                            secondary_clocks_popover.popup();
                        },
                    } -> extra_clocks_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::ExtraClocks),
                    },
                },
            },

            PageSection {
                #[watch]
                set_visible: model.visible_count(GpuStatDisplay::LevelBar) > 0,

                append_child = &gtk::FlowBox {
                    set_orientation: gtk::Orientation::Horizontal,
                    set_column_spacing: 10,
                    set_homogeneous: true,
                    #[watch]
                    set_max_children_per_line: model.columns(GpuStatDisplay::LevelBar),
                    set_selection_mode: gtk::SelectionMode::None,

                    append_child = &InfoRowLevel {
                        #[watch]
                        set_name: {
                            if model.stats.clockspeed.gpu_clockspeed.is_some()
                                && model.stats.clockspeed.target_gpu_clockspeed.is_some() {
                                    fl!(I18N, "gpu-clock-avg")
                                } else {
                                    fl!(I18N, "gpu-clock")
                                }
                        },
                        #[watch]
                        set_value: formatting::fmt_clockspeed(
                            model.stats.clockspeed.gpu_clockspeed,
                            1.0,
                        ),
                        #[watch]
                        set_level_value: {
                            match (&model.stats.clockspeed.gpu_clockspeed, model.max_gpu_clock, model.min_gpu_clock) {
                                (Some(cur), Some(max), Some(min)) if max > min => {
                                    (cur.saturating_sub(min) as f64) / (max.saturating_sub(min) as f64)
                                }
                                _ => 0.0,
                            }
                        },
                        set_value_size_group: &value_size_group,
                    } -> gpu_clock_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::GpuClock),
                    },

                    append_child = &InfoRowLevel {
                        set_name: fl!(I18N, "vram-clock"),
                        set_value_size_group: &value_size_group,
                        #[watch]
                        set_value: formatting::fmt_clockspeed(
                            model.stats.clockspeed.vram_clockspeed,
                            model.vram_clock_ratio,
                        ),
                        #[watch]
                        set_level_value: {
                            match (&model.stats.clockspeed.vram_clockspeed, model.max_vram_clock, model.min_vram_clock) {
                                (Some(cur), Some(max), Some(min)) if max > min => {
                                    (cur.saturating_sub(min) as f64) / (max.saturating_sub(min) as f64)
                                }
                                _ => 0.0,
                            }
                        }
                    } -> vram_clock_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::VramClock),
                    },

                    append_child = &InfoRowLevel {
                        set_name: fl!(I18N, "gpu-usage"),
                        set_value_size_group: &value_size_group,
                        #[watch]
                        set_value: format!("{}%", Mono::uint(model.stats.busy_percent.unwrap_or(0))),
                        #[watch]
                        set_level_value: model.stats.busy_percent.unwrap_or(0) as f64 / 100.0,
                    } -> gpu_usage_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::GpuUsage),
                    },

                    append_child = &InfoRowLevel {
                        set_name: fl!(I18N, "vram-usage"),
                        set_value_size_group: &value_size_group,
                        #[watch]
                        set_value: formatting::fmt_human_bytes(
                            model.stats.vram.used.unwrap_or(0),
                            Some(formatting::ByteUnit::Gibibyte),
                        ),
                        #[watch]
                        set_level_value: model
                            .stats
                            .vram
                            .used
                            .zip(model.stats.vram.total)
                            .map(|(used, total)| used as f64 / total as f64)
                            .unwrap_or(0.0),
                    } -> vram_usage_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::VramUsage),
                    },

                    append_child = &InfoRowLevel {
                        set_name: fl!(I18N, "gtt-usage"),
                        set_value_size_group: &value_size_group,
                        #[watch]
                        set_value: formatting::fmt_human_bytes(
                            model.stats.vram.gtt_used.unwrap_or(0),
                            Some(formatting::ByteUnit::Gibibyte),
                        ),
                        #[watch]
                        set_level_value: model
                            .stats
                            .vram
                            .gtt_used
                            .zip(model.stats.vram.gtt_total_usable)
                            .map(|(used, total)| used as f64 / total as f64)
                            .unwrap_or(0.0),
                    } -> gtt_usage_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::GttUsage),
                    },

                    append_child = &InfoRowLevel {
                        set_name: fl!(I18N, "power-usage"),
                        set_value_size_group: &value_size_group,
                        #[watch]
                        set_value: {
                            let PowerStats {
                                average: power_average,
                                current: power_current,
                                ..
                            } = model.stats.power;

                            let power_current = power_current
                                .filter(|value| *value != 0.0)
                                .or(power_average);

                            format!(
                                "{} {}",
                                Mono::float(power_current.unwrap_or(0.0), 1),
                                fl!(I18N, "watt")
                            )
                        },
                        #[watch]
                        set_level_value: {
                            let PowerStats {
                                average: power_average,
                                current: power_current,
                                cap_current: power_cap_current,
                                ..
                            } = model.stats.power;

                            let power_current = power_current
                                .filter(|value| *value != 0.0)
                                .or(power_average);

                            let power_cap_current = power_cap_current
                                .filter(|value| *value != 0.0);

                            power_current
                                .zip(power_cap_current)
                                .map(|(current, cap)| current / cap)
                                .unwrap_or(0.0)
                        },
                    } -> power_usage_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::PowerUsage),
                    },

                    append_child = &InfoRowLevel {
                        set_name: fl!(I18N, "fan-speed"),
                        set_value_size_group: &value_size_group,
                        #[watch]
                        set_value: formatting::fmt_fan_speed(&model.stats, true)
                            .unwrap_or_else(|| fl!(I18N, "no-fan-detected")),
                        #[watch]
                        set_level_value: model.stats.fan.pwm_current.map(|pwm| pwm as f64 / u8::MAX as f64).unwrap_or(0.0),
                    } -> fan_speed_item: gtk::FlowBoxChild {
                        #[watch]
                        set_visible: model.shows(GpuStat::FanSpeed),
                    },
                },
            },
        }
    }

    fn init(
        config: Self::Init,
        root: Self::Root,
        _sender: ComponentSender<Self>,
    ) -> ComponentParts<Self> {
        let value_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);

        let (primary_temperatures, secondary_temperatures): (Vec<String>, Vec<String>) =
            (Vec::new(), Vec::new());
        let extra_clocks: Vec<String> = Vec::new();

        let model = Self {
            stats_to_show: config.stats,
            stats: Arc::new(DeviceStats::default()),
            vram_clock_ratio: 1.0,
            max_gpu_clock: None,
            max_vram_clock: None,
            min_gpu_clock: None,
            min_vram_clock: None,
        };

        let widgets = view_output!();

        ComponentParts { widgets, model }
    }

    fn update(&mut self, msg: Self::Input, _sender: relm4::ComponentSender<Self>) {
        match msg {
            GpuStatsSectionMsg::Info(info) => {
                self.vram_clock_ratio = info.vram_clock_ratio();
            }
            GpuStatsSectionMsg::Stats(stats) => {
                self.stats = stats;
            }
            GpuStatsSectionMsg::PowerStates(pstates) => {
                self.max_gpu_clock = pstates.max_gpu_clock();
                self.max_vram_clock = pstates.max_vram_clock();
                self.min_gpu_clock = pstates.min_gpu_clock();
                self.min_vram_clock = pstates.min_vram_clock();
            }
        }
    }

    fn pre_view(&self) {
        let (primary_temperatures, secondary_temperatures) =
            formatting::fmt_temperature_text(&model.stats);

        let extra_clocks = extra_clocks(&model.stats);
    }
}

#[derive(Debug, Clone)]
pub struct GpuStatsSectionConfig {
    pub stats: HashSet<GpuStat>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GpuStat {
    Throttling,
    GpuClockTarget,
    GpuVoltage,
    Temperature,
    GpuClock,
    VramClock,
    ExtraClocks,
    GpuUsage,
    VramUsage,
    GttUsage,
    PowerUsage,
    FanSpeed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GpuStatDisplay {
    Digits,
    LevelBar,
}

impl GpuStat {
    /// Exhaustive on purpose: a new stat does not compile until it has been
    /// given a display.
    fn display(self) -> GpuStatDisplay {
        match self {
            Self::Throttling
            | Self::GpuClockTarget
            | Self::GpuVoltage
            | Self::Temperature
            | Self::ExtraClocks => GpuStatDisplay::Digits,
            Self::GpuClock
            | Self::VramClock
            | Self::GpuUsage
            | Self::VramUsage
            | Self::GttUsage
            | Self::PowerUsage
            | Self::FanSpeed => GpuStatDisplay::LevelBar,
        }
    }
}

impl GpuStatsSection {
    /// Whether a stat is both configured to be shown and reported by this GPU.
    fn shows(&self, stat: GpuStat) -> bool {
        self.stats_to_show.contains(&stat) && self.has_data_for(stat)
    }

    fn has_data_for(&self, stat: GpuStat) -> bool {
        match stat {
            // Rendered with a placeholder when unavailable
            GpuStat::Throttling | GpuStat::Temperature | GpuStat::VramUsage | GpuStat::FanSpeed => {
                true
            }
            GpuStat::GpuClockTarget => self.stats.clockspeed.target_gpu_clockspeed.is_some(),
            GpuStat::GpuVoltage => self.stats.voltage.gpu.is_some(),
            GpuStat::GpuClock => self.stats.clockspeed.gpu_clockspeed.is_some(),
            GpuStat::VramClock => self.stats.clockspeed.vram_clockspeed.is_some(),
            GpuStat::GpuUsage => self.stats.busy_percent.is_some(),
            GpuStat::GttUsage => self.stats.vram.gtt_used.is_some(),
            GpuStat::PowerUsage => {
                self.stats.power.average.is_some() || self.stats.power.current.is_some()
            }
            GpuStat::ExtraClocks => !self.stats.clockspeed.sensors.is_empty(),
        }
    }

    fn visible_count(&self, display: GpuStatDisplay) -> u32 {
        self.stats_to_show
            .iter()
            .filter(|stat| stat.display() == display && self.has_data_for(**stat))
            .count() as u32
    }

    fn columns(&self, display: GpuStatDisplay) -> u32 {
        self.visible_count(display).clamp(1, 3)
    }
}

fn format_current_gfxclk(value: Option<u64>) -> String {
    if let Some(v) = value {
        // if the APU/GPU does not actually support current_gfxclk,
        // the value will be `u16::MAX (65535)`
        if v >= u16::MAX as u64 || v == 0 {
            fl!(I18N, "missing-stat")
        } else {
            formatting::fmt_clockspeed(Some(v), 1.0)
        }
    } else {
        fl!(I18N, "missing-stat")
    }
}

fn extra_clocks(stats: &DeviceStats) -> Vec<String> {
    stats
        .clockspeed
        .sensors
        .iter()
        .map(|(name, value)| format!("{name}: {}", formatting::fmt_clockspeed(Some(*value), 1.0)))
        .collect::<Vec<_>>()
}
