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

use anyhow::{anyhow, bail, Result};
use ashpd::{desktop::open_uri::OpenFileRequest, WindowIdentifier};
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use rsvg::{CairoRenderer, Loader, SvgHandle};

use crate::icons::{clean_svg, icons_set, replace_fill_with_classes, Category, Icon, SYMBOLIC_CSS};

#[derive(Default, Clone, Debug, Copy, Eq, PartialEq, glib::Enum)]
#[enum_type(name = "ProjectType")]
pub enum ProjectType {
    #[default]
    Symbolic,
    IconsSet,
}

mod imp {
    use std::cell::{Cell, OnceCell, RefCell};

    use super::*;

    #[derive(glib::Properties, Default)]
    #[properties(wrapper_type = super::Project)]
    pub struct Project {
        #[property(get, set, construct_only)]
        pub cache_dir: OnceCell<PathBuf>,
        #[property(get, set, construct_only)]
        pub file: OnceCell<gio::File>, // File to monitor
        #[property(get, set, construct_only, builder(Default::default()))]
        pub project_type: Cell<ProjectType>,
        #[property(get, set, construct, nullable)]
        pub set: RefCell<Option<icons_set::IconsSet>>,
        pub handle: OnceCell<Arc<SvgHandle>>,
    }

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

    #[glib::derived_properties]
    impl ObjectImpl for Project {}
}

glib::wrapper! {
    pub struct Project(ObjectSubclass<imp::Project>);
}

impl Project {
    pub async fn parse(file: gio::File) -> Result<Self> {
        let parser = libxml::parser::Parser::default();
        let xml_input = async_std::fs::read_to_string(file.path().unwrap()).await?;
        let output = crate::icons::replace_classes_with_fill(&xml_input)?;
        let document = parser.parse_string(&output)?;
        let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(output.as_bytes()));
        let mut handle = Loader::new()
            .read_stream(&stream, gio::File::NONE, gio::Cancellable::NONE)
            .unwrap();
        handle.set_stylesheet(SYMBOLIC_CSS)?;

        let renderer = CairoRenderer::new(&handle);

        let (width, height) = {
            let dimensions = renderer.intrinsic_dimensions();
            (dimensions.width.length, dimensions.height.length)
        };

        // Create cache directory
        let project_name = file
            .basename()
            .unwrap_or_else(|| "default".into())
            .into_os_string();
        let cache_dir = [
            glib::user_cache_dir(),
            project_name
                .to_str()
                .unwrap()
                .trim_end_matches(".svg")
                .replace('.', "_")
                .into(),
            "icons/hicolor/symbolic/apps/".into(),
        ]
        .iter()
        .collect::<PathBuf>();
        if cache_dir.exists() {
            async_std::fs::remove_dir_all(&cache_dir).await?;
        }
        async_std::fs::create_dir_all(&cache_dir).await?;

        let handle_clone = std::sync::Arc::new(handle);
        // Try to see if it's a 16x16 rect, a symbolic icon
        if (width.round() - 16f64).abs() < std::f64::EPSILON
            && (height.round() - 16f64).abs() < std::f64::EPSILON
        {
            let dest = gio::File::for_path(
                [cache_dir.clone(), file.basename().unwrap()]
                    .iter()
                    .collect::<PathBuf>(),
            );
            file.copy_future(&dest, gio::FileCopyFlags::NONE, glib::Priority::default())
                .0
                .await?;
            let project = glib::Object::builder::<Self>()
                .property("project-type", ProjectType::Symbolic)
                .property("cache-dir", cache_dir)
                .property("file", file)
                .build();
            let _ = project.imp().handle.set(handle_clone);
            return Ok(project);
        }

