// SPDX-License-Identifier: MIT OR Apache-2.0
// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder

//! AOOSTAR-X json configuration file format.
//!
//! Derived from the available Monitor3.json file in AOOSTAR-X v1.3.4.
//! Likely not fully compatible with files created with the original editor.

use anyhow::Context;
use image::{Rgb, Rgba};
use imageproc::definitions::HasWhite;
use log::{info, warn};
use regex::Regex;
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::collections::HashMap;
use std::io::BufReader;
use std::num::ParseIntError;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{fmt, fs};

pub fn load_cfg<P: AsRef<Path>>(path: P) -> anyhow::Result<MonitorConfig> {
    let path = path.as_ref();
    let file = fs::File::open(path).with_context(|| format!("Failed to load config {path:?}"))?;
    let reader = BufReader::new(file);
    let config: MonitorConfig = serde_json::from_reader(reader)?;

    for active in config.active_panels.clone() {
        if active == 0 || active > config.panels.len() as u32 {
            warn!("Ignoring invalid active panel {active}");
            continue;
        }
        let panel = &config.panels[active as usize - 1];

        println!(
            "Panel {active}: {}",
            panel.img.as_deref().unwrap_or_default()
        );
        for sensor in &panel.sensor {
            println!(
                "  {}: {} {} {}",
                sensor.label,
                sensor
                    .name
                    .as_deref()
                    .or(sensor.item_name.as_deref())
                    .unwrap_or_default(),
                sensor.value.as_deref().unwrap_or_default(),
                sensor.unit.as_deref().unwrap_or_default()
            );
        }
    }

    Ok(config)
}

/// Load a custom panel configuration.
///
/// The distributed panel ZIP file must be extracted and contain:
/// - `panel.json` configuration file
/// - `img` subdirectory containing the referenced images in panel.json
/// - `fonts` subdirectory containing the referenced fonts in panel.json
///
/// # Arguments
///
/// * `path`: directory path of the extracted custom panel.
///
/// returns: Result<Panel, Error>
pub fn load_custom_panel<P: AsRef<Path>>(path: P) -> anyhow::Result<Panel> {
    let path = path.as_ref();
    let panel_file = path.join("panel.json");

    info!("Loading custom panel {panel_file:?}");

    let file = fs::File::open(&panel_file)
        .with_context(|| format!("Failed to load custom panel {panel_file:?}"))?;
    let reader = BufReader::new(file);
    let mut panel: Panel = serde_json::from_reader(reader)?;

    // adjust font and image file paths
    let img_path = fs::canonicalize(path.join("img"))?;
    let font_path = fs::canonicalize(path.join("fonts"))?;
    if let Some(img) = &panel.img
        && !Path::new(img).is_absolute()
    {
        panel.img = Some(img_path.join(img).display().to_string());
    }
    for sensor in panel.sensor.iter_mut() {
        if let Some(pic) = &sensor.pic
            && !Path::new(pic).is_absolute()
        {
            sensor.pic = Some(img_path.join(pic).display().to_string());
        }
        if let Some(font_family) = &sensor.font_family
            && !font_family.is_empty()
            && !Path::new(&font_family).is_absolute()
        {
            sensor.font_family = Some(font_path.join(font_family).display().to_string());
        }
    }

    Ok(panel)
}

/// AOOSTAR-X monitor json configuration file
#[derive(Debug, Serialize, Deserialize)]
pub struct MonitorConfig {
    // _Not used_
    // pub credentials: Option<Credentials>,
    /// Configuration settings.
    pub setup: Setup,
    /// Panels: 1-based index into `panels`
    #[serde(rename = "mianban")]
    pub active_panels: Vec<u32>,
    /// Custom panels / DIY "Do It Yourself",
    #[serde(rename = "diy")]
    pub panels: Vec<Panel>,
    /// Internal index of the currently active panel. 1-based!
    #[serde(skip)]
    active_panel_idx: Option<usize>,
    /// Internal sensor label mapping
    #[serde(skip)]
    sensor_mapping: Option<HashMap<String, String>>,
    /// Internal sensor filter
    #[serde(skip)]
    pub sensor_filter: Option<Vec<Regex>>,
}

impl MonitorConfig {
    pub fn get_next_active_panel(&mut self) -> Option<&Panel> {
        let mut active_panel_idx = self.active_panel_idx.unwrap_or(0) + 1;
        if active_panel_idx > self.panels.len() {
            active_panel_idx = 1;
        }

        for (index, active) in self
            .active_panels
            .iter()
            .filter(|&active| *active > 0)
            .enumerate()
        {
            if *active > self.panels.len() as u32 {
                warn!("Ignoring invalid active panel {active}");
                continue;
            }
            if index + 1 == active_panel_idx {
                self.active_panel_idx = Some(active_panel_idx);
                return Some(&self.panels[*active as usize - 1]);
            }
        }

        None
    }

