use std::{collections::BTreeMap, fs::File, io::BufReader};

use anyhow::Result;
use gettextrs::gettext;
use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*};

use super::icon::{Icon, IconJson};
use crate::{config, models::IconsGroup};

static FORBIDDEN_ICONS: [&str; 6] = [
    "gesture-rotate-clockwise-symbolic",
    "gesture-pinch-symbolic",
    "gesture-two-finger-swipe-left-symbolic",
    "gesture-two-finger-swipe-right-symbolic",
    "gesture-stretch-symbolic",
    "gesture-rotate-anticlockwise-symbolic",
];

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

    use super::*;

    #[derive(Debug, Default)]
    pub struct IconsModel(pub RefCell<Vec<IconsGroup>>);

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

    impl ObjectImpl for IconsModel {
        fn constructed(&self) {
            self.parent_constructed();

            if let Err(err) = self.obj().init() {
                log::error!("Failed to init IconsModel {err}");
            }
        }
    }
    impl ListModelImpl for IconsModel {
        fn item_type(&self) -> glib::Type {
            IconsGroup::static_type()
        }
        fn n_items(&self) -> u32 {
            self.0.borrow().len() as u32
        }
        fn item(&self, position: u32) -> Option<glib::Object> {
            self.0
                .borrow()
                .get(position as usize)
                .map(|o| o.clone().upcast::<glib::Object>())
        }
    }
}

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

impl IconsModel {
    pub fn get_icon_byname(&self, icon_name: &str) -> Option<Icon> {
        for pos in 0..self.n_items() {
            let obj = self.item(pos);
            let icon_group = obj.and_downcast_ref::<IconsGroup>().unwrap();
            for poss in 0..icon_group.icons().n_items() {
                let obj = icon_group.icons().item(poss);
                let icon = obj.and_downcast::<Icon>().unwrap();
                if icon.name() == icon_name {
                    return Some(icon);
                }
            }
        }
        None
    }

    // TODO This is ugly.
    pub fn search(&self, terms: &[String]) -> Vec<String> {
        let mut res = vec![];
        for pos in 0..self.n_items() {
            let obj = self.item(pos);
            let icon_group = obj.and_downcast_ref::<IconsGroup>().unwrap();
            for poss in 0..icon_group.icons().n_items() {
                let obj = icon_group.icons().item(poss);
                let icon = obj.and_downcast_ref::<Icon>().unwrap();
                for term in terms.iter() {
                    if icon.should_display(term) {
                        res.push(icon.name())
                    }
                }
            }
        }
        res
    }

    fn init(&self) -> Result<()> {
        self.init_devkit_icons()?;
        self.init_system_icons()?;

        Ok(())
    }

    fn init_devkit_icons(&self) -> Result<()> {
        let dev_kit = [config::PKGDATADIR, "icons_dev_kit.json"]
            .iter()
            .collect::<std::path::PathBuf>();

        let file = File::open(&dev_kit)?;
        let reader = BufReader::new(file);
        let dev_kit_icons: Vec<IconJson> = serde_json::from_reader(reader)?;

        let mut icons: BTreeMap<String, Vec<Icon>> = BTreeMap::new();

        dev_kit_icons.into_iter().for_each(|json_icon| {
            let icon = Icon::new(&json_icon.name, json_icon.tags);
            let ctx = json_icon.context.split('-').collect::<Vec<_>>().join(" ");
            icons
                .entry(ctx)
                .and_modify(|i| {
                    i.push(icon.clone());
                })
                .or_insert_with(|| vec![icon]);
        });
        {
            let mut data = self.imp().0.borrow_mut();
            let total = icons.len() as u32;

            icons.into_iter().for_each(|(name, icons)| {
                let group = IconsGroup::new(&name);
                group.add_icons(&icons);
                data.push(group);
            });

            self.items_changed(0, 0, total);
        };
        Ok(())
    }

    fn init_system_icons(&self) -> Result<()> {
        // Load default theme icons
        let theme = gtk::IconTheme::for_display(&gdk::Display::default().unwrap());

        let gtk_group = IconsGroup::new(&gettext("Pre-Installed System Icons"));
        gtk_group.set_is_system(true);

        let icons = theme
            .icon_names()
            .into_iter()
            .filter(|icon_name| {
                // filter out unneeded icons
                !FORBIDDEN_ICONS.contains(&icon_name.as_str())
                    && icon_name.ends_with("-symbolic")
                    && icon_name != config::APP_SYMBOLIC
            })
            .collect::<Vec<glib::GString>>();
        let total = icons.len() as u32;

        gtk_group.add_icons(
            &icons
                .into_iter()
                .map(|icon_name| {
                    let icon = Icon::new(&icon_name, vec![]);
                    icon.set_is_system(true);
                    icon
                })
                .collect::<Vec<Icon>>(),
        );

        let mut data = self.imp().0.borrow_mut();
        data.push(gtk_group);

        self.items_changed(0, 0, total);
        Ok(())
    }
}

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