// SPDX-FileCopyrightText: 2025 Andrey Kutejko <andy128k@gmail.com>
// SPDX-FileCopyrightText: 2026 Wladimir Palant https://palant.info/
//
// SPDX-License-Identifier: GPL-3.0-or-later

use super::{
    backend, backend::SearchMessage, profile::SearchProfile,
    selection_profile_component::SelectionProfileComponent,
};
use crate::{
    dialogs::profiles::{ProfileManager, manage_profiles_dialog::manage_profiles},
    dir::Directory,
    file::FileOps,
    main_win::MainWindow,
    options::SearchConfig,
    shortcuts::Area,
    tags::FileMetadataService,
    user_actions::UserAction,
    utils::WindowExt,
};
use gettextrs::{gettext, ngettext};
use gtk::{gio, glib, pango, prelude::*, subclass::prelude::*};
use std::rc::Rc;

struct SearchProfiles {
    profiles: gio::ListStore,
}

impl Clone for SearchProfiles {
    fn clone(&self) -> Self {
        Self {
            profiles: self
                .profiles
                .iter::<SearchProfile>()
                .flatten()
                .map(|p| p.deep_clone())
                .collect(),
        }
    }
}

impl SearchProfiles {
    fn new(cfg: &SearchConfig, with_default: bool) -> Self {
        let profiles: gio::ListStore = cfg
            .profiles()
            .iter::<SearchProfile>()
            .flatten()
            .map(|p| p.deep_clone())
            .collect();

        if with_default {
            profiles.append(&cfg.default_profile().deep_clone());
        }
        Self { profiles }
    }

    fn profile(&self, profile_index: usize) -> Option<SearchProfile> {
        self.profiles
            .item(profile_index as u32)
            .and_downcast::<SearchProfile>()
    }

    fn len(&self) -> usize {
        self.profiles.n_items() as usize
    }

    fn profile_name(&self, profile_index: usize) -> String {
        self.profile(profile_index)
            .map(|p| p.name())
            .unwrap_or_default()
    }

    fn set_profile_name(&self, profile_index: usize, name: &str) {
        if let Some(profile) = self.profile(profile_index) {
            profile.set_name(name);
        }
    }

    fn profile_description(&self, profile_index: usize) -> String {
        self.profile(profile_index)
            .map(|p| p.path_pattern())
            .unwrap_or_default()
    }

    fn reset_profile(&self, profile_index: usize) {
        if let Some(profile) = self.profile(profile_index) {
            profile.reset();
        }
    }

    fn duplicate_profile(&self, profile_index: usize) -> usize {
        let profile = self
            .profile(profile_index)
            .map(|p| p.deep_clone())
            .unwrap_or_default();
        self.profiles.append(&profile);
        self.profiles.n_items() as usize - 1
    }

    fn pick(&self, indexes: &[usize]) {
        let picked: Vec<_> = indexes
            .iter()
            .filter_map(|i| self.profiles.item(*i as u32))
            .collect();
        self.profiles.remove_all();
        for p in picked {
            self.profiles.append(&p);
        }
    }
}

struct SearchProfileManager {
    config: Rc<SearchConfig>,
    profiles: SearchProfiles,
}

impl ProfileManager for SearchProfileManager {
    fn len(&self) -> usize {
        self.profiles.len()
    }

    fn profile_name(&self, profile_index: usize) -> String {
        self.profiles.profile_name(profile_index)
    }

    fn set_profile_name(&self, profile_index: usize, name: &str) {
        self.profiles.set_profile_name(profile_index, name);
    }

    fn profile_description(&self, profile_index: usize) -> String {
        self.profiles.profile_description(profile_index)
    }

    fn reset_profile(&self, profile_index: usize) {
        self.profiles.reset_profile(profile_index);
    }

    fn duplicate_profile(&self, profile_index: usize) -> usize {
        self.profiles.duplicate_profile(profile_index)
    }

