use crate::Theme;
use configparser::ini::Ini;
use palette::blend::Compose;
use palette::rgb::Rgba;
use palette::{Mix, Srgba, WithAlpha};
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::vec;

use super::{OutputError, qt_settings_ini_style};

impl Theme {
    /// The "version" of this theme.
    ///
    /// To avoid repeatedly overwriting the user's config, we use a version system.
    ///
    /// Increment this value when changes to qt{5,6}ct.conf are needed.
    /// If the config's version is outdated, we update several sections.
    /// Otherwise, only the light/dark mode is updated.
    const COSMIC_QT_VERSION: u64 = 2;

    /// Produces a QPalette ini file for qt5ct and qt6ct.
    ///
    /// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf
    #[must_use]
    #[cold]
    pub fn as_qpalette(&self) -> String {
        let lightest = if self.is_dark {
            self.background.on
        } else {
            self.background.base
        };
        let darkest = if self.is_dark {
            self.background.base
        } else {
            self.background.on
        };
        let active = QPaletteGroup {
            window_text: self.background.on,
            button: self.button.base,
            light: self.button.base.mix(lightest, 0.1),
            midlight: self.button.base.mix(lightest, 0.05),
            dark: self.button.base.mix(darkest, 0.1),
            mid: self.button.base.mix(darkest, 0.05),
            text: self.background.component.on,
            bright_text: lightest,
            button_text: self.button.on,
            base: self.background.component.base,
            window: self.background.base,
            shadow: darkest,
            // selection colors are swapped to fix menu bar contrast
            highlight: self.background.component.selected_text,
            highlighted_text: self.background.component.selected,
            link: self.link_button.on,
            link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2),
            alternate_base: self.background.base.mix(self.accent.base, 0.05),
            no_role: self.background.component.disabled,
            tool_tip_base: self.background.component.base,
            tool_tip_text: self.background.component.on,
            placeholder_text: self.background.component.on.with_alpha(0.5),
        };
        let inactive = QPaletteGroup {
            window_text: active.window_text.with_alpha(0.8),
            text: active.text.with_alpha(0.8),
            highlighted_text: active.highlighted_text.with_alpha(0.8),
            tool_tip_text: active.tool_tip_text.with_alpha(0.8),
            ..active
        };
        let disabled = QPaletteGroup {
            button: self.button.disabled,
            text: self.background.component.on_disabled,
            button_text: self.button.on_disabled,
            base: self.background.component.disabled,
            highlighted_text: active.highlighted_text.with_alpha(0.5),
            link: self.link_button.on_disabled,
            link_visited: self
                .link_button
                .on_disabled
                .mix(self.secondary.component.disabled, 0.2),
            alternate_base: self.background.base.mix(self.accent.disabled, 0.05),
            tool_tip_base: self.background.component.disabled,
            tool_tip_text: self.background.component.on_disabled,
            placeholder_text: self.background.component.on_disabled.with_alpha(0.5),
            ..inactive
        };

