use adw::subclass::prelude::*;
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};

use crate::{
    components::OverlappingAvatars,
    i18n::{gettext_f, ngettext_f},
    prelude::*,
    session::model::{Member, TypingList},
    utils::BoundObjectWeakRef,
};

mod imp {
    use std::marker::PhantomData;

    use glib::subclass::InitializingObject;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
    #[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/typing_row.ui")]
    #[properties(wrapper_type = super::TypingRow)]
    pub struct TypingRow {
        #[template_child]
        pub avatar_list: TemplateChild<OverlappingAvatars>,
        #[template_child]
        pub label: TemplateChild<gtk::Label>,
        /// The list of members that are currently typing.
        #[property(get, set = Self::set_list, explicit_notify, nullable)]
        pub list: BoundObjectWeakRef<TypingList>,
        /// Whether the list is empty.
        #[property(get = Self::is_empty, default = true)]
        is_empty: PhantomData<bool>,
    }

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

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

            klass.set_css_name("typing-row");
            klass.set_accessible_role(gtk::AccessibleRole::Status);
        }

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

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

    impl WidgetImpl for TypingRow {}
    impl BinImpl for TypingRow {}

    impl TypingRow {
        /// Set the list of members that are currently typing.
        fn set_list(&self, list: Option<TypingList>) {
            if self.list.obj() == list {
                return;
            }
            let obj = self.obj();

            let prev_is_empty = self.is_empty();

            self.list.disconnect_signals();

            if let Some(list) = list {
                let items_changed_handler_id = list.connect_items_changed(clone!(
                    #[weak]
                    obj,
                    move |list, _pos, removed, added| {
                        if removed != 0 || added != 0 {
                            obj.update_label(list);
                        }
                    }
                ));
                let is_empty_notify_handler_id = list.connect_is_empty_notify(clone!(
                    #[weak]
                    obj,
                    move |_| obj.notify_is_empty()
                ));

                self.avatar_list.bind_model(Some(list.clone()), |item| {
                    item.downcast_ref::<Member>().unwrap().avatar_data().clone()
                });

                self.list.set(
                    &list,
                    vec![items_changed_handler_id, is_empty_notify_handler_id],
                );
                obj.update_label(&list);
            }

            if prev_is_empty != self.is_empty() {
                obj.notify_is_empty();
            }

            obj.notify_list();
        }

        /// Whether the list is empty.
        fn is_empty(&self) -> bool {
            let Some(list) = self.list.obj() else {
                return true;
            };

            list.is_empty()
        }
    }
}

glib::wrapper! {
    /// A widget row used to display typing notification.
    pub struct TypingRow(ObjectSubclass<imp::TypingRow>)
        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}

impl TypingRow {
    pub fn new() -> Self {
        glib::Object::new()
    }

    fn update_label(&self, list: &TypingList) {
        let n = list.n_items();
        if n == 0 {
            // Don't update anything, the `is-empty` property should trigger a revealer
            // animation.
            return;
        }

        let members = list.members();
        let user = members[0].disambiguated_name();

        let label = if n == 1 {
            gettext_f(
                // Translators: Do NOT translate the content between '{' and '}', these are
                // variable names.
                "{user} is typing…",
                &[("user", &format!("<b>{user}</b>"))],
            )
        } else {
            ngettext_f(
                // Translators: Do NOT translate the content between '{' and '}', these are
                // variable names.
                "{n} member is typing…",
                "{n} members are typing…",
                n,
                &[("n", &n.to_string())],
            )
        };
        self.imp().label.set_label(&label);
    }
}