    fn pick(&self, profile_indexes: &[usize]) {
        self.profiles.pick(profile_indexes);
    }

    fn create_component(
        &self,
        profile_index: usize,
        labels_size_group: &gtk::SizeGroup,
    ) -> gtk::Widget {
        let widget = SelectionProfileComponent::with_profile(
            &self.profiles.profile(profile_index).unwrap(),
            Some(labels_size_group),
        );
        widget.set_name_patterns_history(&self.config.name_patterns());
        widget.set_content_patterns_history(&self.config.content_patterns());
        widget.update();
        widget.grab_focus();
        widget.upcast()
    }

    fn update_component(&self, _profile_index: usize, component: &gtk::Widget) {
        component
            .downcast_ref::<SelectionProfileComponent>()
            .unwrap()
            .update();
    }

    fn copy_component(&self, _profile_index: usize, component: &gtk::Widget) {
        component
            .downcast_ref::<SelectionProfileComponent>()
            .unwrap()
            .copy();
    }
}

mod imp {
    use super::*;
    use crate::{
        connection::list::ConnectionList,
        dir::Directory,
        file::File,
        file_list::list::FileList,
        intviewer::search_bar::Mode,
        main_win::MainWindow,
        options::{GeneralOptions, ViewerOptions, utils::remember_window_size},
        select_directory_button::DirectoryButton,
        types::FileSelectorID,
        utils::{dialog_button_box, display_help},
    };
    use std::cell::{OnceCell, RefCell};

    #[derive(glib::Properties)]
    #[properties(wrapper_type = super::SearchDialog)]
    pub struct SearchDialog {
        pub labels_size_group: gtk::SizeGroup,

        pub config: OnceCell<Rc<SearchConfig>>,

        #[property(get, construct_only)]
        pub file_metadata_service: OnceCell<FileMetadataService>,
        #[property(get, construct_only)]
        pub main_window: OnceCell<MainWindow>,

        #[property(get, set)]
        pub dir_browser: RefCell<DirectoryButton>,
        #[property(get, set)]
        pub profile_component: RefCell<SelectionProfileComponent>,
        virtual_directory: Directory,
        #[property(get, set)]
        pub result_list: RefCell<Option<FileList>>,
        quick_search_box: gtk::Box,
        pub status_label: gtk::Label,
        pub progress_bar: gtk::ProgressBar,
        #[property(get, set)]
        pub profile_menu_button: RefCell<gtk::MenuButton>,

        jump_button: gtk::Button,
        stop_button: gtk::Button,
        find_button: gtk::Button,

        #[property(get, set, nullable)]
        start_dir: RefCell<Option<Directory>>,

        #[property(get, set, nullable)]
        cancellable: RefCell<Option<gio::Cancellable>>,
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.install_action_async("search.save-profile", None, |obj, _, _| async move {
                obj.imp().save_profile().await
            });
            klass.install_action_async("search.manage-profiles", None, |obj, _, _| async move {
                obj.imp().manage_profiles().await
            });
            klass.install_action(
                "search.load-profile",
                Some(&u32::static_variant_type()),
                |obj, _, param| {
                    if let Some(profile_idx) = param.and_then(u32::from_variant) {
                        obj.imp().load_profile(profile_idx);
                    }
                },
            );

            klass.install_action(UserAction::FileView.name(), None, |obj, _, _| {
                obj.imp().file_view();
            });
            klass.install_action(UserAction::FileInternalView.name(), None, |obj, _, _| {
                obj.imp().file_internal_view();
            });
            klass.install_action(UserAction::FileExternalView.name(), None, |obj, _, _| {
                obj.imp().file_external_view();
            });
            klass.install_action(UserAction::FileEdit.name(), None, |obj, _, _| {
                obj.imp().file_edit();
            });
            klass.install_action(UserAction::FileDelete.name(), None, |obj, _, _| {
                obj.imp().file_delete(false);
            });
            klass.install_action(
                UserAction::FileDeletePermanently.name(),
                None,
                |obj, _, _| {
                    obj.imp().file_delete(true);
                },
            );
        }

