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

use adw::prelude::*;
use adw::subclass::prelude::*;
use gettextrs::gettext;
use smol::lock::{RwLockReadGuard, RwLockWriteGuard};
use tracing::{info, warn};

use std::{
    iter::{self, repeat},
    path::PathBuf,
};

use crate::{
    config::{APP_ID, PKGDATADIR, PROFILE, VERSION},
    utils,
    widgets::Window,
};

use gtk::{gio, glib};

use gio::ListStore;
use textpieces_core::{collection, providers::Scripts, Actions};

mod imp {
    use super::*;

    use adw::StyleManager;
    use derive_debug::Dbg;
    use gtk::{gdk::Display, gio::File, glib::clone, CssProvider};
    use pollster::block_on;
    use smol::lock::RwLock;
    use sourceview::{Style, StyleSchemeManager};
    use textpieces_core::StopHandle;

    /// Private part of [`Application`].
    ///
    /// [`Application`]: super::Application
    #[derive(Dbg)]
    pub struct Application {
        /// Application settings.
        pub settings: utils::Settings,

        /// Built-in actions.
        #[dbg(placeholder = "RwLock(<built-in actions>)")]
        pub builtin_actions: RwLock<Actions<collection::Provider>>,

        /// Custom actions.
        pub custom_actions: RwLock<Actions<Scripts>>,

        /// A handle that allows to stop all running built-in actions.
        #[dbg(placeholder = "<stop handle for built-in actions>")]
        pub builtin_stop_handle: StopHandle<collection::Provider>,

        /// A handle that allows to stop all running custom actions.
        pub custom_stop_handle: StopHandle<Scripts>,

        /// Action list model.
        pub action_list: ListStore,
    }

    impl Default for Application {
        fn default() -> Self {
            let settings = utils::Settings::new(APP_ID);

            let builtin_actions = block_on(Actions::new(collection::provider())).unwrap();
            let custom_actions = block_on(Actions::new(Scripts::new(
                super::Application::script_dir(),
                utils::is_flatpak().then_some(&["flatpak-spawn", "--host", "--watch-bus"]),
            )))
            .unwrap();

            let action_list = ListStore::new::<utils::ActionModel>();

            Self {
                settings,

                builtin_stop_handle: builtin_actions.stop_handle(),
                custom_stop_handle: custom_actions.stop_handle(),

                builtin_actions: RwLock::new(builtin_actions),
                custom_actions: RwLock::new(custom_actions),

                action_list,
            }
        }
    }

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

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

