// Fragments - app.rs
// Copyright (C) 2022-2024  Felix Häcker <haeckerfelix@gnome.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::cell::{OnceCell, RefCell};
use std::path::PathBuf;
use std::rc::Rc;

use adw::prelude::*;
use adw::subclass::prelude::*;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use futures_util::FutureExt;
use glib::{clone, Properties, WeakRef};
use gtk::{gio, glib, FileFilter};
use smol::fs;

use crate::backend::{secret_store, FrgConnection, FrgConnectionManager};
use crate::i18n::i18n;
use crate::settings::{settings_manager, Key};
use crate::ui::{
    about_dialog, FrgAddConnectionDialog, FrgApplicationWindow, FrgPreferencesDialog,
    FrgStatsDialog,
};
use crate::{config, utils};

mod imp {
    use super::*;

    #[derive(Properties)]
    #[properties(wrapper_type = super::FrgApplication)]
    pub struct FrgApplication {
        #[property(get)]
        connection_manager: FrgConnectionManager,
        #[property(get, set)]
        search: RefCell<String>,
        #[property(get, set=Self::set_inhibit_suspend)]
        inhibit_suspend: RefCell<bool>,

        inhibit_cookie: RefCell<Option<u32>>,
        window: OnceCell<WeakRef<FrgApplicationWindow>>,
        pub settings: gio::Settings,
    }

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

        fn new() -> Self {
            Self {
                connection_manager: FrgConnectionManager::new(),
                search: RefCell::default(),
                inhibit_suspend: RefCell::default(),
                inhibit_cookie: RefCell::default(),
                window: OnceCell::default(),
                settings: gio::Settings::new(config::APP_ID),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for FrgApplication {}

    impl ApplicationImpl for FrgApplication {
        fn activate(&self) {
            debug!("Activate GIO Application...");
            let obj = self.obj();

            // If the window already exists,
            // present it instead creating a new one again.
            if let Some(weak_win) = self.window.get() {
                let window = weak_win.upgrade().unwrap();
                window.present();
                info!("Application window presented.");
                return;
            }

            // Connect to last used connection
            let fut = clone!(@weak self as this => async move {
                let cm = FrgConnectionManager::default();

                let local_connection = FrgConnection::default();
                cm.connections().insert(0, &local_connection);

                if let Err(err) = secret_store::open_keyring().await {
                    error!("Could not unlock keyring: {err}");
                };

                // Restore last used connection
                let uuid = settings_manager::string(Key::ClientLastConnection);
                this.set_connection(uuid).await;
            });
            glib::spawn_future_local(fut);

            // No window available -> we have to create one
            let window = FrgApplicationWindow::new(&self.obj());
            let _ = self.window.set(window.downgrade());
            window.present();
            info!("Created application window.");

            // Setup GActions
            let string_ty = Some(glib::VariantTy::new("s").unwrap());

            // app.add-torrent
            let action = gio::SimpleAction::new("add-torrent", None);
            action.connect_activate(clone!(@weak self as app => move |_, _| {
                app.open_torrent_filechooser();
            }));
            utils::bind_connected_property(&action);
            obj.add_action(&action);
            obj.set_accels_for_action("app.add-torrent", &["<primary>o"]);

            // app.add-link
            let action = gio::SimpleAction::new("add-link", string_ty);
            action.connect_activate(clone!(@weak self as this => move |_, target| {
                if let Some(torrent_link) = target.and_then(|x| x.get::<String>()) {
                    // Both magnet and torrent links are supported
                    this.add_torrent_by_link(torrent_link);
                }
            }));
            utils::bind_connected_property(&action);
            obj.add_action(&action);

            // app.search
            let action = gio::SimpleAction::new("search", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                window.show_search(false);
            }));
            utils::bind_connected_property(&action);
            obj.add_action(&action);
            obj.set_accels_for_action("app.search", &["<primary>f"]);

            // app.toggle-search
            let action = gio::SimpleAction::new("toggle-search", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                window.show_search(true);
            }));
            utils::bind_connected_property(&action);
            obj.add_action(&action);

            // app.resume-torrents
            let action = gio::SimpleAction::new("resume-torrents", None);
            action.connect_activate(clone!(@weak self as app => move |_, _| {
                let fut = async move {
                    let client = FrgConnectionManager::default().client();
                    if let Err(err) = client.start_torrents().await {
                        debug!("Could not resume torrents: {:?}", err);
                    };
                };
                glib::spawn_future_local(fut);
            }));
            obj.add_action(&action);