        // If not, fallback to a set of icons
        if let Ok(set) =
            icons_set::IconsSet::parse(handle_clone.clone(), document, cache_dir.clone()).await
        {
            let project = glib::Object::builder::<Self>()
                .property("project-type", ProjectType::IconsSet)
                .property("cache-dir", cache_dir)
                .property("file", file)
                .property("set", set)
                .build();
            let _ = project.imp().handle.set(handle_clone);
            return Ok(project);
        }
        // remove the cache dir in case the file is broken
        async_std::fs::remove_dir_all(cache_dir).await?;
        bail!("Project type undetected for {:#?}", file.path());
    }

    pub fn icon_theme_dir(&self) -> PathBuf {
        let icons_dir = self.cache_dir();
        PathBuf::from(
            icons_dir
                .as_os_str()
                .to_str()
                .unwrap()
                .trim_end_matches("hicolor/symbolic/apps/"),
        )
    }

    pub fn path(&self) -> PathBuf {
        self.file().path().unwrap()
    }

    fn handle(&self) -> &SvgHandle {
        self.imp().handle.get().unwrap()
    }

    pub async fn create(path: PathBuf, project_type: ProjectType) -> Result<Self> {
        let dest = gio::File::for_path(&path);
        let template_name = match project_type {
            ProjectType::IconsSet => "icons-set.svg",
            ProjectType::Symbolic => "symbolic.svg",
        };

        let resource_uri =
            format!("resource:///org/gnome/design/SymbolicPreview/templates/{template_name}",);
        let from = gio::File::for_uri(&resource_uri);

        let parent = dest
            .parent()
            .ok_or_else(|| anyhow!("File doesn't have a parent?"))?;
        if !parent.query_exists(gio::Cancellable::NONE) {
            parent.make_directory_with_parents(gio::Cancellable::NONE)?;
        }

        from.copy_future(
            &dest,
            gio::FileCopyFlags::OVERWRITE,
            glib::Priority::DEFAULT_IDLE,
        )
        .0
        .await?;
        Self::parse(dest).await
    }

    pub async fn export(&self, dest: PathBuf) -> Result<()> {
        let project_name = self.name();
        tracing::info!("Exporting project {} into {:#?}", project_name, dest);
        match self.project_type() {
            ProjectType::IconsSet => {
                for item in self.set().unwrap().iter::<Category>() {
                    let item = item.unwrap();
                    let name = item.name();
                    let icons = item.icons();
                    let mut category_dest = dest.clone();
                    category_dest.push(name);
                    async_std::fs::create_dir_all(&category_dest).await?;
                    for icon in icons.iter::<Icon>() {
                        let icon = icon.unwrap();
                        let mut icon_dest = category_dest.clone();
                        // special case RTL icons
                        if icon.name().ends_with("-rtl") {
                            icon_dest.push(&format!(
                                "{}-symbolic-rtl.svg",
                                icon.name().trim_end_matches("-rtl")
                            ));
                        } else {
                            icon_dest.push(&format!("{}-symbolic.svg", icon.name()));
                        }
                        let renderer = CairoRenderer::new(self.handle());
                        icon.render(&renderer, Some(icon_dest.clone())).await?;
                        // Render the icon in that directory
                    }
                }
            }
            ProjectType::Symbolic => {
                let mut icon_dest = dest;
                async_std::fs::create_dir_all(&icon_dest).await?;
                // special case RTL icons
                if project_name.trim_end_matches("-symbolic").ends_with("-rtl")
                    || project_name.ends_with("-rtl")
                {
                    icon_dest.push(&format!(
                        "{}-symbolic-rtl.svg",
                        project_name.trim_end_matches("-symbolic")
                    ));
                } else {
                    icon_dest.push(&format!("{project_name}.svg"));
                }
                let dest_file = gio::File::for_path(icon_dest.as_path());
                let bytes = self.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)?)?;
                dest_file
                    .replace_contents_future(
                        xml_output,
                        None,
                        false,
                        gio::FileCreateFlags::REPLACE_DESTINATION,
                    )
                    .await
                    .map_err(|e| e.1)?;
            }
        };
        Ok(())
    }

    pub async fn open<W: glib::IsA<gtk::Window> + glib::IsA<gtk::Native>>(
        &self,
        parent: &W,
    ) -> anyhow::Result<()> {
        let uri = self.file().uri();
        tracing::info!("Opening file URI: {}", uri);
        let identifier = WindowIdentifier::from_native(parent).await;
        OpenFileRequest::default()
            .identifier(identifier)
            .ask(true)
            .writeable(true)
            .send_uri(&url::Url::parse(&uri).unwrap())
            .await?
            .response()?;
        Ok(())
    }

    pub fn name(&self) -> String {
        let project_name = self.file().basename().unwrap_or_else(|| "default".into());
        project_name
            .to_str()
            .unwrap()
            .trim_end_matches(".svg")
            .to_string()
    }
}
