// Fragments - actions.rs
// Copyright (C) 2023-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::fs::File;
use std::os::fd::AsFd;
use std::path::{Path, PathBuf};

use adw::prelude::*;
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest};
use ashpd::WindowIdentifier;
use gio::{SimpleAction, SimpleActionGroup};
use glib::clone;
use gtk::{gdk, gio, glib};
use transmission_gobject::{TrFile, TrTorrent, TrTorrentStatus};

use crate::backend::FrgConnectionManager;
use crate::i18n::i18n;
use crate::ui::FrgTorrentDialog;
use crate::utils;

pub fn install_torrent_actions(torrent: &TrTorrent, widget: gtk::Widget) {
    let current_connection = FrgConnectionManager::default().current_connection();
    let actions = SimpleActionGroup::new();
    widget.insert_action_group("torrent", Some(&actions));

    // torrent.continue
    let continue_action = SimpleAction::new("continue", None);
    continue_action.connect_activate(clone!(@weak torrent => move |_, _| {
        let fut = async move {
            torrent.start(false).await.expect("Unable to start torrent");
        };
        glib::spawn_future_local(fut);
    }));
    actions.add_action(&continue_action);

    // torrent.pause
    let pause_action = SimpleAction::new("pause", None);
    pause_action.connect_activate(clone!(@weak torrent => move |_, _| {
        let fut = async move {
            torrent.stop().await.expect("Unable to stop torrent");
        };
        glib::spawn_future_local(fut);
    }));
    actions.add_action(&pause_action);

    // Disable `continue`/`pause` action depending on the torrent status
    torrent.connect_status_notify(
        clone!(@weak torrent, @weak continue_action, @weak pause_action => move |_|{
            update_status_action(&torrent, &continue_action, &pause_action);
        }),
    );
    update_status_action(torrent, &continue_action, &pause_action);

    // torrent.remove
    let remove_action = SimpleAction::new("remove", None);
    remove_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
        show_remove_torrent_dialog(&torrent, widget);
    }));
    actions.add_action(&remove_action);

    // torrent.copy-magnet
    let copy_magnet_action = SimpleAction::new("copy-magnet", None);
    copy_magnet_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
        let display = gdk::Display::default().unwrap();
        let clipboard = display.clipboard();

        let content = gdk::ContentProvider::for_value(&torrent.magnet_link().to_value());
        if let Err(err) = clipboard.set_content(Some(&content)){
            utils::inapp_notification(&i18n("Unable to copy magnet link to clipboard"), Some(&widget));
            warn!("Unable to copy magnet link to clipboard: {}", err.to_string());
        }

        utils::inapp_notification(&i18n("Copied magnet link to clipboard"), Some(&widget));
    }));
    actions.add_action(&copy_magnet_action);

    // torrent.manual-update
    let manual_update_action = SimpleAction::new("manual-update", None);
    manual_update_action.connect_activate(clone!(@weak torrent => move |_, _| {
        let fut = async move {
            torrent.reannounce().await.expect("Unable to reannounce torrent");
        };
        glib::spawn_future_local(fut);
    }));
    actions.add_action(&manual_update_action);

    // torrent.queue-up
    let queue_up_action = SimpleAction::new("queue-up", None);
    queue_up_action.connect_activate(clone!(@weak torrent => move |_, _| {
        move_queue(false, torrent);
    }));
    actions.add_action(&queue_up_action);

    // torrent.queue-down
    let queue_down_action = SimpleAction::new("queue-down", None);
    queue_down_action.connect_activate(clone!(@weak torrent => move |_, _| {
        move_queue(true, torrent);
    }));
    actions.add_action(&queue_down_action);

    // Disable queue actions for non queued torrents
    torrent.connect_status_notify(
        clone!(@weak torrent, @weak queue_up_action, @weak queue_down_action => move |_|{
            update_queue_action(&torrent, &queue_up_action, &queue_down_action);
        }),
    );
    update_queue_action(torrent, &queue_up_action, &queue_down_action);

    // torrent.show-dialog
    let show_dialog_action = SimpleAction::new("show-dialog", None);
    show_dialog_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
        let dialog = FrgTorrentDialog::new(&torrent);
        dialog.present(&widget);
    }));
    actions.add_action(&show_dialog_action);

    // torrent.open
    let open_action = SimpleAction::new("open", None);
    open_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
        let name = if let Some(top_level) = torrent.files().top_level() {
            top_level.name()
        } else {
            warn!("Top level file is not available, falling back to torrent name for opening");
            torrent.name()
        };

        let path = Path::new(&torrent.download_dir()).join(name);
        open(path, &widget.native().unwrap(), false, Some(widget));
    }));
    actions.add_action(&open_action);

    // Only downloaded torrents can be opened
    torrent.connect_downloaded_notify(clone!(@weak torrent, @weak open_action => move |_|{
        update_torrent_open_action(&torrent, &open_action);
    }));
    update_torrent_open_action(torrent, &open_action);

    // torrent.set-location
    let set_location_action = SimpleAction::new("set-location", None);
    set_location_action.connect_activate(clone!(@weak torrent, @weak widget => move |_, _| {
            show_set_torrent_location(&torrent, &widget);
        }
    ));
    set_location_action.set_enabled(current_connection.is_fragments());
    actions.add_action(&set_location_action);

    // Disable filesystem actions when connected with a remote session
    current_connection.connect_is_fragments_notify(
        clone!(@weak torrent, @weak open_action, @weak set_location_action => move |c|{
            set_location_action.set_enabled(c.is_fragments());
            update_torrent_open_action(&torrent, &open_action);
        }),
    );
}