            // app.pause-torrents
            let action = gio::SimpleAction::new("pause-torrents", None);
            action.connect_activate(clone!(@weak self as app => move |_, _| {
                let fut = async move {
                    let client = FrgConnectionManager::default().client();
                    if let Err(err) = client.stop_torrents().await {
                        debug!("Could not pause torrents: {:?}", err);
                    };
                };
                glib::spawn_future_local(fut);

            }));
            obj.add_action(&action);

            // app.remove-torrents
            let action = gio::SimpleAction::new("remove-torrents", None);
            action.connect_activate(clone!(@weak self as this => move |_, _| {
                this.show_remove_torrents_dialog();
            }));
            obj.add_action(&action);

            // app.show-stats
            let action = gio::SimpleAction::new("show-stats", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                FrgStatsDialog::new().present(&window);
            }));
            utils::bind_connected_property(&action);
            obj.add_action(&action);

            // app.set-connection
            let action = gio::SimpleAction::new("set-connection", string_ty);
            action.connect_activate(clone!(@weak self as this => move |_, target| {
                if let Some(connection_uuid) = target.and_then(|x| x.get::<String>()) {
                    let fut = async move {
                        this.set_connection(connection_uuid).await;
                    };
                    glib::spawn_future_local(fut);
                }
            }));
            obj.add_action(&action);

            // app.reconnect
            let action = gio::SimpleAction::new("reconnect", None);
            action.connect_activate(clone!(@weak self as this => move |_, _| {
                let cm = FrgConnectionManager::default();
                this.obj().activate_action("set-connection", Some(&cm.current_connection().uuid().to_variant()));
            }));
            obj.add_action(&action);

            // app.add-remote-connection
            let action = gio::SimpleAction::new("add-remote-connection", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                let dialog = FrgAddConnectionDialog::new();
                dialog.present(&window);
            }));
            obj.add_action(&action);

            // app.show-preferences
            let action = gio::SimpleAction::new("show-preferences", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                FrgPreferencesDialog::default().present(&window);
            }));
            obj.set_accels_for_action("app.show-preferences", &["<primary>comma"]);
            obj.add_action(&action);

            // app.about
            let action = gio::SimpleAction::new("about", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                about_dialog::show(&window);
            }));
            obj.add_action(&action);

            // app.quit
            let action = gio::SimpleAction::new("quit", None);
            action.connect_activate(clone!(@weak window => move |_, _| {
                window.close();
            }));
            obj.set_accels_for_action("app.quit", &["<primary>q"]);
            obj.add_action(&action);

            obj.set_accels_for_action("window.close", &["<primary>w"]);

            // Update state of `pause-torrents` and `remove-torrents` action
            let stats = FrgConnectionManager::default().client().session_stats();
            stats.connect_active_torrent_count_notify(clone!(@weak self as this => move |_|{
                this.obj().update_inhibit_state();
            }));
            stats.connect_downloaded_torrent_count_notify(clone!(@weak self as this => move |_|{
                this.update_session_gactions();
                this.obj().update_inhibit_state();
            }));
            stats.connect_paused_torrent_count_notify(clone!(@weak self as this => move |_|{
                this.update_session_gactions();
                this.obj().update_inhibit_state();
            }));
            stats.connect_torrent_count_notify(clone!(@weak self as this => move |_|{
                this.update_session_gactions();
            }));

            let client = FrgConnectionManager::default().client();
            client.connect_is_connected_notify(clone!(@weak self as this => move |_|{
                this.update_session_gactions();
            }));

            self.update_session_gactions();
        }

        fn open(&self, files: &[gio::File], _hint: &str) {
            let application = self.obj();
            application.activate();
            debug!("Application {} handling files.", config::APP_ID);

            let client = self.connection_manager.client();

            if client.is_connected() {
                application.add_torrents_from_files(&files.to_vec());
            } else {
                // Wait till we're connected with something to add the files
                let signal_id: Rc<RefCell<Option<glib::SignalHandlerId>>> = Rc::default();
                let files = files.to_vec().clone();
                *signal_id.borrow_mut() = Some(client.connect_is_connected_notify(
                    clone!(@weak application, @strong signal_id, @strong files => move |client| {
                        if client.is_connected() {
                            application.add_torrents_from_files(&files);

                            // Disconnect from the signal, we only need it one time
                            let signal_id = signal_id.borrow_mut().take().unwrap();
                            glib::object::ObjectExt::disconnect(client, signal_id);
                        }
                    }),
                ));
            }
        }
    }

    impl GtkApplicationImpl for FrgApplication {}

    impl AdwApplicationImpl for FrgApplication {}

    impl FrgApplication {
        fn set_inhibit_suspend(&self, inhibit: bool) {
            let mut inhibit_cookie = self.inhibit_cookie.borrow_mut();

            if inhibit_cookie.is_some() && !inhibit {
                debug!("Uninhibit suspend");
                self.obj().uninhibit(inhibit_cookie.take().unwrap());
            } else if inhibit && inhibit_cookie.is_none() {
                debug!("Inhibit suspend");
                let window = FrgApplicationWindow::default();

                let cookie = self.obj().inhibit(
                    Some(&window),
                    gtk::ApplicationInhibitFlags::SUSPEND,
                    Some(&i18n("Downloading data…")),
                );
                *inhibit_cookie = Some(cookie);
            }
        }

        /// Disable the `pause-torrents` or `remove-torrents` if there's no
        /// connection or there are no torrents available to apply the
        /// action eg. no 100% downloaded torrents available -> disable
        /// `remove-torrents` action
        fn update_session_gactions(&self) {
            let client = FrgConnectionManager::default().client();
            let stats = client.session_stats();
            let obj = self.obj();

            let resume_torrents_action: gio::SimpleAction = obj
                .lookup_action("resume-torrents")
                .unwrap()
                .downcast()
                .unwrap();

            let pause_torrents_action: gio::SimpleAction = obj
                .lookup_action("pause-torrents")
                .unwrap()
                .downcast()
                .unwrap();

            let remove_torrents_action: gio::SimpleAction = obj
                .lookup_action("remove-torrents")
                .unwrap()
                .downcast()
                .unwrap();

            if !client.is_connected() {
                resume_torrents_action.set_enabled(false);
                pause_torrents_action.set_enabled(false);
                remove_torrents_action.set_enabled(false);
            } else {
                let total_torrent_count = stats.torrent_count();
                let paused_torrent_count = stats.paused_torrent_count();
                let downloaded_torrent_count = stats.downloaded_torrent_count();

                let resume_action = paused_torrent_count != 0;
                resume_torrents_action.set_enabled(resume_action);

                let pause_action = paused_torrent_count != total_torrent_count;
                pause_torrents_action.set_enabled(pause_action);

                let remove_action = downloaded_torrent_count != 0;
                remove_torrents_action.set_enabled(remove_action);
            }
        }

        async fn set_connection(&self, connection_uuid: String) {
            let window = FrgApplicationWindow::default();
            window.set_connection(connection_uuid).await;
        }

        pub fn add_torrent_by_file(&self, file: &PathBuf) {
            debug!("Add torrent by file: {:?}", file);

            if let Some(ext) = file.extension() {
                if ext != "torrent" {
                    warn!("Unsupported file extension: {:?}", ext);
                    utils::inapp_notification(&i18n("Unsupported file format"), gtk::Widget::NONE);
                    return;
                }
            }

            let fut = clone!(@strong file => async move {
                match fs::read(&file).await {
                    Ok(content) => {
                        let encoded = STANDARD.encode(content);

                        let cm = FrgConnectionManager::default();
                        let res = cm.add_torrent_by_metainfo(encoded).await;
                        if let Err(err) = res {
                            debug!("Could not add file torrent: {:?}", err);
                            utils::inapp_notification("Unable to add file torrent", gtk::Widget::NONE);
                        }

                        if settings_manager::boolean(Key::TrashOriginalTorrentFiles){
                            let gfile = gio::File::for_path(file);
                            if let Err(err) = gfile.trash_future(glib::Priority::DEFAULT).await {
                                debug!("Unable to trash torrent file: {:?}", err);
                                utils::inapp_notification("Unable to trash torrent file", gtk::Widget::NONE);
                            }
                        }
                    }
                    Err(err) => {
                        debug!("Unable to read file content: {:?}", err);
                        utils::inapp_notification("Unable to read file content", gtk::Widget::NONE);
                    }
                }
            });

            glib::spawn_future_local(fut);
        }

        pub fn add_torrent_by_link(&self, link: String) {
            debug!("Add torrent by link: {}", link);
            let fut = async move {
                let cm = FrgConnectionManager::default();
                cm.add_torrent_by_filename(link)
                    .map(move |result| match result {
                        Ok(_) => (),
                        Err(err) => {
                            debug!("Could not add torrent: {:?}", err);
                            utils::inapp_notification("Unable to add torrent", gtk::Widget::NONE);
                        }
                    })
                    .await;
            };

            glib::spawn_future_local(fut);
        }

        fn open_torrent_filechooser(&self) {
            let window = FrgApplicationWindow::default();
            let dialog = gtk::FileDialog::new();
            dialog.set_title(&i18n("Open Torrents"));
            dialog.set_accept_label(Some(&i18n("_Open")));

            // Set a filter to only show torrent files
            let filter = FileFilter::new();
            FileFilter::set_name(&filter, Some(&i18n("Torrent files")));
            filter.add_mime_type("application/x-bittorrent");

            // Set a filter to show all files
            let all_files_filter = FileFilter::new();
            FileFilter::set_name(&all_files_filter, Some(&i18n("All files")));
            all_files_filter.add_pattern("*");

            let filters = gio::ListStore::new::<gtk::FileFilter>();
            filters.append(&filter);
            filters.append(&all_files_filter);

            dialog.set_filters(Some(&filters));

            dialog.open_multiple(
                Some(&window),
                gio::Cancellable::NONE,
                clone!(@strong dialog, @weak self as this => move |result| {
                    if let Ok(files) = result {
                        let files = files
                            .snapshot()
                            .iter()
                            .map(|o| o.downcast_ref::<gio::File>().unwrap().clone())
                            .collect();
                        this.obj().add_torrents_from_files(&files);
                    }
                }),
            );
        }

        fn show_remove_torrents_dialog(&self) {
            let window = self.obj().active_window().unwrap();

            // NOTE: Title of the modal when the user removes all downloaded torrents
            let heading = i18n("Remove all Downloaded Torrents?");
            // NOTE: Text displayed in the modal when the user removes all downloaded
            // torrents
            let body = i18n(
                "This will only remove torrents from Fragments, but will keep the downloaded content.",
            );

            let dialog = adw::MessageDialog::new(Some(&window), Some(&heading), Some(&body));

            dialog.add_response("cancel", &i18n("_Cancel"));
            dialog.set_default_response(Some("cancel"));
            dialog.add_response("remove", &i18n("_Remove"));
            dialog.set_response_appearance("remove", adw::ResponseAppearance::Destructive);

            dialog.connect_response(None, move |dialog, response| {
                dialog.destroy();
                if response == "remove" {
                    let fut = async move {
                        let client = FrgConnectionManager::default().client();
                        if let Err(err) = client.remove_torrents(true, false).await {
                            debug!("Could not remove torrents: {:?}", err);
                        };
                    };
                    glib::spawn_future_local(fut);
                }
            });

            dialog.present();
        }
    }
}

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

