mod parser;
mod renderer;
mod token;

use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
use crate::clients::sysinfo::TokenType;
use crate::config::{CommonConfig, LayoutConfig, ModuleOrientation};
use crate::gtk_helpers::IronbarLabelExt;
use crate::modules::sysinfo::token::Part;
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
use crate::{clients, module_impl, spawn};
use color_eyre::Result;
use gtk::Label;
use gtk::prelude::*;
use serde::Deserialize;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::sleep;

#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
#[serde(default)]
pub struct SysInfoModule {
    /// List of strings including formatting tokens.
    /// For available tokens, see [below](#formatting-tokens).
    ///
    /// **Required**
    format: Vec<String>,

    /// Number of seconds between refresh.
    ///
    /// This can be set as a global interval,
    /// or passed as an object to customize the interval per-system.
    ///
    /// **Default**: `5`
    interval: Interval,

    /// The orientation by which the labels are laid out.
    ///
    /// **Valid options**: `horizontal`, `vertical`, `h`, `v`
    /// <br>
    /// **Default** : `horizontal`
    direction: Option<ModuleOrientation>,

    // -- common --
    /// See [layout options](module-level-options#layout)
    #[serde(flatten)]
    layout: LayoutConfig,

    /// See [common options](module-level-options#common-options).
    #[serde(flatten)]
    pub common: Option<CommonConfig>,
}

impl Default for SysInfoModule {
    fn default() -> Self {
        Self {
            format: vec![],
            interval: Interval::default(),
            direction: None,
            layout: LayoutConfig::default(),
            common: Some(CommonConfig::default()),
        }
    }
}

#[derive(Debug, Deserialize, Copy, Clone)]
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
#[serde(default)]
pub struct Intervals {
    /// The number of seconds between refreshing memory data.
    ///
    /// **Default**: `5`
    memory: u64,

    /// The number of seconds between refreshing CPU data.
    ///
    /// **Default**: `5`
    cpu: u64,

    /// The number of seconds between refreshing temperature data.
    ///
    /// **Default**: `5`
    temps: u64,

    /// The number of seconds between refreshing disk data.
    ///
    /// **Default**: `5`
    disks: u64,

    /// The number of seconds between refreshing network data.
    ///
    /// **Default**: `5`
    networks: u64,

    /// The number of seconds between refreshing system data.
    ///
    /// **Default**: `5`
    system: u64,
}

impl Default for Intervals {
    fn default() -> Self {
        Self {
            memory: 5,
            cpu: 5,
            temps: 5,
            disks: 5,
            networks: 5,
            system: 5,
        }
    }
}

#[derive(Debug, Deserialize, Copy, Clone)]
#[serde(untagged)]
#[cfg_attr(feature = "extras", derive(schemars::JsonSchema))]
pub enum Interval {
    All(u64),
    Individual(Intervals),
}

impl Default for Interval {
    fn default() -> Self {
        Self::All(5)
    }
}

impl Interval {
    const fn memory(self) -> u64 {
        match self {
            Self::All(n) => n,
            Self::Individual(intervals) => intervals.memory,
        }
    }

    const fn cpu(self) -> u64 {
        match self {
            Self::All(n) => n,
            Self::Individual(intervals) => intervals.cpu,
        }
    }

    const fn temps(self) -> u64 {
        match self {
            Self::All(n) => n,
            Self::Individual(intervals) => intervals.temps,
        }
    }

    pub const fn disks(self) -> u64 {
        match self {
            Self::All(n) => n,
            Self::Individual(intervals) => intervals.disks,
        }
    }

    pub const fn networks(self) -> u64 {
        match self {
            Self::All(n) => n,
            Self::Individual(intervals) => intervals.networks,
        }
    }

