use std::{path::PathBuf, sync::Arc};

use anyhow::Result;
use glib::subclass::prelude::*;
use gtk::{gdk, gio, glib, prelude::*};
use rsvg::{CairoRenderer, SvgHandle};

use super::{clean_svg, utils::replace_fill_with_classes};

mod imp {
    use std::cell::OnceCell;

    use super::*;

    #[derive(Default, glib::Properties)]
    #[properties(wrapper_type = super::Icon)]
    pub struct Icon {
        #[property(get, set, construct_only)]
        pub name: OnceCell<String>,
        #[property(get, set, construct_only)]
        pub id: OnceCell<String>,
        #[property(get, set, construct_only)]
        pub rect: OnceCell<Option<String>>,
        #[property(get, set, construct_only)]
        pub keywords: OnceCell<Vec<String>>,
        pub cache_file: OnceCell<PathBuf>,
        pub handle: OnceCell<Arc<SvgHandle>>,
    }

    #[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>);
}

unsafe impl Send for Icon {}
unsafe impl Sync for Icon {}

impl Icon {
    pub fn new(
        handle: Arc<SvgHandle>,
        name: String,
        id: String,
        rect: Option<String>,
        keywords: Vec<String>,
        cache_file: PathBuf,
    ) -> Self {
        let obj = glib::Object::builder::<Self>()
            .property("id", id)
            .property("name", name)
            .property("rect", rect)
            .property("keywords", keywords)
            .build();
        let imp = obj.imp();
        imp.cache_file.set(cache_file).unwrap();
        let _ = imp.handle.set(handle);
        obj
    }

    pub fn cache(&self) -> PathBuf {
        self.imp().cache_file.get().unwrap().to_owned()
    }

    pub async fn render(&self, renderer: &CairoRenderer<'_>, into: Option<PathBuf>) -> Result<()> {
        let imp = self.imp();
        let cache = match into {
            Some(path) => path,
            None => self.cache(),
        };
        let id = format!(
            "#{}",
            imp.rect
                .get()
                .unwrap()
                .as_ref()
                .unwrap_or(&imp.id.get().unwrap().to_string())
        );

        let viewport = {
            let dimensions = renderer.intrinsic_dimensions();

            cairo::Rectangle::new(0.0, 0.0, dimensions.width.length, dimensions.height.length)
        };
        let (rect, _) = renderer.geometry_for_layer(Some(&id), &viewport)?;

        let mut surface =
            cairo::SvgSurface::new(rect.width(), rect.height(), Some(cache.clone())).unwrap();
        surface.set_document_unit(cairo::SvgUnit::Px);
        let cr = cairo::Context::new(&surface)?;
        cr.translate(-rect.x(), -rect.y());

        renderer.render_layer(&cr, None, &viewport)?;
        surface.finish();
        let file = gio::File::for_path(cache);
        let bytes = file.load_contents_future().await?.0;
        let xml_input = String::from_utf8_lossy(bytes.as_ref()).to_string();
        if xml_input.is_empty() {
            return Ok(());
        }
        let xml_output = clean_svg(&replace_fill_with_classes(&xml_input)?)?;
        file.replace_contents_future(
            xml_output,
            None,
            false,
            gio::FileCreateFlags::REPLACE_DESTINATION,
        )
        .await
        .map_err(|e| e.1)?;
        Ok(())
    }

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

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

        let icon_file = gio::File::for_path(self.cache());
        let (bytes, _) = icon_file.load_bytes_future().await?;

        let content = gdk::ContentProvider::for_bytes("image/svg+xml", &bytes);
        clipboard.set_content(Some(&content))?;
        Ok(())
    }
}
