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

use gtk::prelude::*;
use gtk::subclass::prelude::*;
use sourceview::prelude::*;

use gtk::{gdk, gio, glib};

mod imp {
    use super::*;

    use std::cell::{Cell, OnceCell};

    use gdk::{Key, ModifierType};
    use gettextrs::gettext;

    use crate::{
        application::Application,
        utils::{spawn_async_local, to_entries, GActions},
        widgets::{Editor, SearchEntry, ShowToast},
    };

    /// Private part of [`SearchBar`].
    ///
    /// [`SearchBar`]: super::SearcBar
    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
    #[properties(wrapper_type = super::SearchBar)]
    #[template(resource = "/io/gitlab/liferooter/TextPieces/ui/search-bar.ui")]
    pub struct SearchBar {
        /// Whether the search is active.
        #[property(get)]
        search_active: Cell<bool>,

        /// Whether to show search options.
        #[property(get, set)]
        show_options: Cell<bool>,

        /// Whether to show "Replace" functionality.
        #[property(get, set)]
        show_replace: Cell<bool>,

        /// Is any match chosen.
        #[property(get, set)]
        has_current_match: Cell<bool>,

        /// Is there any search matches.
        #[property(get, set)]
        has_matches: Cell<bool>,

        /// Connected editor.
        #[property(get, construct_only)]
        editor: OnceCell<Editor>,

        /// Search context.
        #[property(get)]
        search_context: OnceCell<sourceview::SearchContext>,

        /// Search settings.
        #[template_child]
        search_settings: TemplateChild<sourceview::SearchSettings>,

        /// Search entry.
        #[template_child]
        search_entry: TemplateChild<SearchEntry>,

        /// Move previous button.
        #[template_child]
        move_next: TemplateChild<gtk::Button>,

        /// Move next button.
        #[template_child]
        move_previous: TemplateChild<gtk::Button>,

        /// Replace mode button.
        #[template_child]
        replace_mode_button: TemplateChild<gtk::ToggleButton>,

        /// Show options button
        #[template_child]
        options_button: TemplateChild<gtk::ToggleButton>,

        /// Close button.
        #[template_child]
        close_button: TemplateChild<gtk::Button>,

        /// Replace entry
        #[template_child]
        replace_entry: TemplateChild<gtk::Entry>,

        /// Replace one button.
        #[template_child]
        replace_button: TemplateChild<gtk::Button>,

        /// Replace all button.
        #[template_child]
        replace_all_button: TemplateChild<gtk::Button>,

        /// Regex button.
        #[template_child]
        regex_button: gtk::TemplateChild<gtk::CheckButton>,

        /// Case sensitive button.
        #[template_child]
        case_button: gtk::TemplateChild<gtk::CheckButton>,

        /// Whole words button.
        #[template_child]
        word_button: gtk::TemplateChild<gtk::CheckButton>,