        format!(
            r#"# GENERATED BY COSMIC

[ColorScheme]
active_colors={}
disabled_colors={}
inactive_colors={}
"#,
            active.as_list(),
            disabled.as_list(),
            inactive.as_list(),
        )
    }

    /// Writes the QPalette ini files to:
    /// - `~/.config/qt6ct/colors/`
    /// - `~/.config/qt5ct/colors/`
    #[cold]
    pub fn write_qt56ct(&self) -> Result<(), OutputError> {
        let qpalette = self.as_qpalette();
        let qt5ct_res = self.write_ct("qt5ct", &qpalette);
        let qt6ct_res = self.write_ct("qt6ct", &qpalette);
        qt5ct_res?;
        qt6ct_res?;
        Ok(())
    }
    #[must_use]
    #[cold]
    fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> {
        let file_path = Self::get_qpalette_path(ct, self.is_dark)?;
        let tmp_file_path = file_path.with_extension("conf.new");

        let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?;
        let res = tmp_file
            .write_all(qpalette.as_bytes())
            .and_then(|_| tmp_file.flush())
            .and_then(|_| std::fs::rename(&tmp_file_path, file_path));
        if let Err(e) = res {
            _ = std::fs::remove_file(&tmp_file_path);
            return Err(OutputError::Io(e));
        }

        Ok(())
    }

    /// Edits qt{5,6}ct.conf to use COSMIC styles if needed.
    #[cold]
    pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> {
        let qt5ct_res = Self::apply_ct("qt5ct", is_dark);
        let qt6ct_res = Self::apply_ct("qt6ct", is_dark);
        qt5ct_res?;
        qt6ct_res?;
        Ok(())
    }
    #[must_use]
    #[cold]
    fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> {
        let path = Self::get_conf_path(ct)?;
        let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
        let mut ini = Ini::new_cs();
        ini.read(file_content).map_err(OutputError::Ini)?;

        let old_version = ini
            .getuint("Appearance", "cosmic_qt_version")
            .map_err(OutputError::Ini)?
            .unwrap_or_default();

        let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?;
        let icon_theme = if is_dark { "breeze-dark" } else { "breeze" };

        ini.set(
            "Appearance",
            "cosmic_qt_version",
            Some(Theme::COSMIC_QT_VERSION.to_string()),
        );

        if old_version < Theme::COSMIC_QT_VERSION {
            // Config is outdated, update it unconditionally!

            ini.setstr(
                "Appearance",
                "color_scheme_path",
                color_scheme_path.to_str(),
            );
            // Enable the above color scheme, instead of using the default color scheme of e.g. Breeze
            ini.setstr("Appearance", "custom_palette", Some("true"));
            // COSMIC icons are stuck in light mode, so use breeze icons instead
            ini.setstr("Appearance", "icon_theme", Some(icon_theme));
            // Use COSMIC dialogs instead of KDE's
            ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal"));

            // TODO: Add fonts section to match COSMIC
        } else {
            // Config is not outdated, check before updating light/dark mode only!

            let old_color_scheme_path = ini
                .get("Appearance", "color_scheme_path")
                .unwrap_or_else(|| "CosmicPlease".to_owned());
            if old_color_scheme_path.contains("Cosmic") {
                ini.setstr(
                    "Appearance",
                    "color_scheme_path",
                    color_scheme_path.to_str(),
                );
            }

            let old_icon_theme = ini
                .get("Appearance", "icon_theme")
                .unwrap_or_else(|| "breeze".to_owned());
            if old_icon_theme.contains("breeze") {
                ini.setstr("Appearance", "icon_theme", Some(icon_theme));
            }
        }

        ini.pretty_write(path, &qt_settings_ini_style())
            .map_err(OutputError::Io)?;
        Ok(())
    }

    /// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file.
    #[cold]
    pub fn reset_qt56ct() -> Result<(), OutputError> {
        let qt5ct_res = Self::reset_ct("qt5ct");
        let qt6ct_res = Self::reset_ct("qt6ct");
        qt5ct_res?;
        qt6ct_res?;
        Ok(())
    }
    #[must_use]
    #[cold]
    fn reset_ct(ct: &str) -> Result<(), OutputError> {
        let path = Self::get_conf_path(ct)?;
        let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?;
        let mut ini = Ini::new_cs();
        ini.read(file_content).map_err(OutputError::Ini)?;

        let old_version = ini
            .getuint("Appearance", "cosmic_qt_version")
            .map_err(OutputError::Ini)?
            .unwrap_or_default();
        if old_version == 0 {
            return Ok(());
        }

        ini.remove_key("Appearance", "cosmic_qt_version");
        ini.remove_key("Appearance", "color_scheme_path");
        ini.remove_key("Appearance", "icon_theme");

        ini.pretty_write(path, &qt_settings_ini_style())
            .map_err(OutputError::Io)?;
        Ok(())
    }

    /// Returns the file paths of the form `~/.config/ct/ct.conf`:
    /// e.g. `~/.config/qt6ct/qt6ct.conf`.
    ///
    /// The file and its parent directory are created if they don't exist.
    #[cold]
    fn get_conf_path(ct: &str) -> Result<PathBuf, OutputError> {
        assert!(ct == "qt5ct" || ct == "qt6ct");

        let Some(mut config_dir) = dirs::config_dir() else {
            return Err(OutputError::MissingConfigDir);
        };
        config_dir.push(&ct);
        if !config_dir.exists() {
            fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
        }

        let file_path = config_dir.join(ct.to_owned() + ".conf");
        if !file_path.exists() {
            File::create_new(&file_path).map_err(OutputError::Io)?;
        }

        Ok(file_path)
    }

    /// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf`
    ///
    /// Its parent directory is created if it doesn't exist.
    #[cold]
    fn get_qpalette_path(ct: &str, is_dark: bool) -> Result<PathBuf, OutputError> {
        assert!(ct == "qt5ct" || ct == "qt6ct");

        let Some(mut config_dir) = dirs::config_dir() else {
            return Err(OutputError::MissingConfigDir);
        };
        config_dir.push(&ct);
        config_dir.push("colors");
        if !config_dir.exists() {
            fs::create_dir_all(&config_dir).map_err(OutputError::Io)?;
        }

        let file_name = if is_dark {
            "CosmicDark.conf"
        } else {
            "CosmicLight.conf"
        };

        Ok(config_dir.join(file_name))
    }
}