    /// Adds a custom panel to the application and maps sensor labels if applicable.
    ///
    /// The panel is marked active and will be returned with [get_next_active_panel] when it is its turn.
    ///
    /// # Arguments
    ///
    /// * `panel` - the Panel to include in the active panels.
    pub fn include_custom_panel(&mut self, mut panel: Panel) {
        if let Some(mapping) = &self.sensor_mapping {
            panel.map_sensor_labels(mapping);
        }
        self.panels.push(panel);
        self.active_panels.push(self.panels.len() as u32);
    }

    /// Apply a sensor label mapping on the included panels.
    ///
    /// The mapping will also be applied on any custom panel added in the future with [include_custom_panel].
    ///
    /// **Attention**: this method may only be called once at startup.
    /// Dynamically changing mappings are not supported, and the original sensor labels are not preserved.
    pub fn set_sensor_mapping(&mut self, mapping: HashMap<String, String>) {
        for panel in self.panels.iter_mut() {
            panel.map_sensor_labels(&mapping);
        }
        self.sensor_mapping = Some(mapping);
    }
}

/// Web-app user login
///
/// Not used, part of AOOSTAR-X json configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Credentials {
    pub username: String,
    pub password: String,
}

/// Configuration settings.
///
/// Note: Trimmed down object to include only required fields for `asterctl`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Setup {
    /// Switch time between panels in seconds, interpreted as float and converted to milliseconds. Default: 5
    pub switch_time: Option<String>, // existed as "30" string
    /// Panel redraw interval in seconds. Default: 1
    pub refresh: f32,
    /*
    // The following fields of the AOOSTAR-X json configuration file are NOT used in `asterctl`
    /// Default: true
    pub off_display: bool,
    /// Selection of default panels based on theme / control_params / control_disk_temp ?
    pub theme: i32,
    /// ? Default: true
    pub control_params: bool,
    /// ? Default: true
    pub control_disk_temp: bool,
    /// Default: false
    pub custom_panel: bool,
    /// Language index. Default: 0
    pub language: Language,
    /// Operation mode: performance, power saving, etc.
    pub operation_mode: Option<OperationMode>,
    /// Operation type 1 or 2 (?). Default: 1
    #[serde(rename = "type")]
    pub operation_type: Option<i16>,
    /// Default: 300
    pub disk_update: i32,
    /// Home Assistant URL
    #[serde(deserialize_with = "empty_string_as_none")]
    #[serde(rename = "ha_url")]
    pub ha_url: Option<String>, // "" in JSON ⇒ Option<String>
    /// Home Assistant long-lived access token
    #[serde(deserialize_with = "empty_string_as_none")]
    #[serde(rename = "ha_token")]
    pub ha_token: Option<String>, // "" in JSON ⇒ Option<String>
    */
}

/// Language setting.
///
/// Not used, part of AOOSTAR-X json configuration file.
#[derive(Debug, Copy, Clone, Serialize_repr, Deserialize_repr, PartialEq)]
#[repr(u8)]
#[allow(dead_code)]
pub enum Language {
    Chinese = 0,
    English = 1,
    Japanese = 2,
}

/// Not used, part of AOOSTAR-X json configuration file.
#[derive(Debug, Copy, Clone, Serialize_repr, Deserialize_repr, PartialEq)]
#[repr(i16)]
#[allow(dead_code)]
pub enum OperationMode {
    None = -1,
    HighPerformance = 0,
    Intelligent = 1,
    PowerSaving = 2,
    Custom30W = 3,
    Custom20W = 4,
    Custom10W = 5,
}

#[derive(Debug, Copy, Clone, Serialize_repr, Deserialize_repr, PartialEq)]
#[repr(u8)]
pub enum SensorDirection {
    /// Also used for clockwise in circular/arc progress & rotating pointer/dial indicator
    LeftToRight = 1,
    /// Also used for counter-clockwise in circular/arc & rotating pointer/dial progress indicator
    RightToLeft = 2,
    TopToBottom = 3,
    BottomToTop = 4,
}

/// Custom DIY panel definition
#[derive(Debug, Serialize, Deserialize)]
pub struct Panel {
    /// Custom panel id
    pub id: Option<String>,
    /// Custom panel name
    pub name: Option<String>,
    /*
    // The following fields of the AOOSTAR-X json configuration file are NOT used in `asterctl`
    /// TODO
    pub checked: Option<bool>,
    /// TODO panel type: 5 = built-in? 6 = custom ?
    #[serde(rename = "type")]
    pub panel_type: i32,
     */
    /// Background image filename
    pub img: Option<String>,
    /// Sensors
    pub sensor: Vec<Sensor>,
}

