use std::cell::OnceCell;

use adw::subclass::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk::{gio, glib, CompositeTemplate, FilterListModel};

use crate::data::KrItem;
use crate::{
    data::{KrCollection, KrFlatpak},
    widgets::{KrCollectionList, KrFlatpakList},
};

mod imp {
    use crate::utils::error::DisplayError;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(file = "overview_page.ui")]
    pub struct KrOverviewPage {
        #[template_child]
        search_bar: TemplateChild<gtk::SearchBar>,
        #[template_child]
        search_entry: TemplateChild<gtk::SearchEntry>,

        #[template_child]
        pub loading_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub content_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        preferences_page: TemplateChild<adw::PreferencesPage>,

        #[template_child]
        collection_list: TemplateChild<KrCollectionList>,
        #[template_child]
        pub uninstalled_list: TemplateChild<KrFlatpakList>,
        #[template_child]
        pub installed_list: TemplateChild<KrFlatpakList>,

        pub collection_model: FilterListModel,
        pub flatpak_model: OnceCell<gio::ListStore>,
        pub installed_model: FilterListModel,
        pub uninstalled_model: FilterListModel,

        pub installed_everyfilter: OnceCell<gtk::EveryFilter>,
        pub uninstalled_everyfilter: OnceCell<gtk::EveryFilter>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for KrOverviewPage {
        const NAME: &'static str = "KrOverviewPage";
        type Type = super::KrOverviewPage;
        type ParentType = adw::NavigationPage;

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();

            klass.install_action("overview.search", None, |obj, _, _| {
                obj.imp().search_bar.set_search_mode(true);
            });
            klass.add_binding_action(
                gtk::gdk::Key::f,
                gtk::gdk::ModifierType::CONTROL_MASK,
                "overview.search",
            );
        }

        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

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

            self.search_bar.connect_entry(&*self.search_entry);

            // Collections filter
            self.collection_model
                .set_model(Some(&crate::app().secret_service()));
            let search_filter = gtk::StringFilter::new(Some(&gtk::PropertyExpression::new(
                KrCollection::static_type(),
                None::<&gtk::Expression>,
                "title",
            )));
            self.search_entry
                .bind_property("text", &search_filter, "search")
                .build();

            self.collection_model.set_filter(Some(&search_filter));
            self.collection_list.set_model(Some(&self.collection_model));

            // Hide collection list box when empty (search)
            self.collection_model.connect_items_changed(
                clone!(@weak self as this => move |m, _, _, _|{
                    this.collection_list.set_visible(m.n_items() != 0);
                    this.update_stack();
                }),
            );

            // Flatpak filter
            let installed_filter = gtk::CustomFilter::new(|o| {
                let flatpak = o.downcast_ref::<KrFlatpak>().unwrap();
                flatpak.is_installed()
            });
            let uninstalled_filter = gtk::CustomFilter::new(|o| {
                let flatpak = o.downcast_ref::<KrFlatpak>().unwrap();
                !flatpak.is_installed()
            });
            let not_empty_filter = gtk::CustomFilter::new(|o| {
                let flatpak = o.downcast_ref::<KrFlatpak>().unwrap();
                flatpak.item_count() != 0
            });

            // Combine flatpak filters
            let installed_everyfilter = gtk::EveryFilter::new();
            installed_everyfilter.append(installed_filter);
            installed_everyfilter.append(not_empty_filter.clone());

            let uninstalled_everyfilter = gtk::EveryFilter::new();
            uninstalled_everyfilter.append(uninstalled_filter);
            uninstalled_everyfilter.append(not_empty_filter);

            self.uninstalled_model
                .set_filter(Some(&uninstalled_everyfilter));
            self.installed_model
                .set_filter(Some(&installed_everyfilter));

            self.installed_everyfilter
                .set(installed_everyfilter)
                .unwrap();
            self.uninstalled_everyfilter
                .set(uninstalled_everyfilter)
                .unwrap();

            // Sort flatpaks alphabetically
            let sorter = gtk::StringSorter::new(Some(&gtk::PropertyExpression::new(
                KrFlatpak::static_type(),
                gtk::Expression::NONE,
                "title",
            )));
            let flatpak_model = gio::ListStore::new::<KrFlatpak>();
            let sorted = gtk::SortListModel::new(Some(flatpak_model.clone()), Some(sorter));

            // Search flatpaks
            let search_filter = gtk::StringFilter::new(Some(&gtk::PropertyExpression::new(
                KrFlatpak::static_type(),
                None::<&gtk::Expression>,
                "title",
            )));
            self.search_entry
                .bind_property("text", &search_filter, "search")
                .build();

            let search_model = FilterListModel::new(Some(sorted), Some(search_filter));

            // Bind flatpak models
            self.installed_model.set_model(Some(&search_model));
            self.uninstalled_model.set_model(Some(&search_model));

            self.installed_list.set_model(Some(&self.installed_model));
            self.uninstalled_list
                .set_model(Some(&self.uninstalled_model));

            // Hide flatpak listboxes without any items
            self.installed_model.connect_items_changed(
                clone!(@weak self as this => move |m, _, _, _|{
                    this.installed_list.set_visible(m.n_items() != 0);
                    this.update_stack();
                }),
            );
            self.uninstalled_model.connect_items_changed(
                clone!(@weak self as this => move |m, _, _, _|{
                    this.uninstalled_list.set_visible(m.n_items() != 0);
                    this.update_stack();
                }),
            );
            self.flatpak_model.set(flatpak_model).unwrap();
        }
    }

    impl WidgetImpl for KrOverviewPage {}

    impl NavigationPageImpl for KrOverviewPage {}

    impl KrOverviewPage {
        pub fn add_flatpak(&self, index: u32, flatpak: KrFlatpak) {
            self.flatpak_model.get().unwrap().insert(index, &flatpak);

            glib::MainContext::default().spawn_local(clone!(@weak flatpak => async move {
                flatpak.load_keyring().await.handle_error("Unable to load keyring");
            }));

            flatpak.connect_item_count_notify(clone!(@weak self as this => move |_| {
                this.installed_everyfilter.get().unwrap().changed(gtk::FilterChange::Different);
                this.uninstalled_everyfilter.get().unwrap().changed(gtk::FilterChange::Different);
            }));

            flatpak.connect_is_installed_notify(clone!(@weak self as this => move |_| {
                this.installed_everyfilter.get().unwrap().changed(gtk::FilterChange::Different);
                this.uninstalled_everyfilter.get().unwrap().changed(gtk::FilterChange::Different);
            }));
        }

        pub fn remove_flatpak(&self, index: u32) {
            self.flatpak_model.get().unwrap().remove(index);
        }

        fn update_stack(&self) {
            let is_empty = self.collection_model.n_items() == 0
                && self.installed_model.n_items() == 0
                && self.uninstalled_model.n_items() == 0;

            if is_empty {
                self.content_stack.set_visible_child(&*self.status_page);
            } else {
                self.content_stack
                    .set_visible_child(&*self.preferences_page);
            }
        }
    }
}