pub fn install_file_actions(file: &TrFile, widget: gtk::Widget) {
    let current_connection = FrgConnectionManager::default().current_connection();

    let actions = SimpleActionGroup::new();
    widget.insert_action_group("file", Some(&actions));

    // file.open
    let open_action = SimpleAction::new("open", None);
    open_action.connect_activate(clone!(@weak file, @weak widget => move |_, _| {
        let path = Path::new(&file.torrent().download_dir()).join(file.name());
        open(path, &widget.native().unwrap(), false, Some(widget));
    }));
    actions.add_action(&open_action);

    // file.open-containing-folder
    let open_containing_action = SimpleAction::new("open-containing-folder", None);
    open_containing_action.connect_activate(clone!(@weak file, @weak widget => move |_, _| {
        let path = Path::new(&file.torrent().download_dir()).join(file.name());
        open(path, &widget.native().unwrap(), true, Some(widget));
    }));
    actions.add_action(&open_containing_action);

    // Only downloaded files can be opened
    file.connect_bytes_completed_notify(clone!(@weak file, @weak actions => move |_|{
        update_file_open_actions(&file, &actions);
    }));
    update_file_open_actions(file, &actions);

    // Disable filesystem actions when connected with a remote session
    current_connection.connect_is_fragments_notify(
        clone!(@weak file, @weak open_action, @weak actions => move |c|{
            open_containing_action.set_enabled(c.is_fragments());
            update_file_open_actions(&file, &actions);
        }),
    );
}

fn move_queue(down: bool, torrent: TrTorrent) {
    let fut = clone!(@weak torrent => async move {
        let pos = if down{
            torrent.download_queue_position() + 1
        }else{
            torrent.download_queue_position() - 1
        };
        torrent.set_download_queue_position(pos).await.expect("Unable to set download queue position");
    });
    glib::spawn_future_local(fut);
}

fn update_status_action(
    torrent: &TrTorrent,
    continue_action: &SimpleAction,
    pause_action: &SimpleAction,
) {
    match torrent.status() {
        TrTorrentStatus::Stopped => {
            continue_action.set_enabled(true);
            pause_action.set_enabled(false);
        }
        _ => {
            continue_action.set_enabled(false);
            pause_action.set_enabled(true);
        }
    }
}

fn update_queue_action(
    torrent: &TrTorrent,
    queue_up_action: &SimpleAction,
    queue_down_action: &SimpleAction,
) {
    let enable = matches!(torrent.status(), TrTorrentStatus::DownloadWait);
    queue_up_action.set_enabled(enable);
    queue_down_action.set_enabled(enable);
}

fn update_torrent_open_action(torrent: &TrTorrent, action: &SimpleAction) {
    let current_connection = FrgConnectionManager::default().current_connection();
    let is_fragments = current_connection.is_fragments();
    let is_downloaded = torrent.size() == torrent.downloaded();

    action.set_enabled(is_fragments && is_downloaded);
}

fn update_file_open_actions(file: &TrFile, actions: &SimpleActionGroup) {
    let current_connection = FrgConnectionManager::default().current_connection();
    let is_fragments = current_connection.is_fragments();
    let is_downloaded = file.length() == file.bytes_completed();

    for action_name in actions.list_actions() {
        let action: SimpleAction = actions
            .lookup_action(&action_name)
            .unwrap()
            .downcast()
            .unwrap();
        action.set_enabled(is_fragments && is_downloaded);
    }
}