        fn new() -> Self {
            let labels_size_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal);
            Self {
                config: Default::default(),
                file_metadata_service: Default::default(),
                main_window: Default::default(),
                dir_browser: Default::default(),
                profile_component: RefCell::new(SelectionProfileComponent::new(Some(
                    &labels_size_group,
                ))),
                virtual_directory: Directory::create_virtual(),
                result_list: Default::default(),
                quick_search_box: gtk::Box::builder()
                    .orientation(gtk::Orientation::Vertical)
                    .build(),
                status_label: gtk::Label::builder()
                    .max_width_chars(1)
                    .hexpand(true)
                    .ellipsize(pango::EllipsizeMode::Middle)
                    .build(),
                progress_bar: gtk::ProgressBar::builder()
                    .show_text(false)
                    .pulse_step(0.02)
                    .build(),
                profile_menu_button: RefCell::new(
                    gtk::MenuButton::builder()
                        .label(gettext("Pr_ofiles…"))
                        .use_underline(true)
                        .build(),
                ),

                jump_button: gtk::Button::builder()
                    .label(gettext("_Jump to"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                stop_button: gtk::Button::builder()
                    .label(gettext("_Stop"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                find_button: gtk::Button::builder()
                    .label(gettext("_Find"))
                    .use_underline(true)
                    .build(),

                labels_size_group,

                start_dir: Default::default(),

                cancellable: Default::default(),
            }
        }
    }

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

            let this = self.obj();
            this.add_css_class("dialog");

            this.set_title(Some(&gettext("Search…")));
            this.set_resizable(true);

            let grid = gtk::Grid::builder().css_classes(["search-grid"]).build();
            this.set_child(Some(&grid));

            // search in
            let dir_browser_label = gtk::Label::builder()
                .label(gettext("_Look in folder:"))
                .use_underline(true)
                .mnemonic_widget(&*self.dir_browser.borrow())
                .halign(gtk::Align::Start)
                .valign(gtk::Align::Center)
                .xalign(0.0)
                .build();
            self.labels_size_group.add_widget(&dir_browser_label);
            grid.attach(&dir_browser_label, 0, 0, 1, 1);

            let dir_browser = this.dir_browser();
            dir_browser.set_hexpand(true);
            grid.attach(&dir_browser, 1, 0, 1, 1);

            // profile
            grid.attach(&this.profile_component(), 0, 1, 2, 1);

            // file list
            let result_list = FileList::new(&this.file_metadata_service());
            result_list.set_height_request(200);
            self.result_list.replace(Some(result_list.clone()));

            self.virtual_directory.connect_closure(
                "file-deleted",
                false,
                glib::closure_local!(
                    #[weak]
                    result_list,
                    move |_: &Directory, f: &File| {
                        result_list.remove_file(f);
                    }
                ),
            );

            result_list.connect_show_quick_search(glib::clone!(
                #[weak(rename_to = quick_search_box)]
                self.quick_search_box,
                move |_, widget| {
                    while let Some(child) = quick_search_box.first_child() {
                        child.unparent();
                    }
                    quick_search_box.append(&widget);
                }
            ));

            let sw = gtk::ScrolledWindow::builder()
                .vexpand(true)
                .hscrollbar_policy(gtk::PolicyType::Automatic)
                .vscrollbar_policy(gtk::PolicyType::Automatic)
                .child(&result_list)
                .build();
            grid.attach(&sw, 0, 2, 2, 1);
            grid.attach(&self.quick_search_box, 0, 3, 2, 1);

            // status & progress
            let statusbar = gtk::Box::builder()
                .css_classes(["search-statusbar"])
                .orientation(gtk::Orientation::Vertical)
                .build();
            statusbar.append(&self.progress_bar);
            statusbar.append(&self.status_label);
            self.progress_bar.set_visible(false);
            grid.attach(&statusbar, 0, 4, 2, 1);

            let help_button = gtk::Button::builder()
                .label(gettext("_Help"))
                .use_underline(true)
                .build();
            help_button.connect_clicked(glib::clone!(
                #[weak]
                this,
                move |_| {
                    glib::spawn_future_local(async move {
                        display_help(this.upcast_ref(), Some("gnome-commander-search")).await;
                    });
                }
            ));

            let close_button = gtk::Button::builder()
                .label(gettext("_Close"))
                .use_underline(true)
                .build();
            close_button.connect_clicked(glib::clone!(
                #[weak]
                this,
                move |_| {
                    this.imp().stop();
                    this.close();
                }
            ));

            self.jump_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.jump()
            ));
            self.stop_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.stop()
            ));
            self.find_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    glib::spawn_future_local(async move {
                        imp.find().await;
                    });
                }
            ));

            this.set_default_widget(Some(&self.find_button));
            this.set_cancel_widget(&close_button);

            grid.attach(
                &dialog_button_box(
                    &[&help_button],
                    &[
                        self.profile_menu_button
                            .borrow()
                            .upcast_ref::<gtk::Widget>(),
                        close_button.upcast_ref(),
                        self.jump_button.upcast_ref(),
                        self.stop_button.upcast_ref(),
                        self.find_button.upcast_ref(),
                    ],
                ),
                0,
                5,
                2,
                1,
            );

            this.set_hide_on_close(true);
            this.connect_hide(glib::clone!(
                #[weak]
                this,
                move |_| {
                    this.profile_component().copy();
                }
            ));

            this.update_style();

            let options = GeneralOptions::new();
            remember_window_size(
                &*this,
                &options.search_window_width,
                &options.search_window_height,
            );
        }

        fn dispose(&self) {
            if let Some(cancellable) = self.cancellable.replace(None) {
                cancellable.cancel();
            }
        }
    }

    impl WidgetImpl for SearchDialog {}
    impl WindowImpl for SearchDialog {}

    impl SearchDialog {
        pub fn config(&self) -> Rc<SearchConfig> {
            self.config.get().unwrap().clone()
        }

        pub fn update_profile_menu(&self) {
            let menu = gio::Menu::new();
            menu.append(
                Some(&gettext("_Save Profile As…")),
                Some("search.save-profile"),
            );

            let search_config = self.config();
            let profiles = search_config.profiles();
            let count = profiles.n_items();
            if count > 0 {
                menu.append(
                    Some(&gettext("_Manage Profiles…")),
                    Some("search.manage-profiles"),
                );

                let profiles_section = gio::Menu::new();
                for (index, profile) in profiles.iter::<SearchProfile>().flatten().enumerate() {
                    let item = gio::MenuItem::new(Some(&profile.name()), None);
                    item.set_action_and_target_value(
                        Some("search.load-profile"),
                        Some(&(index as u32).to_variant()),
                    );
                    profiles_section.append_item(&item);
                }
                menu.append_section(None, &profiles_section);
            }

            self.obj().profile_menu_button().set_menu_model(Some(&menu));
        }

        async fn save_profile(&self) {
            self.obj().profile_component().copy();
            self.do_manage_profiles(true).await;
        }

        async fn manage_profiles(&self) {
            self.do_manage_profiles(false).await;
        }

        async fn do_manage_profiles(&self, new_profile: bool) {
            let config = self.config();

            let profiles = SearchProfiles::new(&config, new_profile);

            if new_profile {
                let last_index = profiles.len() - 1;
                profiles.set_profile_name(last_index, &gettext("New profile"));
            }

            let manager = Rc::new(SearchProfileManager { config, profiles });

            if manage_profiles(
                self.obj().upcast_ref(),
                &manager,
                &gettext("Profiles"),
                Some("gnome-commander-search"),
                new_profile,
            )
            .await
            {
                let profiles = manager.profiles.clone();
                manager.config.set_profiles(&profiles.profiles);
                self.update_profile_menu();
            }
        }

        fn load_profile(&self, profile_idx: u32) {
            let config = self.config();
            let Some(profile) = config
                .profiles()
                .item(profile_idx)
                .and_downcast::<SearchProfile>()
            else {
                return;
            };
            config.default_profile().copy_from(&profile);
            self.obj().profile_component().update();
        }

        fn selected_file(&self) -> Option<File> {
            let result_list = self.obj().result_list()?;
            let file = result_list.selected_file()?;
            Some(file)
        }

        fn jump(&self) {
            let Some(file) = self.selected_file() else {
                return;
            };
            self.obj().close();

            let file_selector = self
                .obj()
                .main_window()
                .file_selector(FileSelectorID::Active);
            file_selector.go_to_file(
                &file,
                self.obj()
                    .result_list()
                    .and_then(|result_list| result_list.connection()),
            );
            file_selector.grab_focus();
        }

        fn stop(&self) {
            self.stop_button.set_sensitive(false);
            self.obj().set_default_widget(Some(&self.find_button));
            if let Some(cancellable) = self.cancellable.replace(None) {
                cancellable.cancel();
            }
        }

        fn start_directory(&self, file: &gio::File) -> Directory {
            let connection = ConnectionList::get().find_by_file(file);
            Directory::new_from_file(&connection, file.clone())
        }

        async fn find(&self) {
            let Some(file) = self.dir_browser.borrow().file() else {
                return;
            };

            let start_dir = self.start_directory(&file);

            let Some(result_list) = self.result_list.borrow().clone() else {
                return;
            };

            if let Some(cancellable) = self.cancellable.replace(None) {
                cancellable.cancel();
            }
            let cancellable = gio::Cancellable::new();
            self.cancellable.replace(Some(cancellable.clone()));

            self.save_default_settings();
            let profile = self.config().default_profile().clone();

            let progress_bar = &self.progress_bar;
            let update_gui_timeout_id = glib::timeout_add_local(
                GeneralOptions::new().gui_update_rate.get(),
                glib::clone!(
                    #[weak]
                    progress_bar,
                    #[upgrade_or]
                    glib::ControlFlow::Break,
                    move || {
                        progress_bar.pulse();
                        glib::ControlFlow::Continue
                    }
                ),
            );

            self.status_label.set_text("");
            self.progress_bar.set_visible(true);
            self.jump_button.set_sensitive(false);
            self.stop_button.set_sensitive(true);
            self.find_button.set_sensitive(false);
            self.obj().set_default_widget(Some(&self.stop_button));

            self.virtual_directory.clear_files();
            result_list.clear();
            result_list.imp().set_connection(&start_dir.connection());
            result_list.set_base_dir(start_dir.local_path());

            let search_result = backend::search(
                &profile,
                &start_dir,
                &|message| match message {
                    SearchMessage::File(file) => {
                        self.virtual_directory.listen_to_file(&file);
                        self.virtual_directory.add_file(&file);
                        result_list.append_file(&file);
                    }
                    SearchMessage::Status(status) => self.status_label.set_text(&status),
                },
                &cancellable,
            )
            .await;

            update_gui_timeout_id.remove();

            let cancelled = self
                .cancellable
                .borrow()
                .as_ref()
                .is_some_and(gio::Cancellable::is_cancelled);
            self.search_finished(cancelled);

            if let Err(error) = search_result {
                error.show(self.obj().upcast_ref()).await;
            }
        }

        fn search_finished(&self, stopped: bool) {
            self.progress_bar.set_visible(false);

            let Some(result_list) = self.obj().result_list() else {
                return;
            };

            let count = result_list.size();

            let status = if stopped {
                ngettext(
                    "Found {} match — search aborted",
                    "Found {} matches — search aborted",
                    count as u32,
                )
            } else {
                ngettext("Found {} match", "Found {} matches", count as u32)
            }
            .replace("{}", count.to_string().as_str());

            self.status_label.set_label(&status);

            self.jump_button.set_sensitive(count > 0);
            self.stop_button.set_sensitive(false);
            self.find_button.set_sensitive(true);
            self.obj().set_default_widget(Some(&self.find_button));

            if count > 0 {
                result_list.grab_focus();
                if result_list.focused_file().is_none() {
                    result_list.select_row(0);
                }
            }
        }

        pub fn save_default_settings(&self) {
            let profile_component = self.obj().profile_component();
            let config = self.config();
            let default_profile = config.default_profile();

            profile_component.copy();

            let filename_pattern = default_profile.path_pattern();
            if !filename_pattern.is_empty() {
                config.add_name_pattern(&filename_pattern);
                profile_component.set_name_patterns_history(&config.name_patterns());
            }

            let text_pattern = default_profile.content_pattern();
            if !text_pattern.is_empty() {
                config.add_content_pattern(&text_pattern);
                if let Err(error) = ViewerOptions::new().add_to_history(&text_pattern, Mode::Text) {
                    eprintln!("{error}");
                }
                profile_component.set_content_patterns_history(&config.content_patterns());
            }
        }

        pub fn file_view(&self) {
            if let Some(file_list) = self.result_list.borrow().as_ref()
                && let Err(error) =
                    file_list.activate_action("fl.file-view", Some(&None::<bool>.to_variant()))
            {
                eprintln!("Cannot activate action `file-view`: {error}");
            }
        }

        pub fn file_internal_view(&self) {
            if let Some(file_list) = self.result_list.borrow().as_ref()
                && let Err(error) =
                    file_list.activate_action("fl.file-view", Some(&true.to_variant()))
            {
                eprintln!("Cannot activate action `file-view`: {error}");
            }
        }

        pub fn file_external_view(&self) {
            if let Some(file_list) = self.result_list.borrow().as_ref()
                && let Err(error) =
                    file_list.activate_action("fl.file-view", Some(&false.to_variant()))
            {
                eprintln!("Cannot activate action `file-view`: {error}");
            }
        }

        pub fn file_edit(&self) {
            if let Some(file_list) = self.result_list.borrow().as_ref()
                && let Err(error) = file_list.activate_action("fl.file-edit", None)
            {
                eprintln!("Cannot activate action `file-edit`: {error}");
            }
        }

        pub fn file_delete(&self, force: bool) {
            if let Some(file_list) = self.result_list.borrow().as_ref() {
                let file_list = file_list.clone();
                glib::spawn_future_local(async move { file_list.show_delete_dialog(force).await });
            }
        }
    }
}

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