glib::wrapper! {
    pub struct KrOverviewPage(ObjectSubclass<imp::KrOverviewPage>)
        @extends gtk::Widget, adw::NavigationPage;
}

impl KrOverviewPage {
    pub fn set_login_collection(&self, login_collection: KrCollection) {
        let imp = self.imp();
        let flatpak_items = login_collection.flatpak_items();

        login_collection
            .bind_property("is-locked", &*imp.installed_list, "sensitive")
            .invert_boolean()
            .sync_create()
            .build();
        login_collection
            .bind_property("is-locked", &*imp.uninstalled_list, "sensitive")
            .invert_boolean()
            .sync_create()
            .build();

        // Map the already existing KrItems to KrFlatpaks
        for (i, o) in flatpak_items.snapshot().into_iter().enumerate() {
            let item = o.downcast::<KrItem>().unwrap();
            let flatpak = KrFlatpak::from(&item);
            self.imp().add_flatpak(i.try_into().unwrap(), flatpak);
        }

        flatpak_items.connect_items_changed(
            clone!(@weak self as this => move |m, pos, removed, added| {
                for i in pos..pos + added {
                    let item = m.item(i).unwrap().downcast::<KrItem>().unwrap();
                    let flatpak = KrFlatpak::from(&item);
                    this.imp().add_flatpak(i, flatpak);
                }

                for i in pos..pos + removed {
                    this.imp().remove_flatpak(i);
                }
            }),
        );

        self.imp()
            .loading_stack
            .set_visible_child(&*self.imp().content_stack);
    }
}