fn show_remove_torrent_dialog(torrent: &TrTorrent, parent: gtk::Widget) {
    let heading = i18n("Remove Torrent?");
    let body =
        i18n("Once removed, continuing the transfer will require the torrent file or magnet link.");

    let dialog = adw::AlertDialog::new(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);

    // Check button
    let check_button = gtk::CheckButton::with_label(&i18n("Remove downloaded data as well"));
    check_button.set_halign(gtk::Align::Center);
    dialog.set_extra_child(Some(&check_button));

    dialog.connect_response(None,
        clone!(@strong dialog, @weak torrent, @weak parent, @weak check_button => move |_ , resp| {
            if resp == "remove" {
                let fut = async move {
                    torrent.remove(check_button.is_active()).await.expect("Unable to remove torrent");
                };

                if let Ok(dialog) = parent.downcast::<adw::Dialog>(){
                    dialog.close();
                }

                glib::spawn_future_local(fut);
            }

            dialog.close();
        }),
    );

    dialog.present(&parent);
}

// TODO: Consider rewriting this as async to get rid of the nested closures
fn show_set_torrent_location(torrent: &TrTorrent, parent: &gtk::Widget) {
    let title = i18n("Set Torrent Location");

    let dialog = gtk::FileDialog::new();
    dialog.set_title(&title);
    dialog.set_accept_label(Some(&i18n("_Select")));

    let root = parent.root().unwrap().downcast::<gtk::Window>().unwrap();
    dialog.select_folder(Some(&root), gio::Cancellable::NONE, clone!(@weak torrent, @weak parent, @strong dialog => move |result| {
        match result {
            Ok(folder) => {
                debug!("Selected torrent directory: {:?}", folder.path());

                if torrent.error() == 0 {
                    let heading = i18n("Move Data?");
                    let body = i18n("In order to continue using the torrent, the data must be available at the new location.");

                    let dialog = adw::AlertDialog::new(Some(&heading), Some(&body));
                    dialog.add_response("keep-data", &i18n("_Only Change Location"));
                    dialog.add_response("move-data", &i18n("_Move Data to New Location"));
                    dialog.set_default_response(Some("move-data"));
                    dialog.set_response_appearance("move-data", adw::ResponseAppearance::Suggested);

                    dialog.present(&parent);
                    dialog.connect_response(
                        None,
                        clone!(@strong dialog, @strong torrent, @strong folder => move |_ , resp| {
                            let resp = resp.to_string();
                            let fut = clone!(@strong resp, @strong torrent, @strong folder => async move {
                                let _ = torrent.set_location(folder, resp == "move-data").await;
                            });

                            glib::spawn_future_local(fut);
                        }),
                    );
                } else {
                    let fut = async move {
                        // If there is a problem with the torrent, it is most likely because the local
                        // downloaded file is no longer available. Therefore, do not offer the option
                        // to move the data in the first place.
                        let _ = torrent.set_location(folder, false).await;
                        // Automatically start torrent again so the error message disappears (hopefully)
                        let _ = torrent.start(false).await;
                    };
                    glib::spawn_future_local(fut);
                }
            }
            Err(err) => {
                warn!("Selected directory could not be accessed {err}");
            }
        }
        }));
}

fn open(
    path: PathBuf,
    native: &gtk::Native,
    open_containing_folder: bool,
    widget: Option<gtk::Widget>,
) {
    let fut = clone!(@strong path, @weak native, @strong open_containing_folder, @strong widget => async move {
        debug!("Opening: {:?} (containing folder: {})", path, open_containing_folder);
        match File::open(&path){
            Ok(file) => {
                let identifier = WindowIdentifier::from_native(&native).await;

                let res = if open_containing_folder || path.is_dir() {
                    OpenDirectoryRequest::default().identifier(identifier).send(&file.as_fd()).await
                } else {
                    OpenFileRequest::default().identifier(identifier).ask(true).send_file(&file.as_fd()).await
                };

                if let Err(err) = res {
                    warn!("Could not open folder / file: {}", err.to_string());
                    utils::inapp_notification(&i18n("Unable to open file / folder"), widget.as_ref());
                }
            },
            Err(err) => {
                warn!("Could not open file: {}", err.to_string());
                utils::inapp_notification(&i18n("Could not open file"), widget.as_ref());
            }
        }
    });
    glib::spawn_future_local(fut);
}
