use crate::prelude::*;

use gio::{SimpleAction, SimpleActionGroup};
use regex::Regex;

use crate::backend::message::Message;
use crate::gui::attachment::Attachment;
use crate::gui::components::*;

glib::wrapper! {
    /// Widget displaying text messages.
    pub struct MessageItem(ObjectSubclass<imp::MessageItem>)
        @extends adw::Bin, gtk::Widget, ContextMenuBin,
        @implements gtk::gio::ActionGroup, gtk::gio::ActionMap, gtk::Accessible, gtk::Buildable,
            gtk::ConstraintTarget;
}

impl MessageItem {
    pub fn from_ftypes_for_display(obj: &Message) -> Self {
        let s = Self::from_ftypes(obj);
        s.set_force_show_timestamp(true);
        s.set_for_display(true);
        s
    }
    pub fn from_ftypes(obj: &Message) -> Self {
        log::trace!("Initializing `MessageItem`");
        let s = Object::builder::<Self>().property("message", obj).build();

        s.init_label_selectable();
        s.setup_showheader();
        s.setup_showtimestamp();
        s.setup_from_group();
        s.setup_from_self();
        s.setup_quote();
        s.setup_emoji();
        s.setup_loaded();
        s.setup_text();
        s.setup_requires_attention();
        s.setup_pending_and_error();
        s
    }

    fn manager(&self) -> Manager {
        self.message().manager()
    }

    pub fn get_popover(&self) -> gtk::PopoverMenu {
        self.imp().msg_menu.to_owned()
    }

    pub fn get_pressed_attachment(&self) -> Attachment {
        self.property("pressed-attachment")
    }

    fn setup_actions(&self) {
        let action_reply = SimpleAction::new("reply", None);
        action_reply.connect_activate(clone!(
            #[weak(rename_to = s)]
            self,
            move |_, _| {
                s.imp().handle_reply();
            }
        ));

        let action_delete = SimpleAction::new("delete", None);
        action_delete.connect_activate(clone!(
            #[weak(rename_to = s)]
            self,
            move |_, _| {
                s.imp().handle_delete();
            }
        ));

        let action_copy = SimpleAction::new("copy", None);
        action_copy.connect_activate(clone!(
            #[weak(rename_to = s)]
            self,
            move |_, _| {
                s.imp().handle_copy();
            }
        ));

        let action_download = SimpleAction::new("download", None);
        action_download.connect_activate(clone!(
            #[weak(rename_to = s)]
            self,
            move |_, _| {
                s.get_pressed_attachment().imp().download();
            }
        ));

        let action_open = SimpleAction::new("open", None);
        action_open.connect_activate(clone!(
            #[weak(rename_to = s)]
            self,
            move |_, _| {
                s.get_pressed_attachment().imp().open();
            }
        ));

        let actions = SimpleActionGroup::new();
        self.insert_action_group("msg", Some(&actions));
        actions.add_action(&action_reply);
        actions.add_action(&action_delete);
        actions.add_action(&action_copy);
        actions.add_action(&action_download);
        actions.add_action(&action_open);

        self.bind_property("message", &action_delete, "enabled")
            .transform_to(|_, msg: Option<Message>| msg.map(|m| m.sender().is_self()))
            .sync_create()
            .build();

        self.bind_property("pressed-attachment", &action_download, "enabled")
            .transform_to(|_, att: Option<Attachment>| Some(att.is_some()))
            .sync_create()
            .build();

        action_download
            .bind_property("enabled", &action_open, "enabled")
            .sync_create()
            .build();
    }

    fn setup_showheader(&self) {
        self.message().connect_notify_local(
            Some("show-header"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |_, _| s.set_show_header()
            ),
        );
        self.connect_notify_local(
            Some("force-show-header"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |_, _| s.set_show_header()
            ),
        );
        self.set_show_header();
    }

    fn setup_showtimestamp(&self) {
        self.message().connect_notify_local(
            Some("show-timestamp"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |_, _| s.set_show_timestamp()
            ),
        );
        self.connect_notify_local(
            Some("force-show-timestamp"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |_, _| s.set_show_timestamp()
            ),
        );
        // Edited messages always show the timestamp.
        self.message().connect_notify_local(
            Some("is-edited"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |_, _| s.set_show_timestamp()
            ),
        );
        self.set_show_timestamp();
    }

    fn setup_quote(&self) {
        // if self.message().quote().is_some() {
        //     self.imp().message_bubble.add_css_class("has-quote");
        // }
    }

