use serde_derive::{Deserialize, Serialize};

use udev::Device;
#[cfg(feature = "dbus")]
use zvariant_derive::Type;

use crate::{error::ProfileError, FanCurvePU};

pub(crate) fn pwm_str(fan: char, index: usize) -> String {
    let mut buf = "pwm1_auto_point1_pwm".to_string();
    unsafe {
        let tmp = buf.as_bytes_mut();
        tmp[3] = fan as u8;
        tmp[15] = char::from_digit(index as u32 + 1, 10).unwrap() as u8;
    }
    buf
}

pub(crate) fn temp_str(fan: char, index: usize) -> String {
    let mut buf = "pwm1_auto_point1_temp".to_string();
    unsafe {
        let tmp = buf.as_bytes_mut();
        tmp[3] = fan as u8;
        tmp[15] = char::from_digit(index as u32 + 1, 10).unwrap() as u8;
    }
    buf
}

#[cfg_attr(feature = "dbus", derive(Type))]
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
pub struct CurveData {
    pub fan: FanCurvePU,
    pub pwm: [u8; 8],
    pub temp: [u8; 8],
}

impl From<&CurveData> for String {
    fn from(c: &CurveData) -> Self {
        format!(
            "{:?}: {}c:{}%,{}c:{}%,{}c:{}%,{}c:{}%,{}c:{}%,{}c:{}%,{}c:{}%,{}c:{}%",
            c.fan,
            c.temp[0],
            (c.pwm[0] as u32) * 100 / 255,
            c.temp[1],
            (c.pwm[1] as u32) * 100 / 255,
            c.temp[2],
            (c.pwm[2] as u32) * 100 / 255,
            c.temp[3],
            (c.pwm[3] as u32) * 100 / 255,
            c.temp[4],
            (c.pwm[4] as u32) * 100 / 255,
            c.temp[5],
            (c.pwm[5] as u32) * 100 / 255,
            c.temp[6],
            (c.pwm[6] as u32) * 100 / 255,
            c.temp[7],
            (c.pwm[7] as u32) * 100 / 255,
        )
    }
}

impl std::str::FromStr for CurveData {
    type Err = ProfileError;

    /// Parse a string to the correct values that the fan curve kernel driver expects
    ///
    /// If the fan curve is given with percentage char '%' then the fan power values are converted
    /// otherwise the expected fan power range is 0-255.
    ///
    /// Temperature range is 0-255 in degrees C. You don't want to be setting over 100.
    fn from_str(input: &str) -> Result<Self, Self::Err> {
        let mut temp = [0u8; 8];
        let mut pwm = [0u8; 8];
        let mut temp_prev = 0;
        let mut pwm_prev = 0;
        let mut percentages = false;

        if input.split(',').count() < 8 {
            return Err(ProfileError::NotEnoughPoints);
        }

        for (index, value) in input.split(',').enumerate() {
            for (select, num) in value.splitn(2, |c| c == 'c' || c == ':').enumerate() {
                if num.contains('%') {
                    percentages = true;
                }
                let r = num.trim_matches(|c| c == 'c' || c == ':' || c == '%');
                let r = r.parse::<u8>().map_err(ProfileError::ParseFanCurveDigit)?;

                if select == 0 {
                    if temp_prev > r {
                        return Err(ProfileError::ParseFanCurvePrevHigher(
                            "temperature",
                            temp_prev,
                            r,
                        ));
                    }
                    temp_prev = r;
                    temp[index] = r;
                } else {
                    let mut p = r;
                    if percentages {
                        if r > 100 {
                            return Err(ProfileError::ParseFanCurvePercentOver100(r));
                        }
                        p = (p as f32 * 2.55).round() as u8;
                    }
                    if pwm_prev > p {
                        return Err(ProfileError::ParseFanCurvePrevHigher(
                            "percentage",
                            pwm_prev,
                            p,
                        ));
                    }
                    pwm_prev = p;
                    pwm[index] = p;
                }
            }
        }
        Ok(Self {
            fan: FanCurvePU::CPU,
            pwm,
            temp,
        })
    }
}

impl CurveData {
    pub fn set_fan(&mut self, fan: FanCurvePU) {
        self.fan = fan;
    }

    fn set_val_from_attr(tmp: &str, device: &Device, buf: &mut [u8; 8]) {
        if let Some(n) = tmp.chars().nth(15) {
            let i = n.to_digit(10).unwrap() as usize;
            let d = device.attribute_value(tmp).unwrap();
            let d: u8 = d.to_string_lossy().parse().unwrap();
            buf[i - 1] = d;
        }
    }

    fn read_from_device(&mut self, device: &Device) {
        for attr in device.attributes() {
            let tmp = attr.name().to_string_lossy();
            if tmp.starts_with("pwm1") && tmp.ends_with("_temp") {
                Self::set_val_from_attr(tmp.as_ref(), device, &mut self.temp)
            }
            if tmp.starts_with("pwm1") && tmp.ends_with("_pwm") {
                Self::set_val_from_attr(tmp.as_ref(), device, &mut self.pwm)
            }
        }
    }

    fn init_if_zeroed(&mut self, device: &mut Device) -> std::io::Result<()> {
        if self.pwm == [0u8; 8] && self.temp == [0u8; 8] {
            // Need to reset the device to defaults to get the proper profile defaults
            match self.fan {
                FanCurvePU::CPU => device.set_attribute_value("pwm1_enable", "3")?,
                FanCurvePU::GPU => device.set_attribute_value("pwm2_enable", "3")?,
            };
            self.read_from_device(device);
        }
        Ok(())
    }

