use super::{Contact, Manager};
use crate::backend::{Attachment, Channel};
use crate::prelude::*;
use ftypes::{AmendmentMessage, MessageId, ReactionOperation};
use gtk::pango::{AttrColor, AttrFontDesc, AttrInt, AttrList, Attribute, FontDescription, Weight};

use glib::{
    Object,
    subclass::{prelude::ObjectImpl, types::ObjectSubclassIsExt},
};

const MENTION_CHAR: char = '@';
const MENTION_COLOR: (u16, u16, u16) = (0, 0, u16::MAX);

/// At least 4 minutes need to pass such that for two messages from the same sender, the second one will
/// also show avatar and sender title.
const MESSAGE_SENT_SHOW_NAME_DURATION: u64 = 4 * 60 * 1000;
/// At least 1 minute need to pass such that for two messages from the same sender, the second one will
/// also show the timestamp.
const MESSAGE_SENT_SHOW_TIMESTAMP_DURATION: u64 = 60 * 1000;

glib::wrapper! {
    pub struct Message(ObjectSubclass<imp::Message>);
}

impl Message {
    pub(super) async fn from_ftypes(
        inner: ftypes::RichMessage,
        manager: &Manager,
    ) -> Result<Option<Self>, ftypes::Error> {
        let Some(channel) = manager.channel_from_id(inner.thread).await? else {
            log::warn!("Cannot find a channel referenced by a message:");
            log::warn!("{:#?}", inner);
            return Ok(None);
        };
        let Some(contact) = manager.contact_from_id(inner.sender()).await? else {
            log::warn!("Cannot find the sender referenced by a message:");
            log::warn!("{:#?}", inner);
            return Ok(None);
        };

        let quote = match &inner.content.inner {
            ftypes::MainMessageContent::Text(text_message) => text_message.quote.as_ref(),
            _ => None,
        };
        let quote = if let Some(quote) = quote {
            // TODO: Error?
            Box::pin(manager.message(quote.quoted_message))
                .await
                .ok()
                .flatten()
        } else {
            None
        };

        let s = Object::builder::<Message>()
            .property("manager", manager)
            .property("sender", &contact)
            .property("channel", &channel)
            .property("quote", &quote)
            .property("read", channel.property::<bool>("is-active"))
            .build();
        s.imp().inner.borrow_mut().replace(inner);
        s.init_data().await;
        Ok(Some(s))
    }

    pub async fn edit_history(&self) -> Vec<Message> {
        let history = self.inner().edit_history();
        let mut result = Vec::with_capacity(history.len());

        for h in history {
            if let Some(m) = Self::from_ftypes(h.as_rich_message(), &self.manager())
                .await
                .ok()
                .flatten()
            {
                result.push(m);
            } else {
                log::warn!("Cannot convert edit history message to usable type");
            }
        }

        result
    }

    pub(super) fn add_amendment(&self, amendment: &AmendmentMessage) {
        let inner = self.inner();
        *self.imp().inner.borrow_mut() = Some(inner.amend(amendment.clone()));
        gspawn!(clone!(
            #[strong(rename_to = s)]
            self,
            #[strong]
            amendment,
            async move {
                s.init_data().await;
                s.notify_changed();

                if let ftypes::AmendmentMessageContent::Reaction(reaction) = &amendment.content
                    && reaction.operation == ReactionOperation::Add
                    && s.sender().is_self()
                    && s.manager().settings().boolean("notify-reactions")
                {
                    let manager = s.manager();

                    let Ok(Some(channel)) = manager.channel_from_id(amendment.thread).await else {
                        return;
                    };
                    let Ok(Some(sender)) = manager.contact_from_id(amendment.sender()).await else {
                        return;
                    };
                    manager.send_notification_for_channel(
                        None,
                        channel,
                        sender,
                        gettextrs::gettext("Reacted {} to a message")
                            .replace("{}", &reaction.emoji),
                    );
                }
            }
        ));
    }