    fn setup_text(&self) {
        if self.message().body().is_some_and(|b| !b.is_empty()) {
            self.imp().message_bubble.add_css_class("has-text");
            self.imp().timestamp_img.set_visible(false);
        } else {
            self.imp().timestamp.set_visible(false);
        }
    }

    fn setup_from_self(&self) {
        if self.message().sender().is_self() {
            self.add_css_class("from-self");
            self.message().set_show_header(false);
            self.set_halign(gtk::Align::End);
            self.imp().reactions.set_halign(gtk::Align::Start);
            self.imp().message_bubble.set_halign(gtk::Align::End);
            self.imp().reaction_grid.set_halign(gtk::Align::End);
        } else {
            self.set_halign(gtk::Align::Start);
            self.imp().reactions.set_halign(gtk::Align::End);
            self.imp().message_bubble.set_halign(gtk::Align::Start);
            self.imp().reaction_grid.set_halign(gtk::Align::Start);
        }
    }

    fn setup_from_group(&self) {
        if self.message().channel().is_contact() {
            self.message().set_show_header(false);
            self.imp().avatar.set_visible(false);
        }
    }

    fn setup_emoji(&self) {
        lazy_static::lazy_static! {
            static ref RE: Regex = Regex::new(r"^[[\p{Emoji}--\p{Ascii}]\u{fe0f}\u{200d} ]+$").unwrap();
        }
        if let Some(body) = self.message().body()
            && RE.is_match(body.as_str())
        {
            self.imp().message_bubble.add_css_class("emoji");
        }
    }

    fn init_label_selectable(&self) {
        let manager = self.manager();
        let settings = manager.settings();
        let label = &self.imp().label_message.imp().label;

        settings
            .bind("messages-selectable", &**label, "selectable")
            .build();
    }

    pub fn open_popup(&self) {
        self.imp().msg_menu.popup();
    }

    /// Set whether this item should show its header.
    pub fn set_show_header(&self) {
        let visible = self.message().show_header() || self.property("force-show-header");
        self.imp().header.set_visible(visible);

        if visible && !self.has_css_class("has-header") {
            self.add_css_class("has-header");
        } else if !visible && self.has_css_class("has-header") {
            self.remove_css_class("has-header");
        }
    }

    /// Set whether this item should show its timestamp.
    pub fn set_show_timestamp(&self) {
        // Edited messages always show the timestamp.
        let visible = self.message().show_timestamp()
            || self.property("force-show-timestamp")
            || self.message().is_edited();
        self.imp().timestamp.set_visible(visible);
        self.imp().timestamp_img.set_visible(visible);
        if visible {
            self.add_css_class("has-timestamp");
        } else {
            self.remove_css_class("has-timestamp");
        }
    }

    fn setup_loaded(&self) {
        self.connect_notify_local(
            Some("shows-media-loading"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |_, _| {
                    s.imp().box_attachments.remove_css_class("not-loaded");
                }
            ),
        );
    }

    pub fn setup_requires_attention(&self) {
        let message = self.message();
        message.connect_notify_local(
            Some("requires-attention"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |m, _| {
                    if m.property("requires-attention") {
                        s.add_css_class("attention-animation");
                        let _ = gtk::prelude::WidgetExt::activate_action(
                            &s,
                            "listitem.scroll-to",
                            None,
                        );
                    } else {
                        s.remove_css_class("attention-animation");
                    }
                }
            ),
        );
        message.notify("requires-attention");
    }

