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

mod audio;
mod caption;
mod content;
mod file;
mod location;
mod message_state_stack;
mod reaction;
mod reaction_list;
mod reply;
mod sender_name;
mod text;
mod visual_media;

pub use self::content::{ContentFormat, MessageContent};
use self::{
    message_state_stack::MessageStateStack, reaction_list::MessageReactionList,
    sender_name::MessageSenderName,
};
use super::{ReadReceiptsList, SenderAvatar};
use crate::{
    Application, gettext_f,
    prelude::*,
    session::model::{Event, EventHeaderState},
    system_settings::ClockFormat,
    utils::BoundObject,
};

mod imp {
    use std::{cell::RefCell, 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/message_row/mod.ui"
    )]
    #[properties(wrapper_type = super::MessageRow)]
    pub struct MessageRow {
        #[template_child]
        avatar: TemplateChild<SenderAvatar>,
        #[template_child]
        header: TemplateChild<gtk::Box>,
        #[template_child]
        display_name: TemplateChild<MessageSenderName>,
        #[template_child]
        timestamp: TemplateChild<gtk::Label>,
        #[template_child]
        content: TemplateChild<MessageContent>,
        #[template_child]
        message_state: TemplateChild<MessageStateStack>,
        #[template_child]
        reactions: TemplateChild<MessageReactionList>,
        #[template_child]
        read_receipts: TemplateChild<ReadReceiptsList>,
        binding: RefCell<Option<glib::Binding>>,
        system_settings_handler: RefCell<Option<glib::SignalHandlerId>>,
        /// The event that is presented.
        #[property(get, set = Self::set_event, explicit_notify)]
        event: BoundObject<Event>,
        /// The texture of the image preview displayed by the descendant of this
        /// widget, if any.
        #[property(get = Self::texture)]
        texture: PhantomData<Option<gdk::Texture>>,
    }

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

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

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

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

            self.content.connect_format_notify(clone!(
                #[weak(rename_to = imp)]
                self,
                move |content| {
                    imp.reactions.set_visible(!matches!(
                        content.format(),
                        ContentFormat::Compact | ContentFormat::Ellipsized
                    ));
                }
            ));
            self.content.connect_texture_notify(clone!(
                #[weak]
                obj,
                move |_| {
                    obj.notify_texture();
                }
            ));

            let system_settings = Application::default().system_settings();
            let system_settings_handler = system_settings.connect_clock_format_notify(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    imp.update_timestamp();
                }
            ));
            self.system_settings_handler
                .replace(Some(system_settings_handler));
        }

        fn dispose(&self) {
            if let Some(binding) = self.binding.take() {
                binding.unbind();
            }

            if let Some(handler) = self.system_settings_handler.take() {
                Application::default().system_settings().disconnect(handler);
            }
        }
    }

    impl WidgetImpl for MessageRow {}
    impl BinImpl for MessageRow {}

    impl MessageRow {
        /// Set the event that is presented.
        fn set_event(&self, event: Event) {
            let obj = self.obj();

            // Remove signals and bindings from the previous event.
            self.event.disconnect_signals();
            if let Some(binding) = self.binding.take() {
                binding.unbind();
            }

            let sender = event.sender();
            self.avatar.set_sender(Some(sender.clone()));
            self.display_name.set_sender(Some(sender));

            let state_binding = event
                .bind_property("state", &*self.message_state, "state")
                .sync_create()
                .build();

            self.binding.replace(Some(state_binding));

            let header_state_handler = event.connect_header_state_notify(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    imp.update_header();
                }
            ));

            let timestamp_handler = event.connect_timestamp_notify(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    imp.update_timestamp();
                }
            ));

            // Listening to changes in the source might not be enough, there are changes
            // that we display that do not affect the source, like related events.
            let item_changed_handler = event.connect_item_changed(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    imp.update_content();
                }
            ));

            self.reactions
                .set_reaction_list(&event.room().get_or_create_members(), &event.reactions());
            self.read_receipts.set_source(event.read_receipts());
            self.event.set(
                event,
                vec![
                    header_state_handler,
                    timestamp_handler,
                    item_changed_handler,
                ],
            );
            obj.notify_event();

            self.update_content();
            self.update_header();
            self.update_timestamp();
        }

        /// Update the header for the current event.
        fn update_header(&self) {
            let Some(event) = self.event.obj() else {
                return;
            };

            let header_state = event.header_state();
            let avatar_name_visible = header_state == EventHeaderState::Full;
            let header_visible = header_state != EventHeaderState::Hidden;

            self.avatar.set_visible(avatar_name_visible);
            self.display_name.set_visible(avatar_name_visible);
            self.header.set_visible(header_visible);

            if let Some(row) = self.obj().parent() {
                if avatar_name_visible {
                    row.add_css_class("has-avatar");
                } else {
                    row.remove_css_class("has-avatar");
                }
            }
        }

        /// Update the displayed timestamp for the current event with the
        /// current clock format setting.
        fn update_timestamp(&self) {
            let Some(event) = self.event.obj() else {
                return;
            };

            let datetime = event.timestamp();

            let clock_format = Application::default().system_settings().clock_format();
            let time = if clock_format == ClockFormat::TwelveHours {
                datetime.format("%I:%M %p").unwrap()
            } else {
                datetime.format("%R").unwrap()
            };

            self.timestamp.set_label(&time);

            let accessible_label = gettext_f("Sent at {time}", &[("time", &time)]);
            self.timestamp
                .update_property(&[gtk::accessible::Property::Label(&accessible_label)]);
        }

        /// Update the content for the current event.
        fn update_content(&self) {
            let Some(event) = self.event.obj() else {
                return;
            };

            self.content.update_for_event(&event);
        }

        /// Get the texture displayed by this widget, if any.
        pub(super) fn texture(&self) -> Option<gdk::Texture> {
            self.content.texture()
        }
    }
}

glib::wrapper! {
    /// A row displaying a message in the timeline.
    pub struct MessageRow(ObjectSubclass<imp::MessageRow>)
        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}

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

impl Default for MessageRow {
    fn default() -> Self {
        Self::new()
    }
}