    /// Write this curve to the device fan specified by `self.fan`
    fn write_to_device(&self, device: &mut Device, enable: bool) -> std::io::Result<()> {
        let pwm_num = match self.fan {
            FanCurvePU::CPU => '1',
            FanCurvePU::GPU => '2',
        };
        let enable = if enable { "1" } else { "2" };

        for (index, out) in self.pwm.iter().enumerate() {
            let pwm = pwm_str(pwm_num, index);
            device.set_attribute_value(&pwm, &out.to_string())?;
        }

        for (index, out) in self.temp.iter().enumerate() {
            let temp = temp_str(pwm_num, index);
            device.set_attribute_value(&temp, &out.to_string())?;
        }

        // Enable must be done *after* all points are written
        match self.fan {
            FanCurvePU::CPU => device.set_attribute_value("pwm1_enable", enable)?,
            FanCurvePU::GPU => device.set_attribute_value("pwm2_enable", enable)?,
        };

        Ok(())
    }
}

/// A `FanCurveSet` contains both CPU and GPU fan curve data
#[cfg_attr(feature = "dbus", derive(Type))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FanCurveSet {
    pub enabled: bool,
    pub cpu: CurveData,
    pub gpu: CurveData,
}

impl Default for FanCurveSet {
    fn default() -> Self {
        Self {
            enabled: false,
            cpu: CurveData {
                fan: FanCurvePU::CPU,
                pwm: [0u8; 8],
                temp: [0u8; 8],
            },
            gpu: CurveData {
                fan: FanCurvePU::GPU,
                pwm: [0u8; 8],
                temp: [0u8; 8],
            },
        }
    }
}

impl From<&FanCurveSet> for String {
    fn from(s: &FanCurveSet) -> Self {
        format!(
            "Enabled: {}, {}, {}",
            s.enabled,
            String::from(&s.cpu),
            String::from(&s.gpu),
        )
    }
}

impl FanCurveSet {
    pub(crate) fn read_cpu_from_device(&mut self, device: &Device) {
        self.cpu.read_from_device(device);
    }

    pub(crate) fn read_gpu_from_device(&mut self, device: &Device) {
        self.gpu.read_from_device(device);
    }

    pub(crate) fn write_cpu_fan(&mut self, device: &mut Device) -> std::io::Result<()> {
        self.cpu.init_if_zeroed(device)?;
        self.cpu.write_to_device(device, self.enabled)
    }

    pub(crate) fn write_gpu_fan(&mut self, device: &mut Device) -> std::io::Result<()> {
        self.gpu.init_if_zeroed(device)?;
        self.gpu.write_to_device(device, self.enabled)
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::*;

    #[test]
    fn curve_data_from_str_to_str() {
        let curve =
            CurveData::from_str("30c:1%,49c:2%,59c:3%,69c:4%,79c:31%,89c:49%,99c:56%,109c:58%")
                .unwrap();
        assert_eq!(curve.fan, FanCurvePU::CPU);
        assert_eq!(curve.temp, [30, 49, 59, 69, 79, 89, 99, 109]);
        assert_eq!(curve.pwm, [3, 5, 8, 10, 79, 125, 143, 148]);

        let string: String = (&curve).into();
        // End result is slightly different due to type conversions and rounding errors
        assert_eq!(
            string.as_str(),
            "CPU: 30c:1%,49c:1%,59c:3%,69c:3%,79c:30%,89c:49%,99c:56%,109c:58%"
        );

        let curve = CurveData::from_str("30c:1%,49c:2%,59c:3%,69c:4%,79c:31%,89c:49%,99c:56%");

        assert!(curve.is_err());
    }

    #[test]
    fn curve_data_from_str_simple() {
        let curve = CurveData::from_str("30:1,49:2,59:3,69:4,79:31,89:49,99:56,109:58").unwrap();
        assert_eq!(curve.fan, FanCurvePU::CPU);
        assert_eq!(curve.temp, [30, 49, 59, 69, 79, 89, 99, 109]);
        assert_eq!(curve.pwm, [1, 2, 3, 4, 31, 49, 56, 58]);
    }

    #[test]
    fn curve_data_from_str_invalid_pwm() {
        let curve =
            CurveData::from_str("30c:4%,49c:2%,59c:3%,69c:4%,79c:31%,89c:49%,99c:56%,109c:58%");
        assert!(&curve.is_err());
        assert!(matches!(
            curve,
            Err(ProfileError::ParseFanCurvePrevHigher(_, _, _))
        ));
    }

    #[test]
    fn check_pwm_str() {
        assert_eq!(pwm_str('1', 0), "pwm1_auto_point1_pwm");
        assert_eq!(pwm_str('1', 4), "pwm1_auto_point5_pwm");
        assert_eq!(pwm_str('1', 7), "pwm1_auto_point8_pwm");
    }

    #[test]
    fn check_temp_str() {
        assert_eq!(temp_str('1', 0), "pwm1_auto_point1_temp");
        assert_eq!(temp_str('1', 4), "pwm1_auto_point5_temp");
        assert_eq!(temp_str('1', 7), "pwm1_auto_point8_temp");
    }

    #[test]
    fn set_to_string() {
        let set = FanCurveSet::default();
        let string = String::from(&set);
        assert_eq!(string.as_str(), "Enabled: false, CPU: 0c:0%,0c:0%,0c:0%,0c:0%,0c:0%,0c:0%,0c:0%,0c:0%, GPU: 0c:0%,0c:0%,0c:0%,0c:0%,0c:0%,0c:0%,0c:0%,0c:0%");
    }
}
