use gtk::{
    glib::{self, clone},
    prelude::*,
    subclass::prelude::*,
};

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

    use super::*;

    #[derive(Default)]
    pub struct EveryFilter {
        pub(super) filters: RefCell<HashMap<String, (gtk::Filter, glib::SignalHandlerId)>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for EveryFilter {
        const NAME: &'static str = "BustleEveryFilter";
        type Type = super::EveryFilter;
        type ParentType = gtk::Filter;
    }

    impl ObjectImpl for EveryFilter {
        fn dispose(&self) {
            let obj = self.obj();

            obj.clear();
        }
    }

    impl FilterImpl for EveryFilter {
        fn strictness(&self) -> gtk::FilterMatch {
            let mut ret = gtk::FilterMatch::All;

            for (filter, _) in self.filters.borrow().values() {
                match filter.strictness() {
                    gtk::FilterMatch::Some => {
                        ret = gtk::FilterMatch::Some;
                    }
                    gtk::FilterMatch::None => {
                        return gtk::FilterMatch::None;
                    }
                    gtk::FilterMatch::All => {}
                    _ => unreachable!(),
                }
            }

            ret
        }

        fn match_(&self, item: &glib::Object) -> bool {
            self.filters
                .borrow()
                .values()
                .all(|(filter, _)| filter.match_(item))
        }
    }
}

glib::wrapper! {
    pub struct EveryFilter(ObjectSubclass<imp::EveryFilter>)
        @extends gtk::Filter;
}

impl EveryFilter {
    pub fn contains(&self, id: &str) -> bool {
        self.imp().filters.borrow().contains_key(id)
    }

    pub fn insert(&self, id: impl Into<String>, filter: impl IsA<gtk::Filter>) -> bool {
        let imp = self.imp();

        let signal_id = filter.connect_changed(clone!(
            #[weak(rename_to = obj)]
            self,
            move |_, change| {
                obj.changed(change);
            }
        ));

        let prev_value = imp
            .filters
            .borrow_mut()
            .insert(id.into(), (filter.as_ref().clone(), signal_id));
        let had_prev_value = prev_value.is_some();

        if let Some((prev_filter, prev_signal_id)) = prev_value {
            prev_filter.disconnect(prev_signal_id);

            self.changed(gtk::FilterChange::Different);
        } else {
            self.changed(gtk::FilterChange::MoreStrict);
        }

        !had_prev_value
    }

    pub fn remove(&self, id: &str) -> bool {
        let imp = self.imp();

        let prev_value = imp.filters.borrow_mut().remove(id);
        let had_prev_value = prev_value.is_some();

        if let Some((_, signal_id)) = prev_value {
            self.disconnect(signal_id);

            self.changed(gtk::FilterChange::LessStrict);
        }

        had_prev_value
    }

    pub fn clear(&self) {
        let imp = self.imp();

        for (_, (filter, signal_id)) in imp.filters.take() {
            filter.disconnect(signal_id);
        }

        self.changed(gtk::FilterChange::LessStrict);
    }
}

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