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

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

use super::{icon::Icon, Category};

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

    use glib::{once_cell::sync::Lazy, subclass::Signal};

    use super::*;

    #[derive(Default)]
    pub struct IconsSet(
        pub(super) RefCell<Vec<Category>>,
        pub(super) RefCell<VecDeque<Category>>,
        pub(super) Cell<u32>,
        pub(super) Cell<u32>,
    );

    #[glib::object_subclass]
    impl ObjectSubclass for IconsSet {
        const NAME: &'static str = "IconsSet";
        type Type = super::IconsSet;
        type Interfaces = (gio::ListModel,);
    }

    impl ObjectImpl for IconsSet {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> =
                Lazy::new(|| vec![Signal::builder("category-parsed").build()]);
            SIGNALS.as_ref()
        }
    }

    impl ListModelImpl for IconsSet {
        fn item_type(&self) -> glib::Type {
            Category::static_type()
        }

        fn n_items(&self) -> u32 {
            self.0.borrow().len() as _
        }

        fn item(&self, position: u32) -> Option<glib::Object> {
            self.0
                .borrow()
                .get(position as usize)
                .map(|o| o.clone().upcast())
        }
    }
}

glib::wrapper! {
    pub struct IconsSet(ObjectSubclass<imp::IconsSet>)
        @implements gio::ListModel;
}

impl IconsSet {
    pub async fn parse(
        handle: Arc<SvgHandle>,
        doc: libxml::tree::Document,
        cache_dir: PathBuf,
    ) -> Result<Self> {
        let set = glib::Object::new::<Self>();
        set.find_categories(handle, doc, cache_dir)?;
        glib::MainContext::default().spawn_local(clone!(@strong set => async move {
            let queue = &mut set.imp().1.borrow_mut();
            while let Some(category) = queue.pop_front() {
                category.queue_rendering().await;
                set.imp().2.set(set.imp().2.get() + 1);
                set.append(&category);
                set.emit_by_name::<()>("category-parsed", &[]);
            }
        }));
        Ok(set)
    }

    pub fn n_parsed_categories(&self) -> u32 {
        self.imp().2.get()
    }

    pub fn total_categories(&self) -> u32 {
        self.imp().3.get()
    }

    pub fn connect_category_parsed<F: Fn(&Self) + 'static>(
        &self,
        callback: F,
    ) -> glib::SignalHandlerId {
        self.connect_closure(
            "category-parsed",
            false,
            glib::closure_local!(move |this: Self| {
                callback(&this);
            }),
        )
    }

    fn append(&self, category: &Category) {
        let pos = self.n_items();
        {
            self.imp().0.borrow_mut().push(category.clone());
        };
        self.items_changed(pos, 0, 1);
    }

    fn find_categories(
        &self,
        handle: Arc<SvgHandle>,
        doc: libxml::tree::Document,
        cache_dir: PathBuf,
    ) -> Result<()> {
        let root = doc
            .get_root_element()
            .ok_or_else(|| anyhow::anyhow!("Document has no root node"))?;

        let elements = root
            .findnodes("//*[@id]")
            .map_err(|_| anyhow::anyhow!("Failed to find elements with id"))?;
        let mut categories = Vec::new();
        for elem in elements.into_iter() {
            if elem.get_attribute("groupmode").is_some() {
                let category_label = elem.get_attribute("label").unwrap_or_default();
                if !category_label.starts_with("noexport-") {
                    let category = Category::new(&category_label);
                    self.find_icons(
                        handle.clone(),
                        &category,
                        doc.clone(),
                        elem,
                        cache_dir.clone(),
                    )?;
                    categories.push(category);
                } else {
                    tracing::warn!("Ignoring category: {}", category_label);
                }
            }
        }
        categories.sort_by_key(|a| a.name());
        self.imp().3.set(categories.len() as u32);
        self.imp().1.borrow_mut().extend(categories);
        Ok(())
    }

    fn find_icons(
        &self,
        handle: Arc<SvgHandle>,
        category: &Category,
        _doc: libxml::tree::Document,
        root: libxml::tree::Node,
        cache_dir: PathBuf,
    ) -> Result<()> {
        let mut icons = Vec::new();
        for elem in root.get_child_nodes() {
            if let Some(label_attribute) = elem.get_attribute("label") {
                let mut icon_name = String::new();
                let mut keywords = Vec::new();

                // The case of icon devkit
                if let Some(title_node) = elem
                    .get_child_nodes()
                    .iter()
                    .find(|s| s.get_name() == "title")
                {
                    icon_name = title_node.get_content();
                    keywords = label_attribute
                        .split(' ')
                        .map(|e| e.to_string())
                        .filter(|keyword| !keyword.is_empty())
                        .collect::<Vec<String>>();
                }

                // The case of Adwaita for now
                if icon_name.is_empty() {
                    icon_name = label_attribute;
                }
                let rect = find_rect(&elem);
                let _id = elem.get_attribute("id").unwrap_or_default();
                if !validate_item(&icon_name, &_id) {
                    continue;
                }
                let mut cache_file = cache_dir.clone();
                cache_file.push(create_file_name(&icon_name));
                let icon = Icon::new(handle.clone(), icon_name, _id, rect, keywords, cache_file);
                icons.push(icon);
            }
        }
        category.splice(icons);
        Ok(())
    }
}

fn find_rect(root: &libxml::tree::Node) -> Option<String> {
    if let Some(rect) = root.get_child_nodes().iter().find(|node| {
        let width = node
            .get_attribute("width")
            .and_then(|w| w.parse::<f64>().ok())
            .unwrap_or_default()
            .round();
        let height = node
            .get_attribute("height")
            .and_then(|w| w.parse::<f64>().ok())
            .unwrap_or_default()
            .round();

        node.get_name() == "rect"
            && (width - 16f64).abs() < EPSILON
            && (height - 16f64).abs() < EPSILON
    }) {
        let rect_id = rect.get_attribute("id").unwrap_or_default();
        return Some(rect_id);
    }
    None
}

impl Default for IconsSet {
    fn default() -> Self {
        glib::Object::new()
    }
}

/// We need to treat rtl icons differently
fn create_file_name(name: &str) -> String {
    if name.ends_with("rtl") {
        format!("{}-rtl-symbolic.svg", name.trim_end_matches("-rtl"))
    } else {
        format!("{}-symbolic.svg", name.trim_end_matches('.'))
    }
}

fn validate_item(name: &str, id: &str) -> bool {
    if name.is_empty() || id.is_empty() {
        tracing::warn!(
            "FIXME: Icon {} with id {} ignored as some information missing, icon ignored",
            name,
            id
        );
        return false;
    }

    if name.starts_with('#') {
        tracing::info!("Icon ignored because it starts with '#': {}", name);
        return false;
    }

    if name.ends_with("-old") {
        tracing::warn!("Warning: {} contains 'old', icon ignored", name);
        return false;
    }
    true
}
