use std::cell::RefCell;

use gtk::{gdk, gio, glib, gsk, prelude::*, subclass::prelude::*};
use serde::{Deserialize, de};

#[derive(Clone, Debug, Deserialize)]
pub struct IconJson {
    #[serde(deserialize_with = "icon_name")]
    pub name: String,
    pub tags: Vec<String>,
    pub context: String,
}

fn icon_name<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: de::Deserializer<'de>,
{
    let icon = String::deserialize(deserializer)?;
    Ok(format!("{icon}-symbolic"))
}
mod imp {
    use std::cell::{Cell, OnceCell};

    use super::*;

    #[derive(Debug, Default, glib::Properties)]
    #[properties(wrapper_type = super::Icon)]
    pub struct Icon {
        #[property(get, set, construct_only)]
        pub name: OnceCell<String>,
        #[property(get, set)]
        pub tags: RefCell<Vec<String>>,
        #[property(get, set, construct_only)]
        pub context: OnceCell<Option<String>>,
        #[property(get, set)]
        pub is_system: Cell<bool>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Icon {
        const NAME: &'static str = "Icon";
        type Type = super::Icon;
    }

    #[glib::derived_properties]
    impl ObjectImpl for Icon {}
}
glib::wrapper! {
    pub struct Icon(ObjectSubclass<imp::Icon>);
}

impl Icon {
    pub fn new(name: &str, tags: Vec<String>) -> Self {
        glib::Object::builder()
            .property("name", name)
            .property("tags", tags)
            .build()
    }

    pub fn should_display(&self, search_str: &str) -> bool {
        let icon_name = self.name().to_lowercase();
        let mut icon_terms = icon_name.split('-');
        // Check if the icon should be shown for the searched string
        let mut found_tags = self.tags();
        found_tags.retain(|tag| tag.to_lowercase().contains(search_str));

        icon_terms.any(|term| term.contains(search_str))
            || !found_tags.is_empty()
            || icon_name.contains(search_str)
    }

    pub async fn derive(&self) -> anyhow::Result<()> {
        let icon_file = self.file();
        // Push the icon into /tmp so other apps can access it for now. Not ideal :(
        let mut dest = std::env::temp_dir();
        dest.push(icon_file.basename().unwrap());
        let dest = gio::File::for_path(dest);
        self.save(&dest).await?;

        let uri = dest.uri();
        gio::AppInfo::launch_default_for_uri_future(&uri, gio::AppLaunchContext::NONE).await?;
        Ok(())
    }

    pub async fn save(&self, destination: &gio::File) -> anyhow::Result<()> {
        let icon_file = self.file();
        icon_file
            .copy_future(
                destination,
                gio::FileCopyFlags::OVERWRITE | gio::FileCopyFlags::TARGET_DEFAULT_PERMS,
                glib::Priority::default(),
            )
            .0
            .await?;
        Ok(())
    }

    pub fn file(&self) -> gio::File {
        self.paintable(-1).file().unwrap()
    }

    pub fn paintable(&self, size: i32) -> gtk::IconPaintable {
        let display = gdk::Display::default().unwrap();
        let theme = gtk::IconTheme::for_display(&display);
        theme.lookup_icon(
            &self.name(),
            &[],
            size,
            1,
            gtk::TextDirection::Ltr,
            gtk::IconLookupFlags::FORCE_SYMBOLIC,
        )
    }

    /// Returns a texture for use in GNOME Shell.
    pub fn texture(&self) -> Option<gdk::Texture> {
        // The shells wants them at 24px, but we draw them at 48 for hdpi
        // monitors.
        const SIZE: i32 = 48;
        let snapshot = gtk::Snapshot::new();
        let paintable = self.paintable(SIZE);

        // Colors displayed by gtk/adw in dark mode.
        let fg_color = "#ffffff"; // Light 1
        let error_color = "#ff7b63";
        let warning_color = "#f8e45c"; // Yellow 2
        let success_color = "#8ff0a4"; // Green 1

        let colors = [
            gdk::RGBA::parse(fg_color).unwrap(),
            gdk::RGBA::parse(error_color).unwrap(),
            gdk::RGBA::parse(warning_color).unwrap(),
            gdk::RGBA::parse(success_color).unwrap(),
        ];

        // The renderer was created up in the air so there is no scale factor
        // and the texture will be drawn at size `SIZE` and later stored as a
        // png of size `SIZE`.
        let renderer = gsk::GLRenderer::new();
        renderer.realize(gdk::Surface::NONE).ok()?;
        paintable.snapshot_symbolic(&snapshot, SIZE.into(), SIZE.into(), &colors);
        let node = snapshot.to_node()?;
        let texture = renderer.render_texture(&node, None);
        renderer.unrealize();

        Some(texture)
    }

    pub async fn copy(&self) -> anyhow::Result<()> {
        let display = gdk::Display::default().unwrap();
        let clipboard = display.clipboard();

        let file = self.file();
        let (bytes, _) = file.load_contents_future().await?;
        let gbytes = glib::Bytes::from_owned(bytes);
        let svg_content = gdk::ContentProvider::for_bytes("image/svg+xml", &gbytes);
        let text_content = gdk::ContentProvider::for_bytes("text/plain", &gbytes);
        let content_union = gdk::ContentProvider::new_union(&[svg_content, text_content]);

        clipboard.set_content(Some(&content_union))?;
        Ok(())
    }

    pub fn copy_name(&self) {
        if let Some(display) = gdk::Display::default() {
            let clipboard = display.clipboard();
            clipboard.set_text(&self.name());
        }
    }
}