            self.obj().add_main_option(
                "new-window",
                glib::Char::from(b'w'),
                glib::OptionFlags::NONE,
                glib::OptionArg::None,
                &gettext("Open a new window"),
                None,
            );
        }
    }

    impl ApplicationImpl for Application {
        fn handle_local_options(
            &self,
            options: &glib::VariantDict,
        ) -> std::ops::ControlFlow<glib::ExitCode> {
            let app = self.obj();

            if options.contains("new-window") {
                if let Err(err) = app.register(None::<&gio::Cancellable>) {
                    warn!("Failed to register the application: {err}");
                }

                if app.is_remote() {
                    app.activate_action("new-window", None);
                    return std::ops::ControlFlow::Break(glib::ExitCode::SUCCESS);
                }
            }

            self.parent_handle_local_options(options)
        }

        /// Activation method.
        ///
        /// Called every time when
        /// user tries to start the application.
        fn activate(&self) {
            self.parent_activate();

            // Open new window when user start the
            // application or raise the existing one
            if let Some(window) = self.obj().active_window() {
                window.present();
            } else {
                self.new_window();
            }
        }

        /// Startup method.
        ///
        /// Called only once during application startup.
        fn startup(&self) {
            self.parent_startup();

            // Register GTypes
            Self::register_gtypes();

            // Bind settings
            self.bind_settings();

            // Set application icon
            gtk::Window::set_default_icon_name(APP_ID);

            let app = self.obj();

            // Setup accelerators (shortcuts)
            Self::setup_accels(&app);

            // Setup application actions
            Self::setup_gactions(&app);

            // Update action list model
            let app = self.obj().to_owned();
            utils::spawn_async_local(async move {
                app.update_actions_model().await;
            });
        }

        /// Open method.
        ///
        /// Called when user tries to open files with the app.
        fn open(&self, files: &[File], _hint: &str) {
            for file in files {
                let win = Window::new(&self.obj());
                let file = file.to_owned();

                utils::spawn_async_local(async move {
                    win.load_file(Some(&file)).await;
                    win.present();
                });
            }
        }

        /// Shutdown method.
        ///
        /// Called when the application is about to quit.
        /// Mainloop is already stopped at this point.
        fn shutdown(&self) {
            if let Err(err) = self.builtin_stop_handle.stop() {
                glib::g_warning!(
                    "textpieces",
                    "Failed to stop running built-in actions: {err}"
                );
            }
            if let Err(err) = self.custom_stop_handle.stop() {
                glib::g_warning!("textpieces", "Failed to stop running custom actions: {err}");
            }

            self.parent_shutdown();
        }
    }

    impl GtkApplicationImpl for Application {}
    impl AdwApplicationImpl for Application {}

    impl Application {
        /// Opens new application window.
        fn new_window(&self) {
            let win = Window::new(&self.obj());

            win.present();
        }

        /// Registers `GType`s used in templates.
        fn register_gtypes() {
            use crate::widgets;

            sourceview::init();
            widgets::Editor::ensure_type();
            widgets::ActionSearch::ensure_type();
            widgets::DragOverlay::ensure_type();
            widgets::SearchBar::ensure_type();
            widgets::SearchEntry::ensure_type();
        }

        /// Setups accelerators for `GAction`s.
        fn setup_accels(app: &super::Application) {
            let accels = [
                ("app.quit", &["<Ctrl>q"]),
                ("app.new-window", &["<Ctrl>n"]),
                ("window.close", &["<Ctrl>w"]),
                ("win.load-file", &["<Ctrl>o"]),
                ("win.save-as", &["<Ctrl>s"]),
                ("win.open-preferences", &["<Ctrl>comma"]),
                ("search.show", &["<Ctrl>f"]),
                ("search.show-replace", &["<Ctrl>h"]),
                ("search.move-next", &["<Ctrl>g"]),
                ("search.move-previous", &["<Ctrl><Shift>g"]),
            ];

            for (action, accels) in accels {
                app.set_accels_for_action(action, accels);
            }
        }

        /// Setups `GAction`s.
        fn setup_gactions(app: &super::Application) {
            let actions: utils::GActions<super::Application> = &[
                ("quit", |app| {
                    // Close active application window to save its state.
                    // The problem is that the active window could be a modal window,
                    // so we closes the active window and then its parents recursively.
                    for window in iter::successors(app.active_window(), |win| win.transient_for()) {
                        window.close();
                    }

                    // Stop the application
                    app.quit()
                }),
                ("new-window", |app| app.imp().new_window()),
            ];

            app.add_action_entries(utils::to_entries(actions, app))
        }

        /// Binds real meaning to settings keys.
        fn bind_settings(&self) {
            let css_provider = CssProvider::new();
            gtk::style_context_add_provider_for_display(
                &Display::default().expect("The default display should exist"),
                &css_provider,
                gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
            );

            Self::update_styling(&css_provider, &self.settings);

            self.settings.connect_changed(
                None,
                clone!(
                    #[strong]
                    css_provider,
                    move |settings, _| {
                        Self::update_styling(&css_provider, &settings.to_owned().into());
                    }
                ),
            );

            let settings = self.settings.clone();
            self.obj().style_manager().connect_dark_notify(move |_| {
                Self::update_styling(&css_provider, &settings);
            });
        }

        /// Updates styling from settings.
        fn update_styling(css_provider: &CssProvider, settings: &utils::Settings) {
            let style_manager = StyleManager::default();

            // Set color scheme from settings
            style_manager.set_color_scheme(settings.color_scheme().into());

            // Get background color of `sourceview::View`.
            // Used to make the window use the same background
            // as the editor.
            let bg_color = {
                let is_dark = style_manager.is_dark();

                let scheme_manager = StyleSchemeManager::default();
                let scheme_id = match is_dark {
                    false => "Adwaita",
                    true => "Adwaita-dark",
                };
                let editor_scheme = scheme_manager
                    .scheme(scheme_id)
                    .expect("Default editor style scheme is not found");

                editor_scheme
                    .style("text")
                    .as_ref()
                    .and_then(Style::background)
                    .map(String::from)
                    .unwrap_or_else(|| {
                        glib::g_warning!(
                            "textpieces",
                            "somehow used `GtkSourceView` theme doesn't have default text color"
                        );
                        String::from("var(--window-fg-color)")
                    })
            };
            let font_family = settings.font_family();
            let font_size = settings.font_size();

            // Set CSS to apply font settings and window background
            css_provider.load_from_string(&format!(
                r#"
                    :root {{
                        --monospace-font-family: "{font_family}";
                        --monospace-font-size: {font_size}pt;
                        --editor-bg-color: {bg_color};
                    }}
                "#
            ));
        }
    }
}