impl Panel {
    pub fn friendly_name(&self) -> String {
        self.name
            .clone()
            .or_else(|| self.id.clone())
            .or_else(|| {
                if let Some(img_file) = &self.img {
                    let img_file = PathBuf::from(img_file);
                    img_file
                        .file_stem()
                        .map(|s| s.to_string_lossy().to_string())
                } else {
                    None
                }
            })
            .unwrap_or_else(|| "panel".into())
    }

    fn map_sensor_labels(&mut self, mapping: &HashMap<String, String>) {
        for sensor in self.sensor.iter_mut() {
            if let Some(new_label) = mapping.get(&sensor.label) {
                sensor.label = new_label.clone();
            }
        }
    }
}

/// One Data Display Unit
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sensor {
    /// Sensor mode: text, fan, progress, pointer
    pub mode: SensorMode,
    /// Sensor type, _not used_.
    /// - 1 Time / Date Labels
    /// - 2 Windows-specific system info
    /// - 3 Hardware value
    /// - 4 AIDA64 sensor
    /// - 5 HA sensor
    /// - 6 http fetch from url
    /// - 7 system info ?
    /// - 8 lm-sensor ?
    #[serde(rename = "type")]
    pub sensor_type: Option<i32>,
    /// Label name for internal panels.
    pub name: Option<String>,
    /// Label name for custom panels.
    pub item_name: Option<String>,
    /// Label identifier, also used as data source identifier.
    pub label: String,
    /// Sensor value. Ignored: value is used from a sensor source
    #[serde(deserialize_with = "empty_string_as_none")]
    pub value: Option<String>, // "" or numbers, so Option<String>

    /// Image for progress, fan and pointer indicators
    pub min_value: Option<f32>,
    /// Image for progress, fan and pointer indicators
    pub max_value: Option<f32>,

    /// Optional unit text to print after the value
    #[serde(deserialize_with = "empty_string_as_none")]
    pub unit: Option<String>,
    /// Rounded x-position. Custom panel coordinates are stored as float!
    #[serde(deserialize_with = "f32_as_rounded_i32")]
    pub x: i32,
    /// Rounded y-position. Custom panel coordinates are stored as float!
    #[serde(deserialize_with = "f32_as_rounded_i32")]
    pub y: i32,
    /// Used for pointer type
    pub width: Option<u32>,
    /// Used for pointer type
    pub height: Option<u32>,
    /// Sensor graphic orientation
    pub direction: Option<SensorDirection>,

    /// Font name matching font filename without file extension.
    pub font_family: Option<String>,
    /// TODO font size unit: points or pixels?
    pub font_size: Option<i32>,
    /// Font color in `#RRGGBB` notation, or -1 if not set. #ffffff = white, #ff0000 = red
    pub font_color: Option<FontColor>,
    /// _Not (yet) used_
    pub font_weight: Option<FontWeight>,
    pub text_align: Option<TextAlign>,

    /// Number of integer places for the sensor value.
    // -1 ≈ unset ⇒ Option<i32>
    #[serde(deserialize_with = "option_none_if_minus_one")]
    pub integer_digits: Option<i32>,
    /// Number of decimal places for the sensor value.
    // -1 ≈ unset ⇒ Option<i32>
    #[serde(deserialize_with = "option_none_if_minus_one")]
    pub decimal_digits: Option<i32>,
    /// Image for progress, fan and pointer indicators
    #[serde(deserialize_with = "empty_string_as_none")]
    pub pic: Option<String>,

    /// Used for fan & pointer sensors
    pub min_angle: Option<i32>,
    /// Used for fan & pointer sensors
    pub max_angle: Option<i32>,

    /// Pivot x
    #[serde(rename = "xz_x")]
    pub xz_x: Option<i32>,
    /// Pivot y
    #[serde(rename = "xz_y")]
    pub xz_y: Option<i32>,
    /*
    // The following fields of the AOOSTAR-X json configuration file are NOT used in `asterctl`
    /// _Not (yet) used_
    pub text_direction: i32, // layout direction
    /// For type = 6
    pub url: Option<String>,
    /// For type = 6
    pub data: Option<String>,
    /// For type = 6
    pub interval: Option<u32>,
     */
}