/// Defines the different symbolic color roles used in current GUIs.
///
/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole:
/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum
/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum
struct QPaletteGroup {
    /// A general foreground color.
    window_text: Srgba,
    /// The general button background color.
    button: Srgba,
    /// Lighter than [button] color, used mostly for 3D bevel and shadow effects.
    light: Srgba,
    /// Between [button] and [light], used mostly for 3D bevel and shadow effects.
    midlight: Srgba,
    /// Darker than [button], used mostly for 3D bevel and shadow effects.
    dark: Srgba,
    /// Between [button] and [dark], used mostly for 3D bevel and shadow effects.
    mid: Srgba,
    /// The foreground color used with [base].
    text: Srgba,
    /// A text color that is very different from [window_text], and contrasts well with e.g. [dark].
    /// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons.
    bright_text: Srgba,
    /// A foreground color used with the [button] color.
    button_text: Srgba,
    /// Used mostly as the background color for text entry widgets, but can also be used for other painting -
    /// such as the background of combobox drop down lists and toolbar handles.
    base: Srgba,
    /// A general background color.
    window: Srgba,
    /// A very dark color, used mostly for 3D bevel and shadow effects.
    /// Opaque black by default.
    shadow: Srgba,
    /// A color to indicate a selected item or the current item.
    highlight: Srgba,
    /// A text color that contrasts with [highlight].
    highlighted_text: Srgba,
    /// A text color used for unvisited hyperlinks.
    link: Srgba,
    /// A text color used for already visited hyperlinks.
    link_visited: Srgba,
    /// Used as the alternate background color in views with alternating row colors.
    alternate_base: Srgba,
    /// No role; this special role is often used to indicate that a role has not been assigned.
    no_role: Srgba,
    /// Used as the background color for QToolTip and QWhatsThis.
    /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows.
    tool_tip_base: Srgba,
    /// Used as the foreground color for QToolTip and QWhatsThis.
    /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows.
    tool_tip_text: Srgba,
    /// Used as the placeholder color for various text input widgets.
    placeholder_text: Srgba,
    // /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct.
    // /// When omitted, it defaults to [highlight].
    // accent: Srgba,
}

impl QPaletteGroup {
    /// Returns a comma-separated list of the colors as hex codes.
    /// E.g. `#ff000000, #ffdcdcdc, ...`
    ///
    /// Any transparent colors are flattened with [base] to avoid issues with
    /// the Fusion style.
    fn as_list(&self) -> String {
        let colors = vec![
            to_argb_hex(self.window_text.over(self.base)),
            to_argb_hex(self.button.over(self.base)),
            to_argb_hex(self.light.over(self.base)),
            to_argb_hex(self.midlight.over(self.base)),
            to_argb_hex(self.dark.over(self.base)),
            to_argb_hex(self.mid.over(self.base)),
            to_argb_hex(self.text.over(self.base)),
            to_argb_hex(self.bright_text.over(self.base)),
            to_argb_hex(self.button_text.over(self.base)),
            to_argb_hex(self.base.over(self.base)),
            to_argb_hex(self.window.over(self.base)),
            to_argb_hex(self.shadow.over(self.base)),
            to_argb_hex(self.highlight.over(self.base)),
            to_argb_hex(self.highlighted_text.over(self.base)),
            to_argb_hex(self.link.over(self.base)),
            to_argb_hex(self.link_visited.over(self.base)),
            to_argb_hex(self.alternate_base.over(self.base)),
            to_argb_hex(self.no_role.over(self.base)),
            to_argb_hex(self.tool_tip_base.over(self.base)),
            to_argb_hex(self.tool_tip_text.over(self.base)),
            to_argb_hex(self.placeholder_text.over(self.base)),
        ];
        colors.join(", ")
    }
}

/// Converts a color to a hex string in the format `#AARRGGBB`.
/// Do not use [to_hex] since that uses the format `RRGGBBAA`.
fn to_argb_hex(c: Srgba) -> String {
    let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
    format!(
        "#{:02x}{:02x}{:02x}{:02x}",
        c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_color_to_argb_hex() {
        let color = Srgba::new(0x33, 0x55, 0x77, 0xff);
        let argb = to_argb_hex(color.into());
        assert_eq!(argb, "#ff335577");
    }

    #[test]
    fn test_light_default_qpalette() {
        let light_default_qpalette = Theme::light_default().as_qpalette();
        insta::assert_snapshot!(light_default_qpalette);
    }

    #[test]
    fn test_dark_default_qpalette() {
        let dark_default_qpalette = Theme::dark_default().as_qpalette();
        insta::assert_snapshot!(dark_default_qpalette);
    }
}