    pub(super) fn remove_amendment(&self, amendment: &AmendmentMessage) {
        let inner = self.inner();
        *self.imp().inner.borrow_mut() = Some(inner.remove_amendment(amendment.clone()));
        gspawn!(clone!(
            #[strong(rename_to = s)]
            self,
            async move {
                s.init_data().await;
                s.notify_changed();
            }
        ));
    }

    fn notify_changed(&self) {
        for prop in self.list_properties() {
            self.notify(prop.name());
        }
        // Message may have been deleted, in which case the last message of the channel may have changed.
        self.channel().notify_last_message();

        self.emit_by_name::<()>("changed", &[&self]);
    }

    // A unique ID of the message, built from the timestamp and sender.
    pub fn uid(&self) -> Option<String> {
        let timestamp = self.timestamp();
        let sender_uuid = self.sender().account_id().0.raw_uuid();
        Some(format!("{timestamp:x}{sender_uuid:x}"))
    }

    pub fn inner(&self) -> ftypes::RichMessage {
        self.imp()
            .inner
            .borrow()
            .clone()
            .expect("Message to have an inner ftypes message")
    }

    pub fn mark_as_read(&self) -> bool {
        let this = self.imp();
        let read = *this.read.borrow();
        if !read {
            this.read.replace(true);
            self.notify("read");
            return true;
        }
        false
    }

    fn text_message(&self) -> Option<ftypes::TextMessage> {
        self.inner().active_text_message().cloned()
    }

    pub(super) async fn init_data(&self) {
        let obj = self.imp();
        self.prepare_format_body().await;

        // Load attachments in parallel.
        // TODO: Stickers
        let attachment_futures = self
            .text_message()
            .into_iter()
            .flat_map(|m| m.attachments)
            .map(|pointer| async move { Attachment::from_ftypes(&pointer, &self.manager()).await });
        let attachments = futures::future::join_all(attachment_futures).await;
        obj.attachments.swap(&RefCell::new(attachments));
    }

    pub fn attachments(&self) -> Vec<Attachment> {
        self.imp().attachments.borrow().clone()
    }

    pub fn quote_timestamp(&self) -> Option<u64> {
        self.quote().map(|q| q.timestamp())
    }

    async fn prepare_format_body(&self) {
        let (body, attrs) = self.format_body().await;
        *self.imp().formatted_body.borrow_mut() = body;
        *self.imp().message_attributes.borrow_mut() = attrs;
        self.notify_message_attributes();
        self.notify_body();
    }

    /// Formats the message body based on its ranges, e.g. to insert mention names.
    ///
    /// Returns the resulting strings and an [AttrList] that can be used in labels to highlight areas.
    async fn format_body(&self) -> (Option<String>, AttrList) {
        let Some(text_message) = self.text_message() else {
            return (None, AttrList::new());
        };
        let Some(mut body) = text_message.body.clone() else {
            return (None, AttrList::new());
        };
        let mut ranges = text_message.body_ranges.clone();

        if ranges.is_empty() {
            return (Some(body), AttrList::new());
        }

        ranges.sort_unstable_by_key(|r| r.start);

        let attrs = AttrList::new();

        let mut offset = 0;
        for r in ranges {
            match r.associated_value {
                ftypes::AssociatedValue::Mention(contact_id) => {
                    // TODO: Error?
                    let Ok(Some(contact)) = self.manager().contact_from_id(contact_id).await else {
                        log::trace!(
                            "Body got a mention for which the contact could not be found; ignoring"
                        );
                        continue;
                    };
                    let title = contact.title();
                    let replacement = format!("{}{}", MENTION_CHAR, title);
                    let index_start = r.start + offset;
                    let index_end = r.start + r.length + offset;
                    body.replace_range(index_start..index_end, &replacement);

                    let index_start_highlight = index_start;
                    let index_end_highlight = index_start_highlight + replacement.len();
                    let (red, green, blue) = MENTION_COLOR;
                    let mut highlight = AttrColor::new_foreground(red, green, blue);
                    highlight.set_start_index(index_start_highlight as u32);
                    highlight.set_end_index(index_end_highlight as u32);
                    attrs.insert(highlight);

                    offset += replacement.len() - r.length;
                }
                ftypes::AssociatedValue::Style(style) => {
                    let attr: Option<Attribute> = match style {
                        ftypes::BodyStyle::None => None,
                        ftypes::BodyStyle::Bold => {
                            Some((*AttrInt::new_weight(Weight::Bold)).clone())
                        }
                        ftypes::BodyStyle::Italic => Some(
                            (*AttrFontDesc::new(&FontDescription::from_string("italic"))).clone(),
                        ),
                        ftypes::BodyStyle::Spoiler => None,
                        ftypes::BodyStyle::Strikethrough => {
                            Some((*AttrInt::new_strikethrough(true)).clone())
                        }
                        ftypes::BodyStyle::Monospace => Some(
                            (*AttrFontDesc::new(&FontDescription::from_string("monospace")))
                                .clone(),
                        ),
                    };

                    if let Some(mut attr) = attr {
                        let index_start_highlight = r.start + offset;
                        let index_end_highlight = index_start_highlight + r.length;
                        attr.set_start_index(index_start_highlight as u32);
                        attr.set_end_index(index_end_highlight as u32);
                        attrs.insert(attr);
                    }
                }
            }
        }

        (Some(body), attrs)
    }