    const fn system(self) -> u64 {
        match self {
            Self::All(n) => n,
            Self::Individual(intervals) => intervals.system,
        }
    }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum RefreshType {
    Memory,
    Cpu,
    Temps,
    Disks,
    Network,
    System,
}

impl TokenType {
    fn is_affected_by(self, refresh_type: RefreshType) -> bool {
        match self {
            Self::CpuFrequency | Self::CpuPercent => refresh_type == RefreshType::Cpu,
            Self::MemoryFree
            | Self::MemoryAvailable
            | Self::MemoryTotal
            | Self::MemoryUsed
            | Self::MemoryPercent
            | Self::SwapFree
            | Self::SwapTotal
            | Self::SwapUsed
            | Self::SwapPercent => refresh_type == RefreshType::Memory,
            Self::TempC | Self::TempF => refresh_type == RefreshType::Temps,
            Self::DiskFree
            | Self::DiskTotal
            | Self::DiskUsed
            | Self::DiskPercent
            | Self::DiskRead
            | Self::DiskWrite => refresh_type == RefreshType::Disks,
            Self::NetDown | Self::NetUp => refresh_type == RefreshType::Network,
            Self::LoadAverage1 | Self::LoadAverage5 | Self::LoadAverage15 => {
                refresh_type == RefreshType::System
            }
            Self::Uptime => refresh_type == RefreshType::System,
        }
    }
}

impl Module<gtk::Box> for SysInfoModule {
    type SendMessage = (usize, String);
    type ReceiveMessage = ();

    module_impl!("sysinfo");

    fn spawn_controller(
        &self,
        _info: &ModuleInfo,
        context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
        _rx: mpsc::Receiver<Self::ReceiveMessage>,
    ) -> Result<()> {
        let interval = self.interval;

        let client = context.client::<clients::sysinfo::Client>();

        let format_tokens = self
            .format
            .iter()
            .map(|format| parser::parse_input(format.as_str()))
            .collect::<Result<Vec<_>>>()?;

        for (i, token_set) in format_tokens.iter().enumerate() {
            let rendered = Part::render_all(token_set, &client, interval);
            context.tx.send_update_spawn((i, rendered));
        }

        let (refresh_tx, mut refresh_rx) = mpsc::channel(16);

        macro_rules! spawn_refresh {
            ($refresh_type:expr, $func:ident) => {{
                let tx = refresh_tx.clone();
                spawn(async move {
                    loop {
                        tx.send_expect($refresh_type).await;
                        sleep(Duration::from_secs(interval.$func())).await;
                    }
                });
            }};
        }

        spawn_refresh!(RefreshType::Memory, memory);
        spawn_refresh!(RefreshType::Cpu, cpu);
        spawn_refresh!(RefreshType::Temps, temps);
        spawn_refresh!(RefreshType::Disks, disks);
        spawn_refresh!(RefreshType::Network, networks);
        spawn_refresh!(RefreshType::System, system);

        let tx = context.tx.clone();
        spawn(async move {
            while let Some(refresh) = refresh_rx.recv().await {
                match refresh {
                    RefreshType::Memory => client.refresh_memory(),
                    RefreshType::Cpu => client.refresh_cpu(),
                    RefreshType::Temps => client.refresh_temps(),
                    RefreshType::Disks => client.refresh_disks(),
                    RefreshType::Network => client.refresh_network(),
                    RefreshType::System => client.refresh_load_average(),
                }

                for (i, token_set) in format_tokens.iter().enumerate() {
                    let is_affected = token_set
                        .iter()
                        .filter_map(|part| {
                            if let Part::Token(token) = part {
                                Some(token)
                            } else {
                                None
                            }
                        })
                        .any(|t| t.token.is_affected_by(refresh));

                    if is_affected {
                        let rendered = Part::render_all(token_set, &client, interval);
                        tx.send_update((i, rendered)).await;
                    }
                }
            }
        });

        Ok(())
    }

    fn into_widget(
        self,
        context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
        info: &ModuleInfo,
    ) -> Result<ModuleParts<gtk::Box>> {
        let layout = match self.direction {
            Some(orientation) => orientation.into(),
            None => self.layout.orientation(info),
        };

        let container = gtk::Box::new(layout, 10);

        let mut labels = Vec::new();

        for _ in &self.format {
            let label = Label::builder()
                .use_markup(true)
                .justify(self.layout.justify.into())
                .build();

            label.add_css_class("item");

            container.append(&label);
            labels.push(label);
        }

        context.subscribe().recv_glib((), move |(), data| {
            let label = &labels[data.0];
            label.set_label_escaped(&data.1);
        });

        Ok(ModuleParts {
            widget: container,
            popup: None,
        })
    }
}