/// Sensor element type. Name is based on AOOSTAR-X web configuration
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum SensorMode {
    /// Text element
    Text = 1,
    /// Circular/arc progress indicator
    Fan = 2,
    /// Horizontal or vertical progress indicator
    Progress = 3,
    /// Rotating pointer/dial indicator
    Pointer = 4,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum TimeDateLabel {
    #[serde(rename = "DATE_year")]
    Year,
    #[serde(rename = "DATE_month")]
    Month,
    #[serde(rename = "DATE_day")]
    Day,
    #[serde(rename = "DATE_hour")]
    Hour,
    #[serde(rename = "DATE_minute")]
    Minute,
    #[serde(rename = "DATE_second")]
    Second,
    #[serde(rename = "DATE_m_d_h_m_1")]
    MDHM1,
    #[serde(rename = "DATE_m_d_h_m_2")]
    MDHM2,
    #[serde(rename = "DATE_m_d_1")]
    MD1,
    #[serde(rename = "DATE_m_d_2")]
    MD2,
    #[serde(rename = "DATE_y_m_d_1")]
    YMD1,
    #[serde(rename = "DATE_y_m_d_2")]
    YMD2,
    #[serde(rename = "DATE_y_m_d_3")]
    YMD3,
    #[serde(rename = "DATE_y_m_d_4")]
    YMD4,
    #[serde(rename = "DATE_h_m_s_1")]
    HMS1,
    #[serde(rename = "DATE_h_m_s_2")]
    HMS2,
    #[serde(rename = "DATE_h_m_s_3")]
    HMS3,
    #[serde(rename = "DATE_h_m_1")]
    HM1,
    #[serde(rename = "DATE_h_m_2")]
    HM2,
    #[serde(rename = "DATE_h_m_3")]
    HM3,
}

#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FontWeight {
    #[default]
    Normal,
    Bold,
}

#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TextAlign {
    #[default]
    Left,
    Center,
    Right,
}

fn option_none_if_minus_one<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
where
    D: Deserializer<'de>,
{
    match Option::<i32>::deserialize(deserializer)? {
        Some(-1) | None => Ok(None),
        Some(other) => Ok(Some(other)),
    }
}

/// Special font color type since it is represented either as numeric -1 or as a string :-(
///
/// A good serde programming exercise...
#[derive(Debug, Clone, Copy)]
pub struct FontColor(Rgb<u8>);

impl Default for FontColor {
    fn default() -> Self {
        FontColor(Rgb::white())
    }
}

impl Deref for FontColor {
    type Target = Rgb<u8>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl TryFrom<&str> for FontColor {
    type Error = ParseIntError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        if value.len() != 7 || !value.starts_with('#') {
            warn!("Invalid font color: {value}");
            Ok(FontColor::default())
        } else {
            Ok(FontColor(Rgb([
                u8::from_str_radix(&value[1..3], 16)?,
                u8::from_str_radix(&value[3..5], 16)?,
                u8::from_str_radix(&value[5..7], 16)?,
            ])))
        }
    }
}

impl From<Rgb<u8>> for FontColor {
    fn from(value: Rgb<u8>) -> Self {
        FontColor(value)
    }
}

impl From<FontColor> for Rgb<u8> {
    fn from(val: FontColor) -> Self {
        val.0
    }
}

impl From<FontColor> for Rgba<u8> {
    fn from(val: FontColor) -> Self {
        Rgba([val.0[0], val.0[1], val.0[2], 255])
    }
}

impl Serialize for FontColor {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        format!("#{:02x}{:02x}{:02x}", self.0[0], self.0[1], self.0[2]).serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for FontColor {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct MyVisitor;

        impl<'de> Visitor<'de> for MyVisitor {
            type Value = FontColor;

            fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt.write_str("integer or string")
            }

            fn visit_i64<E>(self, val: i64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match val {
                    -1 => Ok(FontColor::default()),
                    _ => Err(E::custom("invalid integer value, expected -1")),
                }
            }

            fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                if val.trim().is_empty() {
                    return Ok(FontColor::default());
                }
                match val.parse::<i32>() {
                    Ok(val) => self.visit_i32(val),
                    Err(_) => val
                        .try_into()
                        .map_err(|e| E::custom(format!("invalid font color value: {e}"))),
                }
            }
        }

        deserializer.deserialize_any(MyVisitor)
    }
}

fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    let option = Option::<String>::deserialize(deserializer)?;
    Ok(option.and_then(|s| if s.trim().is_empty() { None } else { Some(s) }))
}

fn f32_as_rounded_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
    D: Deserializer<'de>,
{
    let rounded = f32::deserialize(deserializer).map(f32::round)?;
    Ok(rounded as i32)
}
