// SPDX-FileCopyrightText: 2023 Gleb Smirnov <glebsmirnov0708@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-or-later

use adw::prelude::*;
use adw::subclass::prelude::*;

use gtk::{gdk, glib};
use textpieces_core::ParameterValue;

mod imp {
    use super::*;

    use crate::{
        actions::action_irrelevance,
        utils::{spawn_async_local, ActionModel},
        widgets::Window,
        Application,
    };
    use glib::clone;
    use tracing::warn;

    use std::{
        cell::{Cell, RefCell},
        cmp::Ordering,
    };

    #[derive(Debug)]
    struct AssociatedEntry {
        entry: gtk::SearchEntry,
        on_change_binding: glib::SignalHandlerId,
    }

    /// Private part of [`ActionSearch`].
    ///
    /// [`ActionSearch`]: super::ActionSearch
    #[derive(Default, Debug, gtk::CompositeTemplate, glib::Properties)]
    #[template(resource = "/io/gitlab/liferooter/TextPieces/ui/action-search.ui")]
    #[properties(wrapper_type = super::ActionSearch)]
    pub struct ActionSearch {
        /// Action sorter.
        sorter: gtk::CustomSorter,

        /// Action filter.
        filter: gtk::CustomFilter,

        /// Action list model.
        model: gtk::SortListModel,

        /// ID of the last used action.
        pub last_action: RefCell<String>,

        /// Parameter values of the last used action.
        pub last_action_params: RefCell<Vec<ParameterValue>>,

