// Fragments - torrent_dialog.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;

use adw::prelude::*;
use adw::subclass::dialog::*;
use adw::subclass::prelude::*;
use gio::SimpleAction;
use glib::{clone, subclass, Properties, VariantTy};
use gtk::{gio, glib, CompositeTemplate};
use transmission_gobject::{TrFile, TrTorrent, TrTorrentStatus};

use super::{FrgFilePage, FrgFileRow, FrgFolderPage, FrgStatusHeader};
use crate::backend::FrgConnectionManager;
use crate::i18n::ni18n_f;
use crate::model::{FrgFileSorter, FrgFileSorting};
use crate::{actions, utils};

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[template(resource = "/de/haeckerfelix/Fragments/gtk/torrent_dialog.ui")]
    #[properties(wrapper_type = super::FrgTorrentDialog)]
    pub struct FrgTorrentDialog {
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
        #[template_child]
        navigation_view: TemplateChild<adw::NavigationView>,

        #[template_child]
        status_header: TemplateChild<FrgStatusHeader>,

        #[template_child]
        location_box: TemplateChild<gtk::Box>,
        #[template_child]
        location_label: TemplateChild<gtk::Label>,

        #[template_child]
        error_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        error_row: TemplateChild<adw::ActionRow>,

        #[template_child]
        button_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pause_button: TemplateChild<gtk::Button>,
        #[template_child]
        continue_button: TemplateChild<gtk::Button>,
        #[template_child]
        open_button: TemplateChild<gtk::Button>,
        #[template_child]
        more_button: TemplateChild<gtk::MenuButton>,

        #[template_child]
        download_image: TemplateChild<gtk::Image>,
        #[template_child]
        upload_image: TemplateChild<gtk::Image>,
        #[template_child]
        download_speed_label: TemplateChild<gtk::Label>,
        #[template_child]
        upload_speed_label: TemplateChild<gtk::Label>,
        #[template_child]
        seeders_label: TemplateChild<gtk::Label>,
        #[template_child]
        leechers_label: TemplateChild<gtk::Label>,
        #[template_child]
        downloaded_label: TemplateChild<gtk::Label>,
        #[template_child]
        uploaded_label: TemplateChild<gtk::Label>,
        #[template_child]
        metadata_loading_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        metadata_percentage_label: TemplateChild<gtk::Label>,
        #[template_child]
        file_listbox: TemplateChild<gtk::ListBox>,
        #[template_child]
        all_files_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        all_files_row: TemplateChild<adw::ActionRow>,

        #[property(get, set, construct_only)]
        torrent: OnceCell<TrTorrent>,

        view_actions: gio::SimpleActionGroup,
        files_sorter: FrgFileSorter,
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for FrgTorrentDialog {
        fn constructed(&self) {
            self.parent_constructed();
            let obj: &super::FrgTorrentDialog = &self.obj();
            let torrent = obj.torrent();

            // Pull extra information from transmission daemon so we can display them in the
            // dialog. Those aren't getting retrieved by default to avoid unnecessary
            // deserialization.
            torrent.set_update_extra_info(true);

            // Bind torrent values
            self.bind_property("name", obj, "title");
            self.bind_property("primary-mime-type", &self.status_header.get(), "mimetype");
            self.bind_property("name", &self.status_header.get(), "title");
            self.bind_property("size", &self.status_header.get(), "total-bytes");
            self.bind_property("downloaded", &self.status_header.get(), "downloaded-bytes");
            self.bind_property("download-dir", &self.location_label.get(), "label");
            self.bind_property("leechers", &self.leechers_label.get(), "label");

            // Setup `torrent` specific actions
            actions::install_torrent_actions(&torrent, obj.clone().upcast());

            // "More" menu button
            let builder =
                gtk::Builder::from_resource("/de/haeckerfelix/Fragments/gtk/torrent_menu.ui");
            let menu: gio::MenuModel = builder.object("torrent_more_menu").unwrap();
            self.more_button.set_menu_model(Some(&menu));

            // Files metadata
            torrent
                .bind_property(
                    "metadata-percent-complete",
                    &self.metadata_percentage_label.get(),
                    "label",
                )
                .transform_to(|_, p: f32| Some(format!("{:.0}%", p * 100.0)))
                .sync_create()
                .build();

            torrent
                .bind_property(
                    "metadata-percent-complete",
                    &self.metadata_loading_group.get(),
                    "visible",
                )
                .transform_to(|_, p: f32| Some(p != 1.0))
                .sync_create()
                .build();

            // Files listbox
            if torrent.files().is_ready() {
                self.show_overview_files();
            }
            torrent
                .files()
                .connect_is_ready_notify(clone!(@weak self as this => move|_| {
                    this.show_overview_files();
                }));

            // Files listbox row activation
            self.file_listbox
                .connect_row_activated(clone!(@weak self as this => move |_, row|{
                    let row = row.downcast_ref::<adw::PreferencesRow>().unwrap().child().unwrap();
                    let file_row = row.downcast_ref::<FrgFileRow>().unwrap();
                    let file = file_row.file();

                    if let Some(file) = file {
                        if file.is_folder() {
                            this.show_folder(&file);
                        } else {
                            this.show_file(&file);
                        }
                    }
                }));

            // Setup view actions
            obj.insert_action_group("view", Some(&self.view_actions));
            let mut action;

            // view.sorting
            action = SimpleAction::new_stateful(
                "sorting",
                VariantTy::STRING.into(),
                &"name".to_variant(),
            );
            action.connect_change_state(
                clone!(@weak self.files_sorter as files_sorter => move |action, state|{
                    if let Some(state) = state {
                        action.set_state(state);

                        let sorter: FrgFileSorting = state.str().unwrap().into();
                        files_sorter.set_sorting(sorter);
                    }
                }),
            );
            self.view_actions.add_action(&action);

            // view.order
            action = SimpleAction::new_stateful(
                "order",
                VariantTy::STRING.into(),
                &"ascending".to_variant(),
            );
            action.connect_change_state(
                clone!(@weak self.files_sorter as files_sorter => move |action, state|{
                    if let Some(state) = state {
                        action.set_state(state);

                        let descending = state.str().unwrap() == "descending";
                        files_sorter.set_descending(descending);
                    }
                }),
            );
            self.view_actions.add_action(&action);

            // view.folder-before-files
            action = SimpleAction::new_stateful("folder-before-files", None, &true.to_variant());
            action.connect_change_state(
                clone!(@weak self.files_sorter as files_sorter => move |action, state|{
                    if let Some(state) = state {
                        action.set_state(state);

                        let value = state.get::<bool>().unwrap();
                        files_sorter.set_folders_before_files(value);
                    }
                }),
            );
            self.view_actions.add_action(&action);

            // view.show-all
            action = SimpleAction::new("show-all", None);
            action.connect_activate(clone!(@weak self as this => move |_, _|{
                if let Some(top_level) = this.obj().torrent().files().top_level(){
                    this.show_folder(&top_level);
                } else {
                    warn!("Unable to show contents, no top level file available");
                }

            }));
            self.view_actions.add_action(&action);

            // view.show-folder
            action = SimpleAction::new("show-folder", VariantTy::STRING.into());
            action.connect_activate(clone!(@weak self as this => move |_, param|{
                if let Some(param) = param {
                    let name = param.get::<String>().unwrap();
                    let folder = this.obj().torrent().files().file_by_name(&name).unwrap();
                    this.show_folder(&folder);
                }
            }));
            self.view_actions.add_action(&action);

            // view.show-file
            action = SimpleAction::new("show-file", VariantTy::STRING.into());
            action.connect_activate(clone!(@weak self as this => move |_, param|{
                if let Some(param) = param {
                    let name = param.get::<String>().unwrap();
                    let file = this.obj().torrent().files().file_by_name(&name).unwrap();
                    this.show_file(&file);
                }
            }));
            self.view_actions.add_action(&action);

            // Update dialog on torrent changes
            torrent.connect_notify_local(
                None,
                clone!(@weak self as this => move |_, _| {
                    this.update_labels();
                    this.update_widgets();
                }),
            );

            self.update_labels();
            self.update_widgets();
        }
    }

    impl WidgetImpl for FrgTorrentDialog {}

    impl AdwDialogImpl for FrgTorrentDialog {
        fn closed(&self) {
            self.obj().torrent().set_update_extra_info(false);
            self.parent_closed();
        }
    }

    impl FrgTorrentDialog {
        fn show_folder(&self, folder: &TrFile) {
            let page = FrgFolderPage::new(folder, &self.files_sorter);
            self.navigation_view.push(&page);
        }

        fn show_file(&self, file: &TrFile) {
            let page = FrgFilePage::new(file);
            self.navigation_view.push(&page);
        }

        fn show_overview_files(&self) {
            let torrent = self.obj().torrent();

            let top_level = if let Some(top_level) = torrent.files().top_level() {
                top_level
            } else {
                warn!("Unable display files in overview page, no top level available");
                return;
            };

            // When a torrent has multiple files, the top level entry is always a folder
            if top_level.is_folder() {
                self.file_listbox.set_visible(true);
                let files = top_level.related();

                // We has to limit the files which are getting displayed in a
                // regular listbox since it doesn't scale like a listview
                let mut max_files = files.n_items();
                if max_files > 8 {
                    max_files = 8;
                    let total = torrent.files().n_items();

                    // Translators: {} is *always* a number greater than 8
                    let text = ni18n_f(
                        "Show All",
                        "Show All {} Items",
                        total,
                        &[&total.to_string()],
                    );

                    // Show row which allows to show all files using a listview
                    self.all_files_row.set_title(&text);
                    self.all_files_group.set_visible(true);
                    self.obj().set_content_height(1200);
                }

                let sorter = FrgFileSorter::new();
                let files_sorted = gtk::SortListModel::new(Some(files), Some(sorter));

                // Add some files to the simple listbox
                for i in 0..max_files {
                    let object = files_sorted.item(i).unwrap();
                    let file = object.downcast_ref::<TrFile>().unwrap();

                    let file_row = FrgFileRow::new();
                    file_row.set_file(file);

                    let row = adw::PreferencesRow::new();
                    row.set_child(Some(&file_row));

                    self.file_listbox.append(&row);
                }
            }
        }

        fn update_labels(&self) {
            let torrent = self.obj().torrent();

            let eta = utils::eta_text(&torrent);
            self.status_header.set_eta(eta);

            let downloaded = utils::format_size(torrent.downloaded());
            self.downloaded_label.set_text(&downloaded);

            let uploaded = utils::format_size(torrent.uploaded());
            self.uploaded_label.set_text(&uploaded);

            let download_speed = utils::format_speed(torrent.download_speed());
            self.download_speed_label.set_text(&download_speed);

            let upload_speed = utils::format_speed(torrent.upload_speed());
            self.upload_speed_label.set_text(&upload_speed);

            let seeders = torrent.seeders().to_string();
            let seeders_active = torrent.seeders_active().to_string();
            // Translators: First {} corresponds to the number of seeders, second {} to the
            // number of active seeders
            let seeders_text = ni18n_f(
                "{} ({} active)",
                "{} ({} active)",
                torrent.seeders_active().try_into().unwrap_or(0),
                &[&seeders, &seeders_active],
            );
            self.seeders_label.set_text(&seeders_text);
        }

        fn update_widgets(&self) {
            let session = FrgConnectionManager::default().client().session();
            let torrent = self.obj().torrent();

            // Location label
            // Only show the location info when it differs from the standard download
            let session_download_dir = if let Some(dir) = session.download_dir() {
                dir.path().unwrap().to_str().unwrap().to_string()
            } else {
                String::new()
            };
            let show_location = torrent.download_dir() != session_download_dir;
            self.location_box.set_visible(show_location);

            // Action button row
            let button_widget = match torrent.status() {
                TrTorrentStatus::Stopped => self.continue_button.get(),
                _ => self.pause_button.get(),
            };
            self.button_stack.set_visible_child(&button_widget);

            // Error message row
            if torrent.error() != 0 {
                self.error_group.set_visible(true);
                self.error_row.set_subtitle(&torrent.error_string());
            } else {
                self.error_group.set_visible(false);
            }

            // Status boxes
            let is_downloading = torrent.download_speed() != 0;
            let is_uploading = torrent.upload_speed() != 0;

            if is_downloading {
                self.download_image.add_css_class("active");
            } else {
                self.download_image.remove_css_class("active");
            }

            if is_uploading {
                self.upload_image.add_css_class("active");
            } else {
                self.upload_image.remove_css_class("active");
            }
        }

        fn bind_property<T: IsA<gtk::Widget>>(
            &self,
            prop_name: &str,
            widget: &T,
            widget_prop_name: &str,
        ) {
            self.obj()
                .torrent()
                .bind_property(prop_name, widget, widget_prop_name)
                .sync_create()
                .build();
        }
    }
}

glib::wrapper! {
    pub struct FrgTorrentDialog(ObjectSubclass<imp::FrgTorrentDialog>)
        @extends gtk::Widget, adw::Dialog;
}

impl FrgTorrentDialog {
    pub fn new(torrent: &TrTorrent) -> Self {
        glib::Object::builder().property("torrent", torrent).build()
    }

    pub fn toast_overlay(&self) -> adw::ToastOverlay {
        self.imp().toast_overlay.get()
    }
}