glib::wrapper! {
    /// Text Pieces application.
    pub struct Application(ObjectSubclass<imp::Application>)
        @extends gio::Application, gtk::Application, adw::Application,
        @implements gio::ActionMap, gio::ActionGroup;
}

impl Default for Application {
    fn default() -> Self {
        glib::Object::builder()
            .property("application-id", APP_ID)
            .property("flags", gio::ApplicationFlags::HANDLES_OPEN)
            .property("resource-base-path", "/io/gitlab/liferooter/TextPieces/")
            .build()
    }
}

impl Application {
    /// Runs the application.
    pub fn run(&self) -> glib::ExitCode {
        info!("Text Pieces ({})", APP_ID);
        info!("Version: {} ({})", VERSION, PROFILE);
        info!("Datadir: {}", PKGDATADIR);

        ApplicationExtManual::run(self)
    }

    /// Returns reference to built-in actions.
    pub async fn builtin_actions(&'_ self) -> RwLockReadGuard<'_, Actions<collection::Provider>> {
        self.imp().builtin_actions.read().await
    }

    /// Returns reference to custom actions.
    pub async fn custom_actions(&'_ self) -> RwLockReadGuard<'_, Actions<Scripts>> {
        self.imp().custom_actions.read().await
    }

    /// Returns mutable reference to custom actions.
    pub async fn custom_actions_mut(&'_ self) -> RwLockWriteGuard<'_, Actions<Scripts>> {
        self.imp().custom_actions.write().await
    }

    /// Returns action list model.
    pub fn action_list() -> ListStore {
        Application::get().imp().action_list.to_owned()
    }

    /// Returns application instance.
    pub fn get() -> Self {
        gio::Application::default()
            .and_downcast()
            .expect("Default `GApplication` is not set yet")
    }

    /// Returns application settings.
    pub fn settings() -> utils::Settings {
        Self::get().imp().settings.to_owned()
    }

    /// Updates actions list model.
    pub async fn update_actions_model(&self) {
        let builtin_actions = self.builtin_actions().await.actions().clone();
        let custom_actions = self.custom_actions().await.actions().clone();
        let action_list = self.imp().action_list.to_owned();

        action_list.remove_all();

        [(builtin_actions, true), (custom_actions, false)]
            .into_iter()
            .flat_map(|(actions, is_builtin)| actions.into_iter().zip(repeat(is_builtin)))
            .for_each(|((id, info), is_builtin)| {
                action_list.append(&utils::ActionModel::new(
                    &id,
                    &info.name,
                    &info.description,
                    is_builtin,
                ))
            });
    }

    /// Returns path to script directory.
    pub fn script_dir() -> PathBuf {
        PathBuf::from_iter([
            dirs::data_dir()
                .expect("Failed to get data directory")
                .as_os_str(),
            "textpieces".as_ref(),
        ])
    }
}
