//! hinoirisetr library
//! Contains core logic for computing temperature and gamma and applying settings.

use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::Child;
use std::process::Command;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU16, Ordering};

use time::OffsetDateTime;

use rayon::prelude::*;

pub mod log;
pub mod notify;

static LAST_TEMP: AtomicU16 = AtomicU16::new(0);
static LAST_GAMMA: AtomicU16 = AtomicU16::new(0);
static GAMMASTEP: LazyLock<Arc<Mutex<Option<Child>>>> =
    LazyLock::new(|| Arc::new(Mutex::new(None)));

#[derive(Debug, Copy, Clone)]
pub struct Config {
    pub temp_day: u16,
    pub temp_night: u16,

    pub gamma_day: u16,
    pub gamma_night: u16,

    pub sunset_start: u8,
    pub sunset_start_minutes: u8,
    pub sunset_end: u8,
    pub sunset_end_minutes: u8,
    pub sunrise_start: u8,
    pub sunrise_start_minutes: u8,
    pub sunrise_end: u8,
    pub sunrise_end_minutes: u8,

    pub notification_timeout: u32,
    pub disable_timeout: u32,
    pub gamma_backend: GammaBackend,
    pub temp_backend: TempBackend,

    pub interpolation_temp: Interpolation,
    pub interpolation_gamma: Interpolation,
}

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Interpolation {
    Linear,
    CubicEaseInOut,
    Cosine,
    Exponential,
}

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum GammaBackend {
    Hyprctl,
    Ddcutil,
    Xsct,
    Redshift,
    Gammastep,
    None,
}

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum TempBackend {
    Hyprctl,
    Redshift,
    Xsct,
    Gammastep,
    None,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            temp_day: 6500,
            temp_night: 2500,
            gamma_day: 100,
            gamma_night: 95,
            sunset_start: 19,
            sunset_start_minutes: 0,
            sunset_end: 22,
            sunset_end_minutes: 0,
            sunrise_start: 4,
            sunrise_start_minutes: 0,
            sunrise_end: 7,
            sunrise_end_minutes: 0,
            disable_timeout: 300,
            notification_timeout: 5000,
            gamma_backend: GammaBackend::Hyprctl,
            temp_backend: TempBackend::Hyprctl,
            interpolation_temp: Interpolation::Linear,
            interpolation_gamma: Interpolation::Linear,
        }
    }
}

#[derive(Debug)]
pub enum ConfigError {
    IoError(std::io::Error),
    ParseIntError(std::num::ParseIntError),
    InvalidTemperature(String),
    InvalidTime(String),
    InvalidGamma(String),
    InvalidGammaBackend(String),
    InvalidTempBackend(String),
    InvalidInterpolation(String),
    DuplicateKey(String),
    UnavailableCommand(String),
}

impl From<std::io::Error> for ConfigError {
    fn from(err: std::io::Error) -> Self {
        ConfigError::IoError(err)
    }
}

impl From<std::num::ParseIntError> for ConfigError {
    fn from(err: std::num::ParseIntError) -> Self {
        ConfigError::ParseIntError(err)
    }
}