    /// Send a notification if needed.
    ///
    /// Sending a notification is skipped if:
    /// - The message is from self, or
    /// - The message has no textual description
    pub fn send_notification(&self) {
        self.manager().send_notification_for_channel(
            self.uid(),
            self.channel(),
            self.sender(),
            self.textual_description().unwrap_or_default(),
        );
    }

    pub fn flash_requires_attention(&self) {
        self.set_property("requires-attention", true);
        gspawn!(clone!(
            #[strong(rename_to = s)]
            self,
            async move {
                glib::timeout_future_seconds(2).await;
                s.set_property("requires-attention", false);
            }
        ));
    }

    pub fn id(&self) -> MessageId {
        self.inner().id
    }

    /// Days since 01.01.1970
    pub fn day_timestamp(&self) -> u64 {
        self.timestamp() / (1000 * 60 * 60 * 24)
    }

    pub fn update_show_header(&self, previous: Option<&Message>) {
        if let Some(msg) = previous.and_then(|p| p.downcast_ref::<super::Message>()) {
            // Show header if:
            // - The sender of the previous message is different to the sender of this message, or
            // - Some time has passed between the two messages.
            // Furthermore, never show headers for messages sent by self.
            self.set_show_header(
                (self.sender().account_id() != msg.sender().account_id()
                    || self.timestamp() >= msg.timestamp() + MESSAGE_SENT_SHOW_NAME_DURATION)
                    && self.sender().account_id() != self.manager().account_id()
                    && !self.channel().is_contact(),
            );
        } else {
            self.set_show_header(
                self.sender().account_id() != self.manager().account_id()
                    && !self.channel().is_contact(),
            );
        }
    }