    pub fn setup_pending_and_error(&self) {
        let message = self.message();
        message.connect_notify_local(
            Some("pending"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |m, _| {
                    if m.property("pending") {
                        s.add_css_class("pending");
                    } else {
                        s.remove_css_class("pending");
                    }
                }
            ),
        );
        message.connect_notify_local(
            Some("error"),
            clone!(
                #[weak(rename_to = s)]
                self,
                move |m, _| {
                    if m.property("error") {
                        s.add_css_class("error");
                    } else {
                        s.remove_css_class("error");
                    }
                }
            ),
        );
        message.notify("pending");
        message.notify("error");
    }
}

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

    use crate::{
        backend::Message,
        gui::message_info_dialog::{MessageInfoDialog, MessageInfoDialogPage},
        prelude::*,
    };

    use lazy_static::lazy_static;
    use regex::Regex;

    use glib::subclass::{InitializingObject, Signal};
    use gtk::{CompositeTemplate, EmojiChooser};

    use crate::gui::{
        attachment::{Attachment, backend_to_gui},
        components::*,
    };
    use adw::subclass::prelude::BinImpl;

    #[derive(CompositeTemplate, Default, glib::Properties)]
    #[properties(wrapper_type = super::MessageItem)]
    #[template(resource = "/ui/message_item.ui")]
    pub struct MessageItem {
        #[template_child]
        pub(super) avatar: TemplateChild<adw::Avatar>,
        #[template_child]
        pub(super) header: TemplateChild<gtk::Box>,
        #[template_child]
        pub(super) reactions: TemplateChild<gtk::Button>,
        #[template_child]
        pub(super) msg_menu: TemplateChild<gtk::PopoverMenu>,
        #[template_child]
        pub(super) box_attachments: TemplateChild<gtk::Box>,
        #[template_child]
        pub(super) media_overlay: TemplateChild<gtk::Overlay>,
        #[template_child]
        media_group: TemplateChild<gtk::Box>,
        #[template_child]
        pub(super) timestamp_img: TemplateChild<MessageIndicators>,
        #[template_child]
        download_btn: TemplateChild<gtk::Button>,
        #[template_child]
        pub(super) label_message: TemplateChild<MessageLabel>,
        #[template_child]
        pub reaction_grid: TemplateChild<gtk::Box>,
        #[template_child]
        pub(super) message_bubble: TemplateChild<gtk::Box>,
        #[template_child]
        pub(super) timestamp: TemplateChild<MessageIndicators>,

        #[property(get, set = Self::set_message, type = Message)]
        message: RefCell<Option<Message>>,
        #[property(get, set, nullable)]
        pressed_attachment: RefCell<Option<Attachment>>,
        #[property(get, set, default = true)]
        force_show_header: Cell<bool>,
        #[property(get, set, default = true)]
        force_show_timestamp: Cell<bool>,
        #[property(get, set)]
        has_attachment: Cell<bool>,
        #[property(get, set)]
        for_display: Cell<bool>,

        #[property(get = Self::shows_media_loading)]
        shows_media_loading: PhantomData<bool>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for MessageItem {
        const NAME: &'static str = "FlMessageItem";
        type Type = super::MessageItem;
        type ParentType = ContextMenuBin;

        fn class_init(klass: &mut Self::Class) {
            EmojiPicker::ensure_type();
            MessageIndicators::ensure_type();
            MessageLabel::ensure_type();
            Self::bind_template(klass);
            Self::bind_template_callbacks(klass);
            Utility::bind_template_callbacks(klass);
        }

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

    #[gtk::template_callbacks]
    impl MessageItem {
        fn shows_media_loading(&self) -> bool {
            self.message
                .borrow()
                .as_ref()
                .iter()
                .flat_map(|m| m.attachments())
                .any(|a| !a.loaded())
        }

        fn set_message(&self, msg: Option<Message>) {
            let obj = self.obj();
            if let Some(msg) = &msg {
                let attachments = msg.attachments();
                let mut container = &self.box_attachments;

                // Set attachments
                if !attachments.is_empty() {
                    obj.notify("shows-media-loading");
                    obj.set_property("has-attachment", true);
                    self.box_attachments
                        .parent()
                        .unwrap()
                        .add_css_class("has-attachment");

                    if attachments[0].is_image() || attachments[0].is_video() {
                        container = &self.media_group;
                        self.media_overlay.set_visible(true);
                    }

                    for att in attachments {
                        log::trace!("MessageItem got Attachment, adding to `box_attachments`");

                        let att_widget = backend_to_gui(&att);

                        att.connect_notify_local(
                            Some("loaded"),
                            clone!(
                                #[weak]
                                obj,
                                move |_, _| obj.notify("shows-media-loading")
                            ),
                        );

                        att_widget.connect_local(
                            "pressed",
                            false,
                            clone!(
                                #[weak]
                                obj,
                                #[weak(rename_to = att)]
                                att_widget,
                                #[upgrade_or_default]
                                move |_| {
                                    obj.set_property("pressed-attachment", Some(att));
                                    None
                                }
                            ),
                        );
                        container.append(&att_widget);
                    }
                }
            }

            self.message.replace(msg);
        }

        #[template_callback(function)]
        fn markup_urls(s: Option<String>) -> Option<String> {
            lazy_static! {
                static ref RE: Regex = Regex::new(r"(?P<l>[a-z]*://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=;]*))").unwrap();
            }
            let s = s.map(|s| glib::markup_escape_text(&s));
            s.map(|s| RE.replace_all(&s, r#"<a href="$l">$l</a>"#).to_string())
        }

        #[template_callback]
        pub fn handle_reaction_clicked(&self) {
            let obj = self.obj();
            let msg = obj.message();
            MessageInfoDialog::new(&msg, MessageInfoDialogPage::Reactions, &obj.manager())
                .present(Some(&self.window()));
        }

        #[template_callback]
        pub fn handle_edited_clicked(&self) {
            let obj = self.obj();
            let msg = obj.message();
            MessageInfoDialog::new(&msg, MessageInfoDialogPage::Edits, &obj.manager())
                .present(Some(&self.window()));
        }

        #[template_callback]
        pub fn handle_receipt_clicked(&self) {
            let obj = self.obj();
            let msg = obj.message();
            MessageInfoDialog::new(&msg, MessageInfoDialogPage::Receipts, &obj.manager())
                .present(Some(&self.window()));
        }

        #[template_callback]
        pub fn open_emoji_picker(&self) {
            let obj = self.obj();

            let emoji_chooser = EmojiChooser::new();
            let (_, rectangle) = self.msg_menu.pointing_to();
            emoji_chooser.set_pointing_to(Some(&rectangle));

            emoji_chooser.connect_emoji_picked(clone!(
                #[weak]
                obj,
                move |_, emoji| {
                    obj.imp().handle_react(emoji.to_owned());
                }
            ));
            emoji_chooser.connect_local("closed", false, move |values| {
                let chooser = values[0]
                    .get::<EmojiChooser>()
                    .expect("Closed of EmojiChooser return value to be EmojiChooser");
                chooser.unparent();
                None
            });

            self.message_bubble.append(&emoji_chooser);

            self.msg_menu.popdown();
            emoji_chooser.popup();
        }

        #[template_callback]
        pub(super) fn handle_reply(&self) {
            let obj = self.obj();
            let msg = obj.message();
            log::trace!(
                "Replying to a message: {}",
                msg.textual_description().unwrap_or("No Text".to_string())
            );
            obj.emit_by_name::<()>("reply", &[&msg]);
        }

        #[template_callback]
        pub(super) fn handle_delete(&self) {
            let obj = self.obj();
            let msg = obj.message();
            let manager = obj.manager();

            gspawn!(async move {
                manager
                    .send_delete_for_everyone(msg.channel().thread(), msg.id())
                    .await;
            });
        }

        // Signal uses the old unicode for the heart emoji, which is recognized as a black heart by gtk. This function converts it to the standard red heart
        #[template_callback(function)]
        pub(super) fn fix_emoji(emoji: Option<String>) -> Option<String> {
            if emoji == Some(String::from("❤")) {
                Some(String::from("❤\u{fe0f}"))
            } else {
                emoji
            }
        }

        #[template_callback]
        pub(super) fn handle_react(&self, emoji: String) {
            let obj = self.obj();
            let manager = obj.manager();
            let msg = obj.message();

            log::trace!(
                "Reacting to message {} with {}",
                msg.body().unwrap_or_default(),
                emoji
            );

            let obj = self.obj();
            gspawn!(clone!(
                #[strong]
                msg,
                #[strong]
                manager,
                async move {
                    log::trace!("Sending message");
                    manager
                        .send_reaction(msg.channel().thread(), msg.id(), emoji)
                        .await;
                }
            ));
            obj.imp().msg_menu.popdown();
        }

        #[template_callback]
        pub(super) fn handle_copy(&self) {
            let obj = self.obj();
            let display = gdk::Display::default().expect("there should be a display");
            let clipboard = display.clipboard();
            let msg = obj.message();
            if let Some(text) = msg.body() {
                clipboard.set_text(&text)
            }
        }

        fn window(&self) -> crate::gui::window::Window {
            self.obj()
                .root()
                .expect("`MessageItem` to have a root")
                .dynamic_cast::<crate::gui::Window>()
                .expect("Root of `MessageItem` to be a `Window`.")
        }

        #[template_callback]
        pub(super) fn load_media(&self) {
            let obj = self.obj();
            let msg = obj.message();
            let btn = &obj.imp().download_btn;
            btn.set_can_target(false);
            let spinner = gtk::Spinner::new();
            spinner.start();
            btn.set_child(Some(&spinner));
            for att in msg.attachments() {
                gspawn!(async move { att.load().await });
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for MessageItem {
        fn constructed(&self) {
            self.parent_constructed();
            self.obj().setup_actions();
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| -> Vec<Signal> {
                vec![
                    Signal::builder("reply")
                        .param_types([Message::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }
    }

    impl WidgetImpl for MessageItem {}
    impl BinImpl for MessageItem {}
    impl ContextMenuBinImpl for MessageItem {
        fn menu_opened(&self) {
            let obj = self.obj();
            if !obj.for_display() {
                let popover = obj.get_popover();
                obj.set_popover(Some(popover));
            }
        }
    }
}