impl Config {
    pub fn load<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, ConfigError> {
        trace!("Config::load({path:?})");
        let mut config = Self::default(); // Start with default values

        let mut seen_keys = HashSet::new();
        let mut seen_time_keys = HashSet::new();

        let config_file = File::open(path)?;
        let reader = BufReader::new(config_file);
        let mut current_section = String::new();

        for line in reader
            .lines()
            .map_while(Result::ok)
            .map(|l| String::from(l.trim()))
            .filter(|l| !l.is_empty())
            .filter(|l| !l.starts_with('#'))
        {
            if line.starts_with('[') && line.contains(']') {
                current_section = line[1..line.find(']').unwrap()].to_string();
                seen_keys.clear();
            } else if let Some((key, value)) = line.split_once('=') {
                let key_trimmed_string = key.trim().replace('"', "");
                let key_trimmed = key_trimmed_string.as_str();

                // Check for duplicate key in current section
                if seen_keys.contains(key_trimmed) {
                    return Err(ConfigError::DuplicateKey(format!(
                        "Duplicate key '{key_trimmed}' in section '{current_section}'",
                    )));
                }
                seen_keys.insert(key_trimmed.to_string());

                let value = value.trim().replace('"', "");
                match current_section.as_str() {
                    "" => match key_trimmed {
                        "notification_timeout" => {
                            config.notification_timeout = value.parse::<u32>()?;
                        }
                        "disable_timeout" => {
                            config.disable_timeout = value.parse::<u32>()?;
                        }
                        "gamma_backend" => match value.to_lowercase().as_str() {
                            "hyprctl" => config.gamma_backend = GammaBackend::Hyprctl,
                            "ddcutil" => config.gamma_backend = GammaBackend::Ddcutil,
                            "gammastep" => config.gamma_backend = GammaBackend::Gammastep,
                            "xsct" => config.gamma_backend = GammaBackend::Xsct,
                            "redshift" => config.gamma_backend = GammaBackend::Redshift,
                            "none" => config.gamma_backend = GammaBackend::None,
                            _ => return Err(ConfigError::InvalidGammaBackend(value.to_string())),
                        },
                        "temp_backend" => match value.to_lowercase().as_str() {
                            "hyprctl" => config.temp_backend = TempBackend::Hyprctl,
                            "gammastep" => config.temp_backend = TempBackend::Gammastep,
                            "xsct" => config.temp_backend = TempBackend::Xsct,
                            "redshift" => config.temp_backend = TempBackend::Redshift,
                            "none" => config.temp_backend = TempBackend::None,
                            _ => return Err(ConfigError::InvalidTempBackend(value.to_string())),
                        },
                        _ => {}
                    },
                    "gamma" => match key_trimmed {
                        "day" => {
                            let parsed = value.parse::<u16>()?;
                            if 0 < parsed && parsed <= 100 {
                                config.gamma_day = parsed
                            } else {
                                return Err(ConfigError::InvalidGamma(value.to_string()));
                            }
                        }
                        "night" => {
                            let parsed = value.parse::<u16>()?;
                            if 0 < parsed && parsed <= 100 {
                                config.gamma_night = parsed
                            } else {
                                return Err(ConfigError::InvalidGamma(value.to_string()));
                            }
                        }
                        _ => {}
                    },
                    "temp" => match key_trimmed {
                        "day" => {
                            let parsed = value.parse::<u16>()?;
                            if (1000..=20000).contains(&parsed) {
                                config.temp_day = parsed
                            } else {
                                return Err(ConfigError::InvalidTemperature(value.to_string()));
                            }
                        }
                        "night" => {
                            let parsed = value.parse::<u16>()?;
                            if (1000..=20000).contains(&parsed) {
                                config.temp_night = parsed
                            } else {
                                return Err(ConfigError::InvalidTemperature(value.to_string()));
                            }
                        }
                        _ => {}
                    },
                    "time" => {
                        fn parse_time(value: &str, key: &str) -> Result<(u8, u8), ConfigError> {
                            let parts: Vec<&str> = value.split(':').collect();

                            match parts.len() {
                                1 => {
                                    warn!("missing minutes in {}, assuming 00", key);
                                    let hour = value.parse::<u8>()?;
                                    if (0..=23).contains(&hour) {
                                        Ok((hour, 0))
                                    } else {
                                        Err(ConfigError::InvalidTime(value.to_string()))
                                    }
                                }
                                2 => {
                                    let hour = parts[0].parse::<u8>()?;
                                    let minute = parts[1].parse::<u8>()?;

                                    if (0..=23).contains(&hour) && (0..=59).contains(&minute) {
                                        Ok((hour, minute))
                                    } else {
                                        Err(ConfigError::InvalidTime(value.to_string()))
                                    }
                                }
                                _ => Err(ConfigError::InvalidTime(value.to_string())),
                            }
                        }

                        seen_time_keys.insert(key_trimmed.to_string());
                        match key_trimmed {
                            "sunset_start" => {
                                (config.sunset_start, config.sunset_start_minutes) =
                                    parse_time(&value, "sunset_start")?
                            }
                            "sunset_end" => {
                                (config.sunset_end, config.sunset_end_minutes) =
                                    parse_time(&value, "sunset_end")?
                            }
                            "sunrise_start" => {
                                (config.sunrise_start, config.sunrise_start_minutes) =
                                    parse_time(&value, "sunrise_start")?
                            }
                            "sunrise_end" => {
                                (config.sunrise_end, config.sunrise_end_minutes) =
                                    parse_time(&value, "sunrise_end")?
                            }
                            "interpolation_temp" => match value.to_lowercase().as_str() {
                                "linear" => config.interpolation_temp = Interpolation::Linear,
                                "cubic" | "cubiceaseinout" => {
                                    config.interpolation_temp = Interpolation::CubicEaseInOut
                                }
                                "cosine" => config.interpolation_temp = Interpolation::Cosine,
                                "exponential" => {
                                    config.interpolation_temp = Interpolation::Exponential
                                }
                                _ => {
                                    return Err(ConfigError::InvalidInterpolation(format!(
                                        "invalid value for interpolation_temp: {value}. Valid values are: linear, cubic, cubiceaseinout, cosine, exponential"
                                    )));
                                }
                            },
                            "interpolation_gamma" => match value.to_lowercase().as_str() {
                                "linear" => config.interpolation_gamma = Interpolation::Linear,
                                "cubic" | "cubiceaseinout" => {
                                    config.interpolation_gamma = Interpolation::CubicEaseInOut
                                }
                                "cosine" => config.interpolation_gamma = Interpolation::Cosine,
                                "exponential" => {
                                    config.interpolation_gamma = Interpolation::Exponential
                                }
                                _ => {
                                    return Err(ConfigError::InvalidInterpolation(format!(
                                        "invalid value for interpolation_gamma: {value}. Valid values are: linear, cubic, cubiceaseinout, cosine, exponential"
                                    )));
                                }
                            },
                            _ => {}
                        }
                    }
                    _ => {}
                }
            }
        }

        if config.sunset_start >= config.sunset_end {
            return Err(ConfigError::InvalidTime(format!(
                "sunset_start ({0}) is greater than sunset_end ({1})",
                config.sunset_start, config.sunset_end
            )));
        }

        if config.sunrise_start >= config.sunrise_end {
            return Err(ConfigError::InvalidTime(format!(
                "sunrise_start ({0}) is greater than sunrise_end ({1})",
                config.sunrise_start, config.sunrise_end
            )));
        }

        if !seen_time_keys.contains("sunset_start")
            || !seen_time_keys.contains("sunset_end")
            || !seen_time_keys.contains("sunrise_start")
            || !seen_time_keys.contains("sunrise_end")
        {
            return Err(ConfigError::InvalidTime(
                "sunset_start, sunset_end, sunrise_start, and sunrise_end are all required"
                    .to_string(),
            ));
        }

        Ok(config)
    }
}