    pub fn update_show_timestamp(&self, next: Option<&Message>) {
        if let Some(msg) = next.and_then(|p| p.downcast_ref::<super::Message>()) {
            // Show timestamp if:
            // - The sender of the previous message is different to the sender of this message, or
            // - Some time has passed between the two messages.
            self.set_show_timestamp(
                self.sender().account_id() != msg.sender().account_id()
                    || msg.timestamp() >= self.timestamp() + MESSAGE_SENT_SHOW_TIMESTAMP_DURATION,
            );
        } else {
            self.set_show_timestamp(true);
        }
    }
}

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

    use gtk::pango::AttrList;

    use crate::backend::{Attachment, Manager};

    use super::*;

    #[derive(Debug, Default, glib::Properties)]
    #[properties(wrapper_type = super::Message)]
    pub struct Message {
        #[property(get, set, construct_only, type = Contact)]
        sender: RefCell<Option<Contact>>,
        #[property(get, set, construct_only, type = Channel)]
        channel: RefCell<Option<Channel>>,
        #[property(get, set, construct_only)]
        pub(super) read: RefCell<bool>,
        #[property(get, set)]
        pub(super) pending: RefCell<bool>,
        #[property(get, set)]
        pub(super) error: RefCell<bool>,

        #[property(name = "timestamp", type = u64, get = |s: &Self| s.inner.borrow().as_ref().map(|m| m.id.timestamp.timestamp_millis()).unwrap_or_default() as u64)]
        #[property(name = "reactions", get = Self::reactions, type = String, default = Some(""))]
        #[property(name = "has-reactions", get = Self::has_reactions, type = bool)]
        #[property(name = "is-unsupported", get = Self::is_unsupported, type = bool)]
        #[property(name = "is-deleted", get = Self::is_deleted, type = bool)]
        #[property(name = "is-edited", get = Self::is_edited, type = bool)]
        #[property(name = "has-delivery-receipt", get = |s: &Self| s.obj().inner().has_delivery_receipt(), type = bool)]
        #[property(name = "has-read-receipt", get = |s: &Self| s.obj().inner().has_read_receipt(), type = bool)]
        pub(super) inner: RefCell<Option<ftypes::RichMessage>>,

        #[property(get, set, construct_only, type = Manager)]
        manager: RefCell<Option<Manager>>,

        #[property(get, set, nullable)]
        pub(super) quote: RefCell<Option<super::Message>>,

        pub(super) attachments: RefCell<Vec<Attachment>>,

        #[property(get = Self::textual_description)]
        pub(super) textual_description: PhantomData<Option<String>>,
        #[property(name = "body", get)]
        pub(super) formatted_body: RefCell<Option<String>>,
        #[property(get)]
        pub(super) message_attributes: RefCell<AttrList>,
        #[property(get, set)]
        pub(super) requires_attention: Cell<bool>,

        #[property(get, set)]
        show_header: Cell<bool>,
        #[property(get, set)]
        show_timestamp: Cell<bool>,

        #[property(get = Self::datetime)]
        datetime: PhantomData<Option<glib::DateTime>>,
    }

    impl Message {
        fn reactions(&self) -> String {
            self.obj()
                .inner()
                .active_reactions()
                .iter()
                .map(|r| r.content.emoji.to_owned())
                .collect()
        }

        fn has_reactions(&self) -> bool {
            !self.obj().inner().active_reactions().is_empty()
        }

        fn is_unsupported(&self) -> bool {
            !matches!(
                self.obj().inner().content.inner,
                ftypes::MainMessageContent::Text(_) | ftypes::MainMessageContent::Deleted
            )
        }

        fn is_deleted(&self) -> bool {
            self.obj().inner().content.deletion.is_some()
        }

        fn is_edited(&self) -> bool {
            !self.obj().inner().content.edits.is_empty()
        }

        fn textual_description(&self) -> Option<String> {
            let obj = self.obj();
            if let Some(body) = obj.body()
                && !body.is_empty()
            {
                body.lines().next().map(|s| s.to_owned())
            } else {
                let attachments = self.attachments.borrow();

                let formatter = if attachments.iter().all(|a| a.is_image()) {
                    gettextrs::ngettext("Sent an image", "Sent {} images", attachments.len() as u32)
                } else if attachments.iter().all(|a| a.is_video()) {
                    gettextrs::ngettext("Sent an video", "Sent {} videos", attachments.len() as u32)
                } else if attachments.iter().all(|a| a.is_audio()) {
                    gettextrs::ngettext(
                        "Sent a voice message",
                        "Sent {} voice messages",
                        attachments.len() as u32,
                    )
                } else {
                    gettextrs::ngettext("Sent a file", "Sent {} files", attachments.len() as u32)
                };

                Some(formatter.replace("{}", &attachments.len().to_string()))
            }
        }

        // Time as [glib::DateTime]. Should not return `None`, but just in case.
        fn datetime(&self) -> Option<glib::DateTime> {
            glib::DateTime::from_unix_utc(
                (self.obj().timestamp() / 1000)
                    .try_into()
                    .unwrap_or_default(),
            )
            .ok()
            .and_then(|d| d.to_local().ok())
        }
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Message {
        const NAME: &'static str = "FlMessage";
        type Type = super::Message;
    }

    #[glib::derived_properties]
    impl ObjectImpl for Message {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| -> Vec<Signal> {
                vec![
                    Signal::builder("changed")
                        .param_types([super::Message::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }
    }
}
