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

use crate::{
    application::Application,
    config,
    models::{IconsGroup, IconsModel},
    widgets::icons::IconsGroupWidget,
};

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

    use super::*;

    #[derive(Debug, gtk::CompositeTemplate, glib::Properties)]
    #[properties(wrapper_type = super::Window)]
    #[template(resource = "/org/gnome/design/IconLibrary/window.ui")]
    pub struct Window {
        #[property(get, set, construct_only)]
        pub model: OnceCell<IconsModel>,
        pub settings: gio::Settings,
        #[template_child]
        pub search_entry: TemplateChild<gtk::SearchEntry>,
        #[template_child]
        pub stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub listbox: TemplateChild<gtk::ListBox>,
        #[template_child]
        pub dark_mode_button: TemplateChild<gtk::Widget>,
    }

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

        fn new() -> Self {
            let settings = gio::Settings::new(config::APP_ID);
            Self {
                model: Default::default(),
                settings,
                listbox: TemplateChild::default(),
                stack: TemplateChild::default(),
                search_entry: TemplateChild::default(),
                dark_mode_button: TemplateChild::default(),
            }
        }

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_instance_callbacks();
            klass.install_action("win.search", None, |win, _, _| {
                win.imp().search_entry.grab_focus();
            });
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for Window {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();
            if config::PROFILE == "Devel" {
                obj.add_css_class("devel");
            }

            self.listbox.bind_model(Some(&obj.model()), move |obj| {
                let icons_group = obj.downcast_ref::<IconsGroup>().unwrap();
                let row = IconsGroupWidget::new(icons_group);
                row.upcast::<gtk::Widget>()
            });

            // Search
            self.search_entry.set_key_capture_widget(Some(&*obj));

            // Load latest window state
            obj.load_state();

            let manager = adw::StyleManager::default();
            self.dark_mode_button
                .set_visible(!manager.system_supports_color_schemes());
        }
    }
    impl WidgetImpl for Window {}
    impl WindowImpl for Window {
        fn close_request(&self) -> glib::Propagation {
            if let Err(err) = self.obj().save_state() {
                log::warn!("Failed to save window state {err:#?}");
            }
            self.parent_close_request()
        }
    }
    impl ApplicationWindowImpl for Window {}
    impl AdwApplicationWindowImpl for Window {}
}

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
        @implements gio::ActionMap, gio::ActionGroup, gtk::ConstraintTarget, gtk::Accessible, gtk::Buildable, gtk::ShortcutManager, gtk::Native, gtk::Root;
}

#[gtk::template_callbacks]
impl Window {
    pub fn new(model: &IconsModel, app: &Application) -> Self {
        glib::Object::builder()
            .property("application", app)
            .property("model", model)
            .build()
    }

    fn filter(&self, search_text: Option<String>) {
        let imp = self.imp();
        if let Some(search_text) = search_text {
            imp.listbox.set_filter_func(move |item| {
                let group_widget = item.downcast_ref::<IconsGroupWidget>().unwrap();
                let group = group_widget.group();
                if group.name().to_ascii_lowercase().contains(&search_text) {
                    group_widget.filter(None);
                    true
                } else {
                    group_widget.filter(Some(search_text.clone()));
                    group_widget.n_visible() > 0
                }
            });
        } else {
            imp.listbox.unset_filter_func();
            let mut index = 0;
            while let Some(child) = imp.listbox.row_at_index(index) {
                let group_widget = child.downcast_ref::<IconsGroupWidget>().unwrap();
                group_widget.filter(None);
                index += 1;
            }
        }
        if self.n_visible() == 0 {
            imp.stack.set_visible_child_name("empty");
        } else {
            imp.stack.set_visible_child_name("icons");
        }
    }

    fn n_visible(&self) -> u32 {
        let mut index = 0;
        let mut counter = 0;
        while let Some(child) = self.imp().listbox.row_at_index(index) {
            index += 1;
            // We use is_child_visible since is_visible and get_visible
            // always return true.
            if child.is_child_visible() {
                counter += 1;
            }
        }
        counter
    }

    fn save_state(&self) -> anyhow::Result<()> {
        let settings = &self.imp().settings;
        let size = self.default_size();

        settings.set_int("window-width", size.0)?;
        settings.set_int("window-height", size.1)?;
        settings.set_boolean("is-maximized", self.is_maximized())?;

        Ok(())
    }

    fn load_state(&self) {
        let settings = &self.imp().settings;

        let width = settings.int("window-width");
        let height = settings.int("window-height");

        if width > -1 && height > -1 {
            self.set_default_size(width, height);
        }

        let is_maximized = settings.boolean("is-maximized");

        if is_maximized {
            self.maximize();
        }
    }

    #[template_callback]
    fn search_entry_started(&self, entry: &gtk::SearchEntry) {
        entry.grab_focus();
    }

    #[template_callback]
    fn search_entry_changed(&self, entry: &gtk::SearchEntry) {
        let search_text = entry.text().to_ascii_lowercase();
        if search_text.is_empty() {
            self.filter(None);
        } else {
            self.filter(Some(search_text));
        }
    }

    #[template_callback]
    fn search_entry_stopped(&self, entry: &gtk::SearchEntry) {
        entry.set_text("");
    }
}
