// 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 glib::{clone, subclass, Properties};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gdk, gio, glib, CompositeTemplate};
use transmission_gobject::{TrTorrent, TrTorrentStatus};

use crate::i18n::i18n_f;
use crate::{actions, utils};

mod imp {
    use super::*;

    #[derive(Debug, CompositeTemplate, Default, Properties)]
    #[template(resource = "/de/haeckerfelix/Fragments/gtk/torrent_row.ui")]
    #[properties(wrapper_type = super::FrgTorrentRow)]
    pub struct FrgTorrentRow {
        #[template_child]
        torrent_name_label: TemplateChild<gtk::Label>,
        #[template_child]
        description_label: TemplateChild<gtk::Label>,
        #[template_child]
        eta_label: TemplateChild<gtk::Label>,
        #[template_child]
        queue_position_label: TemplateChild<gtk::Label>,
        #[template_child]
        progress_bar: TemplateChild<gtk::ProgressBar>,
        #[template_child]
        spinner: TemplateChild<gtk::Spinner>,
        #[template_child]
        queue_box: TemplateChild<gtk::Box>,
        #[template_child]
        index_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        action_stack: TemplateChild<gtk::Stack>,

        popover_menu: OnceCell<gtk::PopoverMenu>,

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

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

        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 FrgTorrentRow {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            actions::install_torrent_actions(&obj.torrent(), obj.clone().upcast());

            // Right-click popover menu
            let builder =
                gtk::Builder::from_resource("/de/haeckerfelix/Fragments/gtk/torrent_menu.ui");
            let menu: gio::MenuModel = builder.object("torrent_menu").unwrap();

            let popover_menu = gtk::PopoverMenu::from_model(Some(&menu));
            popover_menu.set_parent(self.obj().upcast_ref::<gtk::Widget>());
            popover_menu.set_has_arrow(false);

            self.popover_menu.set(popover_menu).unwrap();

            // Binding torrent values
            self.bind_property("name", self.torrent_name_label.get(), "label");
            self.bind_property("progress", self.progress_bar.get(), "fraction");

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

            // Shift+F10 / `Menu` button to open context menu
            let trigger = gtk::AlternativeTrigger::new(
                gtk::KeyvalTrigger::new(gdk::Key::F10, gdk::ModifierType::SHIFT_MASK),
                gtk::KeyvalTrigger::new(gdk::Key::Menu, gdk::ModifierType::empty()),
            );
            let action = gtk::CallbackAction::new(
                clone!(@weak self as this => @default-return glib::Propagation::Stop, move |_, _| {
                    this.show_context_menu(None::<&gtk::Gesture>, 40.0, 40.0);
                    glib::Propagation::Stop
                }),
            );
            let shortcut = gtk::Shortcut::new(Some(trigger), Some(action));

            let controller = gtk::ShortcutController::new();
            controller.add_shortcut(shortcut);
            self.obj().add_controller(controller);

            // Right click to open context menu
            let controller = gtk::GestureClick::new();
            controller.set_button(gdk::BUTTON_SECONDARY);
            controller.connect_pressed(
                clone!(@weak self as this => move |c, _, x, y| this.show_context_menu(Some(c), x, y)),
            );
            self.obj().add_controller(controller);

            // Touch long-press to open context menu
            let controller = gtk::GestureLongPress::new();
            controller.set_touch_only(true);
            controller.connect_pressed(
                clone!(@weak self as this => move |c, x, y| this.show_context_menu(Some(c), x, y)),
            );
            self.obj().add_controller(controller);

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

        fn dispose(&self) {
            self.popover_menu.get().unwrap().unparent();
        }
    }

    impl WidgetImpl for FrgTorrentRow {}

    impl ListBoxRowImpl for FrgTorrentRow {}

    impl FrgTorrentRow {
        fn show_context_menu<G>(&self, controller: Option<&G>, x: f64, y: f64)
        where
            G: IsA<gtk::Gesture>,
        {
            let popover_menu = self.popover_menu.get().unwrap();

            if let Some(controller) = controller {
                controller.set_state(gtk::EventSequenceState::Claimed);
            }

            let coords = gdk::Rectangle::new(x as i32, y as i32, 0, 0);
            popover_menu.set_pointing_to(Some(&coords));
            popover_menu.set_halign(gtk::Align::Start);
            popover_menu.popup();
        }

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

            let eta = utils::eta_text(&torrent);
            let description = if torrent.error() == 0 {
                self.description_text()
            } else {
                torrent.error_string()
            };
            let pos = (torrent.download_queue_position() + 1).to_string();

            self.eta_label.set_text(&eta);
            self.description_label.set_text(&description);
            self.queue_position_label.set_text(&pos);
        }

        fn update_widgets(&self) {
            self.queue_box.set_visible(false);
            self.spinner.set_spinning(false);

            self.obj().remove_css_class("inactive");
            self.description_label.remove_css_class("error");

            let index_name: String;
            let action_name: String;

            match self.obj().torrent().status() {
                TrTorrentStatus::Download => {
                    index_name = "download".into();
                    action_name = "pause".into();
                }
                TrTorrentStatus::DownloadWait => {
                    index_name = "queued".into();
                    action_name = "pause".into();

                    self.obj().add_css_class("inactive");
                    self.queue_box.set_visible(true);
                }
                TrTorrentStatus::Check | TrTorrentStatus::CheckWait => {
                    index_name = "check".into();
                    action_name = "pause".into();

                    self.obj().add_css_class("inactive");
                    self.spinner.set_spinning(true);
                }
                TrTorrentStatus::Stopped => {
                    index_name = "stopped".into();
                    action_name = "continue".into();

                    self.obj().add_css_class("inactive");
                }
                TrTorrentStatus::Seed | TrTorrentStatus::SeedWait => {
                    index_name = "upload".into();
                    action_name = "remove".into();
                }
            }

            if self.obj().torrent().error() != 0 {
                self.description_label.add_css_class("error");
            }

            self.index_stack.set_visible_child_name(&index_name);
            self.action_stack.set_visible_child_name(&action_name);
        }

        fn description_text(&self) -> String {
            let torrent = self.obj().torrent();

            let downloaded = utils::format_size(torrent.downloaded());
            let uploaded = utils::format_size(torrent.uploaded());
            let download_speed = utils::format_speed(torrent.download_speed());
            let upload_speed = utils::format_speed(torrent.upload_speed());
            let size = utils::format_size(torrent.size());

            if torrent.downloaded() == torrent.size() {
                // Translators: First {} is the amount uploaded, second {} is the
                // uploading speed
                // Displayed under the torrent name in the main window when torrent
                // download is finished
                i18n_f("{} uploaded · {}", &[&uploaded, &upload_speed])
            } else if torrent.status() == TrTorrentStatus::Stopped
                || torrent.status() == TrTorrentStatus::DownloadWait
            {
                // Translators: First {} is the amount downloaded, second {} is the
                // total torrent size
                // Displayed under the torrent name in the main window when torrent
                // is stopped
                i18n_f("{} of {}", &[&downloaded, &size])
            } else {
                // Translators: First {} is the amount downloaded, second {} is the
                // total torrent size, third {} is the download speed
                // Displayed under the torrent name in the main window when torrent
                // is downloading
                i18n_f("{} of {} · {}", &[&downloaded, &size, &download_speed])
            }
        }

        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)
                .flags(glib::BindingFlags::SYNC_CREATE)
                .build();
        }
    }
}

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

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