/// Linearly interpolate between start and end by factor [0.0, 1.0]
pub fn interpolate_linear(start: u16, end: u16, factor: f64) -> u16 {
    trace!("interpolate_linear({start}, {end}, {factor})");
    if end < start {
        (end as f64 + (start - end) as f64 * (1.0 - factor)).round() as u16
    } else {
        (start as f64 + (end - start) as f64 * factor).round() as u16
    }
}

/// Cubic interpolation
fn interpolate_cubic(start: u16, end: u16, factor: f64) -> u16 {
    trace!("interpolate_cubic({start}, {end}, {factor})");
    let factor = factor.clamp(0.0, 1.0);
    // Convert to f64 for arithmetic
    let start_f = start as f64;
    let end_f = end as f64;
    // Cubic ease-in-out: 3t^2 - 2t^3
    let t = factor;
    let smooth_t = (3.0 * t * t) - (2.0 * t * t * t);
    // Interpolate
    let result = start_f + smooth_t * (end_f - start_f);
    // Round and clamp to u16 bounds
    result.round().max(0.0).min(u16::MAX as f64) as u16
}

/// Cosine interpolation
fn interpolate_cosine(start: u16, end: u16, factor: f64) -> u16 {
    trace!("interpolate_cosine({start}, {end}, {factor})");
    let t = (1.0 - (factor.clamp(0.0, 1.0) * std::f64::consts::PI).cos()) / 2.0;
    interpolate_linear(start, end, t)
}