impl SearchDialog {
    pub fn new(
        config: Rc<SearchConfig>,
        file_metadata_service: &FileMetadataService,
        main_window: &MainWindow,
    ) -> Self {
        let this: Self = glib::Object::builder()
            .property("file-metadata-service", file_metadata_service)
            .property("main-window", main_window)
            .build();
        this.imp().config.set(config.clone()).ok().unwrap();
        this.imp().update_profile_menu();

        main_window
            .shortcuts()
            .add_controller(&this, Area::MainWindow);
        main_window.shortcuts().add_controller(&this, Area::Panel);

        let profile_component = this.profile_component();
        profile_component.set_profile(Some(config.default_profile()));
        profile_component.set_name_patterns_history(&config.name_patterns());
        profile_component.set_content_patterns_history(&config.content_patterns());
        profile_component.grab_focus();

        this
    }

    pub fn show_and_set_focus(
        &self,
        start_dir: Option<&Directory>,
        transient_for: Option<&MainWindow>,
    ) {
        self.set_transient_for(transient_for);
        self.present();
        self.profile_component().grab_focus();

        self.profile_component().update();
        self.set_start_dir(start_dir);

        self.dir_browser()
            .set_file(start_dir.map(|d| d.file().clone()));
    }

    pub fn update_style(&self) {
        if let Some(fl) = self.result_list() {
            fl.update_style(self.imp().main_window.get());
        }
    }
}
