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 glib::subclass::InitializingObject;
    use once_cell::sync::Lazy;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/typing_row.ui")]
    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.
        pub bound_list: BoundObjectWeakRef<TypingList>,
    }

    #[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");
        }

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

    impl ObjectImpl for TypingRow {
        fn properties() -> &'static [glib::ParamSpec] {
            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
                vec![
                    glib::ParamSpecObject::builder::<TypingList>("list")
                        .explicit_notify()
                        .build(),
                    glib::ParamSpecBoolean::builder("is-empty")
                        .default_value(true)
                        .read_only()
                        .build(),
                ]
            });

            PROPERTIES.as_ref()
        }

        fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
            match pspec.name() {
                "list" => self.obj().set_list(value.get().unwrap()),
                _ => unimplemented!(),
            }
        }

        fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
            let obj = self.obj();

            match pspec.name() {
                "list" => obj.list().to_value(),
                "is-empty" => obj.is_empty().to_value(),
                _ => unimplemented!(),
            }
        }
    }

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

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()
    }

    /// The list of members that are currently typing.
    pub fn list(&self) -> Option<TypingList> {
        self.imp().bound_list.obj()
    }

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

        let imp = self.imp();
        let prev_is_empty = self.is_empty();

        imp.bound_list.disconnect_signals();

        if let Some(list) = list {
            let items_changed_handler_id = list.connect_items_changed(
                clone!(@weak self as obj => move |list, _pos, removed, added| {
                    if removed != 0 || added != 0 {
                        obj.update_label(list);
                    }
                }),
            );
            let is_empty_notify_handler_id = list.connect_notify_local(
                Some("is-empty"),
                clone!(@weak self as obj => move |_, _| obj.notify("is-empty")),
            );

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

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

        if prev_is_empty != self.is_empty() {
            self.notify("is-empty");
        }

        self.notify("list");
    }

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

        list.is_empty()
    }

    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].display_name();

        let label = if n == 1 {
            gettext_f(
                // Translators: Do NOT translate the content between '{' and '}', these are
                // variable names.
                "<b>{user}</b> is typing…",
                &[("user", &user)],
            )
        } 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);
    }
}