fn interpolate_exponential(start: u16, end: u16, factor: f64) -> u16 {
    trace!("interpolate_exponential({start}, {end}, {factor})");
    let t = factor.clamp(0.0, 1.0);
    let t = if t < 0.5 {
        0.5 * (2.0 * t).powf(3.0)
    } else {
        0.5 * (1.0 - (2.0 * (1.0 - t)).powf(3.0)) + 0.5
    };
    interpolate_linear(start, end, t)
}

/// Interpolation meta-function
fn interpolate_value(start: u16, end: u16, factor: f64, interpolation: &Interpolation) -> u16 {
    trace!("interpolate_value({start}, {end}, {factor}, {interpolation:?})");
    match interpolation {
        Interpolation::Linear => interpolate_linear(start, end, factor),
        Interpolation::CubicEaseInOut => interpolate_cubic(start, end, factor),
        Interpolation::Cosine => interpolate_cosine(start, end, factor),
        Interpolation::Exponential => interpolate_exponential(start, end, factor),
    }
}

/// Compute current temperature and gamma based on provided time
pub fn compute_settings(now: OffsetDateTime, config: &Config) -> (u16, u16) {
    trace!("compute_settings({now:?})");

    // extract just the time-of-day
    let tod = now.time();
    let time_in_hours = tod.hour() as f64 + tod.minute() as f64 / 60.0;
    trace!("time_in_hours: {time_in_hours}");

    let sunset_start = config.sunset_start as f64 + config.sunset_start_minutes as f64 / 60.0;
    let sunset_end = config.sunset_end as f64 + config.sunset_end_minutes as f64 / 60.0;
    let sunrise_start = config.sunrise_start as f64 + config.sunrise_start_minutes as f64 / 60.0;
    let sunrise_end = config.sunrise_end as f64 + config.sunrise_end_minutes as f64 / 60.0;

    trace!("sunset_start: {sunset_start}, sunset_end: {sunset_end}");
    trace!("sunrise_start: {sunrise_start}, sunrise_end: {sunrise_end}");

    if (time_in_hours >= sunset_start) && (time_in_hours <= sunset_end) {
        trace!("time_in_hours is within sunset");
        let factor = ((time_in_hours - sunset_start) / (sunset_end - sunset_start)).clamp(0.0, 1.0);

        (
            interpolate_value(
                config.temp_day,
                config.temp_night,
                factor,
                &config.interpolation_temp,
            ),
            interpolate_value(
                config.gamma_day,
                config.gamma_night,
                factor,
                &config.interpolation_gamma,
            ),
        )
    } else if (time_in_hours >= sunrise_start) && (time_in_hours <= sunrise_end) {
        trace!("time_in_hours is within sunrise");
        let factor =
            1.0 - ((time_in_hours - sunrise_start) / (sunrise_end - sunrise_start)).clamp(0.0, 1.0);

        (
            interpolate_value(
                config.temp_day,
                config.temp_night,
                factor,
                &config.interpolation_temp,
            ),
            interpolate_value(
                config.gamma_day,
                config.gamma_night,
                factor,
                &config.interpolation_gamma,
            ),
        )
    } else if time_in_hours > sunset_end || time_in_hours < sunrise_start {
        trace!("time_in_hours is within night");
        (config.temp_night, config.gamma_night)
    } else {
        trace!("time_in_hours is within day");
        (config.temp_day, config.gamma_day)
    }
}