impl FrgApplication {
    pub fn run() {
        debug!(
            "{} ({}) ({}) - Version {} ({})",
            config::NAME,
            config::APP_ID,
            config::VCS_TAG,
            config::VERSION,
            config::PROFILE
        );

        // Create new GObject and downcast it into FrgApplication
        let app = glib::Object::builder::<FrgApplication>()
            .property("application-id", config::APP_ID)
            .property("flags", gio::ApplicationFlags::HANDLES_OPEN)
            .property("resource-base-path", config::PATH_ID)
            .build();

        // Start running gtk::Application
        app.run();
    }

    /// Inhibit standby/suspend when there are active torrents
    /// (and `InhibitSuspendActiveTorrents` setting is enabled)
    pub fn update_inhibit_state(&self) {
        debug!("Update inhibit state...");

        let client = FrgConnectionManager::default().client();
        let stats = client.session_stats();

        let total_torrent_count = stats.torrent_count();
        let paused_torrent_count = stats.paused_torrent_count();
        let downloaded_torrent_count = stats.downloaded_torrent_count();

        let inhibit = settings_manager::boolean(Key::InhibitSuspendActiveTorrents)
            && (total_torrent_count - paused_torrent_count - downloaded_torrent_count) != 0;

        self.set_inhibit_suspend(inhibit);
    }

    pub fn add_torrents_from_files(&self, files: &Vec<gio::File>) {
        for file in files {
            if let Some(path) = file.path() {
                // Local file
                self.imp().add_torrent_by_file(&path);
            } else {
                // URI, means magnet or torrent link
                let uri = file.uri().to_string();
                self.imp().add_torrent_by_link(uri);
            }
        }
    }
}

impl Default for FrgApplication {
    fn default() -> Self {
        gio::Application::default()
            .expect("Could not get default GApplication")
            .downcast()
            .unwrap()
    }
}