        /// Search entry.
        #[property(
            get,
            get = Self::search_entry,
            set = Self::set_search_entry,
            type = Option<gtk::SearchEntry>
        )]
        entry: RefCell<Option<AssociatedEntry>>,

        /// Application window containing the search.
        #[property(get, set)]
        window: RefCell<Option<Window>>,

        /// Whether the search items are sensitive.
        #[property(get, set)]
        search_sensitive: Cell<bool>,

        /// Stack container.
        ///
        /// Allows to show either search or "No Actions Found" page.
        #[template_child]
        stack: TemplateChild<gtk::Stack>,

        /// Search entry key controller.
        ///
        /// Used to navigate between actions from search entry.
        #[template_child]
        key_controller: TemplateChild<gtk::EventControllerKey>,

        /// Widget for list of actions.
        #[template_child]
        list_box: TemplateChild<gtk::ListBox>,

        /// Viewport used to scroll search results.
        #[template_child]
        pub viewport: TemplateChild<gtk::Viewport>,
    }

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

        fn class_init(class: &mut Self::Class) {
            class.bind_template();
            Self::bind_template_callbacks(class);
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for ActionSearch {
        /// Object contructor.
        fn constructed(&self) {
            self.parent_constructed();

            // Load the last used action from settings.
            //
            // Note that it's important to do it before
            // the model initialization since these values
            // are used in the sorter.
            self.load_last_action();

            // Load actions and setup the model
            self.setup_actions();

            // Select the first row
            self.reset_selection();
        }

        /// Kind of destructor, but can be ran multiple times.
        fn dispose(&self) {
            self.disconnect_entry();
        }
    }

    impl WidgetImpl for ActionSearch {}
    impl BinImpl for ActionSearch {}

    impl ActionSearch {
        /// Load last used action from `GSettings`.
        fn load_last_action(&self) {
            let settings = Application::settings();

            self.last_action.replace(settings.last_action_id());
            self.last_action_params.replace(settings.last_params());
        }

        /// Load actions and setup the list model.
        fn setup_actions(&self) {
            // Load actions
            let model = Application::action_list();

            let search = self.obj();

            // Wrap it with a filter
            let filter_model = gtk::FilterListModel::new(Some(model), Some(self.filter.clone()));
            self.filter.set_filter_func(clone!(
                #[weak]
                search,
                #[upgrade_or]
                true,
                move |obj| {
                    let action = obj.downcast_ref::<ActionModel>().unwrap();
                    let query = search.entry().map(|entry| entry.text()).unwrap_or_default();

                    action_irrelevance(action, &query) != u16::MAX
                }
            ));

            // Wrap it with a sorter and store
            self.model.set_model(Some(&filter_model));
            self.model.set_sorter(Some(&self.sorter));
            self.sorter.set_sort_func(clone!(
                #[weak]
                search,
                #[upgrade_or]
                gtk::Ordering::Equal,
                move |obj_a, obj_b| {
                    let action_a = obj_a.downcast_ref::<ActionModel>().unwrap();
                    let action_b = obj_b.downcast_ref::<ActionModel>().unwrap();
                    let search = search.imp();

                    let query = search
                        .search_entry()
                        .map(|entry| entry.text())
                        .unwrap_or_default();

                    if query.is_empty() {
                        let last = &*search.last_action.borrow();
                        (&action_a.id() == last)
                            .cmp(&(&action_b.id() == last))
                            .reverse()
                    } else {
                        Ordering::Equal
                    }
                    .then_with(|| {
                        action_irrelevance(action_a, &query)
                            .cmp(&action_irrelevance(action_b, &query))
                            .then_with(|| action_a.name().cmp(&action_b.name()))
                    })
                    .into()
                }
            ));

            // Ensure that selection is reset
            // each time the model changes
            let action_search = self.obj();
            self.model.connect_items_changed(clone!(
                #[weak]
                action_search,
                move |_, _, _, _| {
                    action_search.imp().reset_selection();
                }
            ));

            // Bind the model to the list
            self.list_box.bind_model(
                Some(&self.model),
                clone!(
                    #[weak_allow_none]
                    action_search,
                    move |obj| {
                        let action = obj.downcast_ref::<ActionModel>().unwrap();

                        let row = adw::ActionRow::builder()
                            .title(action.name())
                            .subtitle(action.description())
                            .activatable(true)
                            .build();

                        if let Some(action_search) = action_search {
                            action_search
                                .bind_property("search-sensitive", &row, "sensitive")
                                .sync_create()
                                .build();
                        }

                        row.upcast()
                    }
                ),
            );
        }

        /// Returns search entry instance
        fn search_entry(&self) -> Option<gtk::SearchEntry> {
            self.entry
                .borrow()
                .as_ref()
                .map(|entry| entry.entry.to_owned())
        }

        /// Unbinds and unsets search entry and sets new one.
        fn set_search_entry(&self, entry: Option<gtk::SearchEntry>) {
            self.disconnect_entry();

            if let Some(entry) = &entry {
                entry.add_controller(self.key_controller.clone());
            }

            let search = self.obj();
            self.entry.replace(entry.map(|entry| AssociatedEntry {
                on_change_binding: entry.connect_changed(clone!(
                    #[weak]
                    search,
                    move |_| {
                        let imp = search.imp();

                        // Invalidate search results
                        imp.filter.changed(gtk::FilterChange::Different);
                        imp.sorter.changed(gtk::SorterChange::Different);

                        // Select the first row if no row selected
                        imp.reset_selection();

                        // Select right stack page
                        imp.stack
                            .set_visible_child_name(if imp.model.n_items() == 0 {
                                "not-found"
                            } else {
                                "search"
                            });
                    }
                )),
                entry,
            }));
        }

        /// Disconnects associated entry.
        fn disconnect_entry(&self) {
            if let Some(AssociatedEntry {
                entry,
                on_change_binding,
            }) = self.entry.take()
            {
                entry.remove_controller(&self.key_controller.get());
                entry.disconnect(on_change_binding);
            }
        }

        /// Save last used action in `GSettings`.
        pub fn save_last_action(&self) {
            let settings = Application::settings();

            settings
                .try_set_last_action_id(self.last_action.borrow().as_str())
                .and_then(|()| settings.try_set_last_params(&self.last_action_params.borrow()))
                .unwrap_or_else(|err| warn!("Failed to save last used action: {err}"))
        }

        /// Select the first row.
        pub fn reset_selection(&self) {
            if let Some(row) = self.list_box.row_at_index(0) {
                self.list_box.select_row(Some(&row))
            }
        }
    }

    #[gtk::template_callbacks]
    impl ActionSearch {
        /// Called when user activates a row.
        #[template_callback]
        async fn on_row_activated(&self, row: &gtk::ListBoxRow) {
            let row: &adw::ActionRow = row
                .downcast_ref()
                .expect("Action list should only contain action rows");

            let action = self
                .model
                .item(row.index() as u32)
                .and_downcast::<ActionModel>()
                .expect("Failed to downcast action model");

            let action_id = action.id();

            let default_params = if action_id == self.last_action.replace(action_id.clone()) {
                self.last_action_params.take()
            } else {
                vec![]
            };

            // Perform action.
            let window = self
                .window
                .borrow()
                .to_owned()
                .expect("Failed to get application window");
            let last_params = window
                .action_selected(&action_id, action.is_builtin(), &default_params)
                .await;

            // Save last used action.
            // Last action ID is already updated.
            if let Some(last_params) = last_params {
                self.last_action_params.replace(last_params);
            }
            self.save_last_action();

            self.sorter.changed(gtk::SorterChange::Different);
            self.filter.changed(gtk::FilterChange::Different);
        }

        /// Called when user presses a key inside search entry.
        ///
        /// Allows to select action from search entry using arrows
        /// and Return key.
        #[template_callback]
        fn on_key_pressed(
            &self,
            keyval: gdk::Key,
            _keycode: u32,
            _modifiers: gdk::ModifierType,
        ) -> glib::signal::Propagation {
            match keyval {
                gdk::Key::Up | gdk::Key::Down => {
                    if let Some(row) = self.list_box.selected_row() {
                        row.grab_focus();
                    }
                    self.key_controller.forward(&self.list_box.get());
                    if let Some(AssociatedEntry { entry, .. }) = self.entry.borrow().as_ref() {
                        entry.grab_focus();
                    }
                }
                gdk::Key::Return => {
                    if let Some(row) = self.list_box.selected_row() {
                        let obj = self.obj().to_owned();
                        spawn_async_local(async move { obj.imp().on_row_activated(&row).await });
                    }
                }
                _ => return glib::signal::Propagation::Proceed,
            }

            glib::signal::Propagation::Stop
        }
    }
}

glib::wrapper! {
    pub struct ActionSearch(ObjectSubclass<imp::ActionSearch>)
        @extends gtk::Widget, adw::Bin,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl ActionSearch {
    /// Resets action search.
    pub fn reset(&self) {
        let imp = self.imp();

        // Select the first row
        imp.reset_selection();

        // Scroll to the top
        if let Some(vadj) = imp.viewport.vadjustment() {
            vadj.set_value(0.0);
        }
    }
}