        /// Revealer of the search bar.
        #[template_child]
        revealer: TemplateChild<gtk::Revealer>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for SearchBar {
        const NAME: &'static str = "TextPiecesSearchBar";
        type Type = super::SearchBar;
        type ParentType = gtk::Widget;

        fn class_init(class: &mut Self::Class) {
            class.bind_template();
            class.bind_template_callbacks();
            class.set_css_name("editorsearchbar");
            class.add_binding_action(Key::Escape, ModifierType::empty(), "search.hide");
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for SearchBar {
        fn constructed(&self) {
            self.parent_constructed();

            let bar = self.obj().to_owned();

            self.revealer
                .connect_child_revealed_notify(move |revealer| {
                    bar.imp().search_active.set(revealer.is_child_revealed());
                    bar.notify_search_active();
                });

            let settings = Application::settings();
            let search_options = ["at-word-boundaries", "regex-enabled", "case-sensitive"];

            for option in search_options {
                settings
                    .bind(&format!("search-{option}"), &*self.search_settings, option)
                    .get()
                    .set()
                    .build();
            }

            if search_options
                .iter()
                .any(|option| self.search_settings.property(option))
            {
                self.obj().set_show_options(true);
            }

            // Dirty hack to prevent accessing not-fully-initialized object
            let search_bar = self.obj().to_owned();
            glib::idle_add_local_once(move || {
                let imp = search_bar.imp();

                let editor = imp
                    .editor
                    .get()
                    .expect("Editor is not connected to search bar during construction");

                let search_context = sourceview::SearchContext::builder()
                    .settings(&*imp.search_settings)
                    .buffer(&editor.buffer())
                    .highlight(true)
                    .build();

                // Show regex errors via entry style and tooltip
                search_context.connect_regex_error_notify(glib::clone!(
                    #[weak]
                    search_bar,
                    move |context| {
                        let imp = search_bar.imp();
                        let error = context.regex_error();

                        if error.is_some() {
                            imp.search_entry.add_css_class("error");
                        } else {
                            imp.search_entry.remove_css_class("error");
                        }

                        imp.search_entry.set_tooltip_text(
                            error
                                .map(|e| gettext!("Invalid regex: {}", e.message()))
                                .as_deref(),
                        );
                    }
                ));

                search_context
                    .bind_property("occurrences-count", &search_bar, "has-matches")
                    .transform_to(|_, n_occurrences: i32| Some(n_occurrences > 0))
                    .build();

                fn update_current_match(
                    search_bar: super::SearchBar,
                    buffer: &sourceview::Buffer,
                    search_context: &sourceview::SearchContext,
                ) {
                    search_bar.set_has_current_match(
                        buffer
                            .selection_bounds()
                            .map(|(start, end)| {
                                search_context.occurrence_position(&start, &end) > 0
                            })
                            .unwrap_or_default(),
                    );
                }

                editor.buffer().connect_cursor_moved(glib::clone!(
                    #[weak]
                    search_context,
                    #[weak]
                    search_bar,
                    move |buffer| {
                        update_current_match(search_bar, buffer, &search_context);
                    }
                ));

                search_context.connect_occurrences_count_notify(glib::clone!(
                    #[weak]
                    search_bar,
                    #[weak]
                    editor,
                    move |search_context| {
                        update_current_match(search_bar, &editor.buffer(), search_context);
                    }
                ));

                imp.search_settings.connect_search_text_notify(glib::clone!(
                    #[weak]
                    search_bar,
                    #[weak]
                    editor,
                    #[weak]
                    search_context,
                    move |_| {
                        update_current_match(search_bar, &editor.buffer(), &search_context);
                    }
                ));

                search_bar
                    .imp()
                    .search_context
                    .set(search_context)
                    .expect("Search context is already set somehow");
            });
        }
    }

    impl WidgetImpl for SearchBar {
        fn grab_focus(&self) -> bool {
            self.search_entry.grab_focus()
        }

        fn root(&self) {
            self.parent_root();

            let actions: GActions<Self::Type> = &[
                ("show", |search_bar| {
                    search_bar.set_show_replace(false);
                    search_bar.imp().show();
                }),
                ("show-replace", |search_bar| {
                    search_bar.set_show_replace(true);
                    search_bar.imp().show();
                }),
                ("hide", |search_bar| {
                    search_bar.imp().hide();
                }),
            ];

            let search_bar = self.obj();

            let move_next_action = gio::SimpleAction::new("move-next", None);
            move_next_action.connect_activate(glib::clone!(
                #[weak]
                search_bar,
                move |_, _| {
                    search_bar.imp().move_next();
                }
            ));
            move_next_action
                .bind_property("enabled", &*search_bar, "has-matches")
                .build();

            let move_previous_action = gio::SimpleAction::new("move-previous", None);
            move_previous_action.connect_activate(glib::clone!(
                #[weak]
                search_bar,
                move |_, _| {
                    search_bar.imp().move_previous();
                }
            ));
            move_previous_action
                .bind_property("enabled", &*search_bar, "has-matches")
                .build();

            let replace_one_action = gio::SimpleAction::new("replace-one", None);
            replace_one_action.connect_activate(glib::clone!(
                #[weak]
                search_bar,
                move |_, _| {
                    search_bar.imp().replace_one();
                }
            ));

            let replace_all_action = gio::SimpleAction::new("replace-all", None);
            replace_all_action.connect_activate(glib::clone!(
                #[weak]
                search_bar,
                move |_, _| {
                    search_bar.imp().replace_all();
                }
            ));

            let action_group = gio::SimpleActionGroup::new();
            action_group.add_action_entries(to_entries(actions, &search_bar));
            action_group.add_action(&move_next_action);
            action_group.add_action(&move_previous_action);
            action_group.add_action(&replace_one_action);
            action_group.add_action(&replace_all_action);

            self.obj()
                .root()
                .expect("Failed to get root widget")
                .insert_action_group("search", Some(&action_group));
        }

        fn focus(&self, direction_type: gtk::DirectionType) -> bool {
            let focus_order: &[&gtk::Widget] = &[
                self.search_entry.upcast_ref(),
                self.replace_entry.upcast_ref(),
                self.move_previous.upcast_ref(),
                self.move_next.upcast_ref(),
                self.replace_mode_button.upcast_ref(),
                self.options_button.upcast_ref(),
                self.replace_button.upcast_ref(),
                self.replace_all_button.upcast_ref(),
                self.regex_button.upcast_ref(),
                self.case_button.upcast_ref(),
                self.word_button.upcast_ref(),
                self.close_button.upcast_ref(),
            ];

            fn move_focus<'a>(
                parent: &gtk::Widget,
                queue: impl Iterator<Item = &'a gtk::Widget>,
            ) -> bool {
                queue
                    .skip_while(|&w| {
                        !w.root()
                            .and_then(|root| root.focus())
                            .map(|focus_widget| &focus_widget == w || focus_widget.is_ancestor(w))
                            .unwrap_or_default()
                    })
                    .skip(1)
                    .find(|&w| {
                        !std::iter::successors(Some(w.to_owned()), |w| w.parent())
                            .take_while(|w| w != parent)
                            .any(|w| !w.is_visible() || !w.is_child_visible() || !w.is_sensitive())
                    })
                    .map(|w| w.grab_focus())
                    .unwrap_or_default()
            }

            match direction_type {
                gtk::DirectionType::TabForward => {
                    move_focus(self.obj().upcast_ref(), focus_order.iter().copied())
                }
                gtk::DirectionType::TabBackward => {
                    move_focus(self.obj().upcast_ref(), focus_order.iter().rev().copied())
                }
                _ => self.parent_focus(direction_type),
            }
        }
    }

    impl SearchBar {
        fn show(&self) {
            self.revealer.set_reveal_child(true);
            self.grab_focus();
        }

        fn hide(&self) {
            self.revealer.set_reveal_child(false);

            self.search_entry.set_text("");
            self.replace_entry.set_text("");
            self.search_settings.set_search_text(None);

            let obj = self.obj();

            obj.set_show_replace(false);

            self.editor().grab_focus();
        }

        fn ctx(&self) -> sourceview::SearchContext {
            self.search_context.get().unwrap().to_owned()
        }

        fn editor(&self) -> Editor {
            self.editor.get().unwrap().to_owned()
        }

        fn move_next(&self) {
            if !self.has_matches.get() {
                return;
            }

            let ctx = self.ctx();
            let editor = self.editor();
            let buffer = editor.buffer();

            let (mut start, mut end) = buffer
                .selection_bounds()
                .unwrap_or_else(|| (buffer.start_iter(), buffer.start_iter()));
            start.order(&mut end);

            spawn_async_local(async move {
                if let Ok((start, end, _)) = ctx.forward_future(&end).await {
                    buffer.select_range(&start, &end);
                    editor.view().scroll_mark_onscreen(&buffer.get_insert());
                }
            });
        }

        fn move_previous(&self) {
            if !self.has_matches.get() {
                return;
            }

            let ctx = self.ctx();
            let editor = self.editor();
            let buffer = editor.buffer();

            let (mut start, mut end) = buffer
                .selection_bounds()
                .unwrap_or_else(|| (buffer.start_iter(), buffer.start_iter()));
            start.order(&mut end);

            spawn_async_local(async move {
                if let Ok((start, end, _)) = ctx.backward_future(&start).await {
                    buffer.select_range(&start, &end);

                    editor.view().scroll_mark_onscreen(&buffer.get_insert());
                }
            });
        }

        fn replace_one(&self) {
            if !self.has_current_match.get() {
                return;
            }

            let editor = self.editor();
            let ctx = self.ctx();
            let buffer = editor.buffer();

            let Some((mut start, mut end)) = buffer.selection_bounds() else {
                return;
            };

            // GtkSourceView is stupid and doesn't update
            // matches count immediately, so we have to use this hack.
            let n_occurrences = ctx.occurrences_count();

            if let Err(err) = ctx.replace(
                &mut start,
                &mut end,
                &sourceview::utils_unescape_search_text(&self.replace_entry.text()),
            ) {
                self.obj()
                    .show_toast(&gettext!("Failed to replace match: {}", err));
            }

            // One is replaced, n - 1 left
            if n_occurrences > 1 {
                self.move_next();
            }
        }

        fn replace_all(&self) {
            if !self.has_matches.get() {
                return;
            }

            let ctx = self.ctx();

            if let Err(err) = ctx.replace_all(&sourceview::utils_escape_search_text(
                &self.replace_entry.text(),
            )) {
                self.obj()
                    .show_toast(&gettext!("Failed to replace all matches: {}", err));
            }

            let editor = self.editor();

            editor
                .view()
                .scroll_mark_onscreen(&editor.buffer().get_insert());
        }
    }

    #[gtk::template_callbacks]
    impl SearchBar {
        #[template_callback]
        fn search_text(&self, text: &str, regex_enabled: bool) -> Option<String> {
            if text.is_empty() {
                None
            } else if !regex_enabled {
                Some(sourceview::utils_unescape_search_text(text).into())
            } else {
                Some(text.to_owned())
            }
        }
    }
}

glib::wrapper! {
    /// Editor search bar.
    pub struct SearchBar(ObjectSubclass<imp::SearchBar>)
        @extends gtk::Widget,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