/// Apply given temperature (Kelvin) and gamma (%) via hyprctl commands
pub fn apply_temp(temp: u16, config: &Config) {
    trace!("apply_temp({temp})");
    let last_temp = LAST_TEMP.load(Ordering::SeqCst);
    if last_temp == temp {
        trace!("Settings unchanged, skipping application");
        return;
    }
    debug!("applying temperature: {temp}");

    if temp != last_temp {
        match config.temp_backend {
            TempBackend::Hyprctl => {
                let _ = Command::new("hyprctl")
                    .args(["hyprsunset", "temperature", &temp.to_string()])
                    .output();
                trace!("hyprctl hyprsunset temperature {temp}");
            }
            TempBackend::Redshift => {
                let _ = Command::new("redshift")
                    .args(["-o", "-t", &temp.to_string()])
                    .output();
                trace!("redshift -o -t {temp}");
            }
            TempBackend::Xsct => {
                let _ = Command::new("xsct").args([&temp.to_string()]).output();
                trace!("xsct {temp}");
            }
            TempBackend::None => {}
            TempBackend::Gammastep => {
                let mut guard = GAMMASTEP.lock().unwrap();
                if let Some(mut child) = guard.take() {
                    let _ = child.kill();
                    let _ = child.wait();
                }
                let child = match Command::new("gammastep")
                    .args(["-O", &temp.to_string()])
                    .spawn()
                {
                    Ok(c) => Some(c),
                    Err(e) => {
                        error!("{}", e);
                        None
                    }
                };
                trace!("gammastep -O {temp}");

                *guard = child;
            }
        }
        LAST_TEMP.store(temp, Ordering::SeqCst);
    }
}

pub fn apply_gamma(gamma: u16, config: &Config) {
    trace!("apply_gamma({gamma})");
    let last_gamma = LAST_GAMMA.load(Ordering::SeqCst);
    if last_gamma == gamma {
        trace!("Settings unchanged, skipping application");
        return;
    }
    debug!("applying gamma: {gamma}");

    if gamma != last_gamma {
        match config.gamma_backend {
            GammaBackend::Hyprctl => {
                let _ = Command::new("hyprctl")
                    .args(["hyprsunset", "gamma", &gamma.to_string()])
                    .output();
                trace!("hyprctl hyprsunset gamma {gamma}");
            }
            GammaBackend::Ddcutil => {
                let display_count = match Command::new("ddcutil").args(["detect"]).output() {
                    Ok(output) => String::from_utf8_lossy(&output.stdout)
                        .trim()
                        .to_string()
                        .lines()
                        .filter(|l| l.contains("Display"))
                        .count(),
                    Err(e) => {
                        error!("Failed to detect the number of displays with error: {e}");
                        1
                    }
                };

                (1..=display_count).into_par_iter().for_each(|display| {
                    let _ = Command::new("ddcutil")
                        .args([
                            "-d",
                            &display.to_string(),
                            "setvcp",
                            "10",
                            &gamma.to_string(),
                        ])
                        .output();
                    trace!("ddcutil -d {display} setvcp 10 {gamma}");
                });
            }
            GammaBackend::Gammastep => {
                let _ = Command::new("gammastep")
                    .args(["-O", "6500", "-g", &(&gamma / 100).to_string()])
                    .output();
                trace!("gammastep -O 6500 -g {gamma}");
            }
            GammaBackend::Xsct => {
                let output = Command::new("xsct").output().expect("xsct failed");
                let stdout = String::from_utf8_lossy(&output.stdout);
                let mut ttemp = 6000;
                for line in stdout.lines() {
                    if line.contains("temperature") {
                        // example: "Screen 0: temperature ~ 6000 0.598234"
                        let parts: Vec<&str> = line.split_whitespace().collect();
                        if parts.len() >= 6 {
                            ttemp = parts[4].parse::<u16>().unwrap();
                        }
                    }
                }

                let _ = Command::new("xsct")
                    .args([&ttemp.to_string(), &(&gamma / 100).to_string()])
                    .output();
                trace!("xsct {ttemp} {gamma}");
            }
            GammaBackend::Redshift => {
                let _ = Command::new("redshift")
                    .args(["-O", "6500", "-g", &(&gamma / 100).to_string()])
                    .output();
                trace!("redshift -O 6500 -g {gamma}");
            }
            GammaBackend::None => {}
        }
        LAST_GAMMA.store(gamma, Ordering::SeqCst);
    }
}

pub fn reset_cache() {
    LAST_TEMP.store(0, Ordering::SeqCst);
    LAST_GAMMA.store(0, Ordering::SeqCst);
}
