use crate::backend::{Contact, Manager, Message};
use crate::prelude::*;

use std::hash::Hash;

use ftypes::{AccountId, ThreadId};
use futures::StreamExt;
use glib::{Object, prelude::Cast};

gtk::glib::wrapper! {
    /// A channel represents either a 1-to-1 channel or a group.
    pub struct Channel(ObjectSubclass<imp::Channel>);
}

const TYPING_NOTIFICATION_DURATION_SECONDS: u32 = 10;

#[derive(Eq, Hash, PartialEq, Clone, Debug)]
pub struct TypingNotification {
    pub sender: Contact,
    pub timestamp: u64,
}

impl Channel {
    pub(super) fn from_ftypes(inner: ftypes::Thread, manager: &Manager) -> Self {
        let s: Self = Object::builder::<Self>()
            .property("manager", manager)
            .build();
        s.imp().inner.borrow_mut().replace(inner);
        s
    }

    pub(super) fn set_ftypes(&self, inner: ftypes::Thread) {
        self.imp().inner.borrow_mut().replace(inner);

        for prop in self.list_properties() {
            self.notify(prop.name());
        }
    }

    pub(super) fn inner(&self) -> ftypes::Thread {
        self.imp()
            .inner
            .borrow_mut()
            .as_ref()
            .expect("Channel to have an inner thread")
            .clone()
    }

    pub fn thread(&self) -> ThreadId {
        self.imp()
            .inner
            .borrow()
            .as_ref()
            .expect("Channel inner to be set")
            .id()
    }

    /// Load the specified number of message from the end of the message queue.
    ///
    /// Returns `true` if messages are loaded, `false` otherwise.
    pub async fn load_last(&self, number: usize) -> bool {
        let thread = self.thread();
        let mut results = vec![];
        let manager = self.manager();

        let first_timestamp = self
            .imp()
            .timeline
            .borrow()
            .iter_forwards()
            .map(|m| m.timestamp())
            .next();
        log::trace!(
            "Loading {} last messages for channel: {} (Thread: {:?}). Needed earlier then {:?}",
            number,
            self.title(),
            thread,
            first_timestamp
        );
        let iter = manager.messages(&thread, first_timestamp).await;
        if iter.is_err() {
            log::error!("Failed to load last messages: {}", iter.err().unwrap());
            return false;
        }

        let iter = iter.unwrap();
        futures::pin_mut!(iter);

        let mut count = 0;

        while let Some(msg) = iter.next().await {
            match msg {
                Ok(msg) => {
                    results.insert(0, msg.clone());
                    // Mark message from storage as already read.
                    // TODO: Mark as read instead using sync messages, and store read state in DB.
                    msg.mark_as_read();

                    let _ = self.do_new_message(&msg).await;

                    if !msg.is_deleted() {
                        count += 1;
                    }
                }
                Err(e) => {
                    log::error!("Failed to load a message: {e}")
                }
            }

            if count == number {
                break;
            }
        }

        let loaded = !results.is_empty();

        self.imp().timeline.borrow().prepend(
            results
                .into_iter()
                .map(|i| {
                    i.dynamic_cast::<Message>()
                        .expect("A 'Message' to be a 'Message'")
                })
                .collect(),
        );
        self.notify("last-message");

        loaded
    }

    pub fn trim_old(&self) {
        if let Some(message) = self.last_message() {
            self.imp()
                .timeline
                .borrow()
                .trim_older_than(message.id().timestamp);
        }
    }

    pub async fn clear_messages(&self) -> Result<(), ApplicationError> {
        self.manager().clear_channel_messages(self.thread()).await?;
        self.imp().timeline.borrow().clear();
        Ok(())
    }

    pub fn account_id(&self) -> Option<AccountId> {
        self.thread().contact()
    }

    pub async fn send_session_reset(&self) -> Result<(), ApplicationError> {
        log::trace!("Sending session reset");
        let Some(account_id) = self.account_id() else {
            return Ok(());
        };
        self.manager().send_session_reset(account_id).await
    }

    /// Register a new message with the channel.
    /// This does the following:
    /// - Mark the message as read if the current channel is active.
    pub(super) async fn do_new_message(
        &self,
        message: &Message,
    ) -> Result<(), gtk::glib::error::BoolError> {
        if self.property("is-active") || message.sender().is_self() {
            message.mark_as_read();
        }

        Ok(())
    }

    /// Add a message to the channel, which includes putting it into the timeline, notifying it and updating the last message.
    pub(super) async fn new_message(
        &self,
        message: Message,
    ) -> Result<(), gtk::glib::error::BoolError> {
        log::trace!("Adding new message to channel");
        self.do_new_message(&message).await?;
        if let Some(message) = message.dynamic_cast_ref::<Message>() {
            self.imp().timeline.borrow().append(
                message
                    .clone()
                    .dynamic_cast::<Message>()
                    .expect("A 'Message' to be a 'Message'"),
            );

            self.notify("last-message");
            if !message.property::<bool>("read") {
                message.send_notification();
            }
            self.emit_by_name::<()>("message", &[&message]);
        } else {
            log::trace!("Channel skip adding empty message");

            self.imp().timeline.borrow().append(
                message
                    .clone()
                    .dynamic_cast::<Message>()
                    .expect("A 'Message' to be a 'Message'"),
            );
        }
        Ok(())
    }

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

    /// Cap the description to a single line.
    pub fn single_line_description(&self) -> Option<String> {
        Utility::single_line(self.description())
    }

    pub fn add_user_typing(&self, notification: TypingNotification) {
        let _ = self
            .imp()
            .typing
            .borrow_mut()
            .insert(notification.sender.account_id(), notification.clone());
        gspawn!(clone!(
            #[weak(rename_to = s)]
            self,
            #[strong]
            notification,
            async move {
                glib::timeout_future_seconds(TYPING_NOTIFICATION_DURATION_SECONDS).await;

                let mut typing = s.imp().typing.borrow_mut();
                if typing
                    .get(&notification.sender.account_id())
                    .map(|t| t.timestamp)
                    == Some(notification.timestamp)
                {
                    typing.remove(&notification.sender.account_id());
                    drop(typing);
                    s.notify("is-typing");
                    s.notify("typing-label");
                }
            }
        ));
        self.notify("is-typing");
        self.notify("typing-label");
    }

    pub fn remove_user_typing(&self, contact: Contact) {
        let mut typing = self.imp().typing.borrow_mut();
        typing.remove(&contact.account_id());
        drop(typing);
        self.notify("is-typing");
        self.notify("typing-label");
    }

    pub fn sort_name(&self) -> String {
        self.title()
    }

    /// Whether the channel is currently visible in the UI.
    pub fn set_active(&self, active: bool) {
        self.set_property("is-active", active);
    }

    fn first_unread_message(&self) -> Option<Message> {
        self.imp()
            .timeline
            .borrow()
            .iter_backwards()
            .map_while(|m| {
                let message = m.dynamic_cast::<Message>().unwrap();
                // We can stop at first read message
                if !message.property::<bool>("read") {
                    return Some(message);
                }
                None
            })
            .last()
    }

    /// Mark all messages as read.
    pub fn mark_as_read(&self) -> Vec<Message> {
        if let Some(first_unread) = self.first_unread_message() {
            first_unread.flash_requires_attention();
        }

        self.imp()
            .timeline
            .borrow()
            .iter_backwards()
            .filter(|i| i.is::<Message>())
            .map_while(|m| {
                let message = m.dynamic_cast::<Message>().unwrap();
                // We can stop at first read message
                if message.mark_as_read() {
                    return Some(message);
                }
                None
            })
            .collect()
    }
}

mod imp {
    use super::TypingNotification;
    use crate::backend::{Contact, Manager, message::Message, timeline::Timeline};
    use crate::prelude::*;

    use std::{
        collections::{BTreeSet, HashMap},
        marker::PhantomData,
    };

    use ftypes::AccountId;
    use gdk::Paintable;

    #[derive(Default, glib::Properties)]
    #[properties(wrapper_type = super::Channel)]
    pub struct Channel {
        #[property(name = "title", type = String, get = |s: &Self| s.inner.borrow().as_ref().expect("Channel inner to be set").title().unwrap_or_else(|| gettextrs::gettext("Unknown Contact")))]
        pub(super) inner: RefCell<Option<ftypes::Thread>>,

        pub(super) participants: RefCell<Vec<Contact>>,

        pub(super) typing: RefCell<HashMap<AccountId, TypingNotification>>,

        #[property(name = "avatar", get, set)]
        pub(super) avatar: RefCell<Option<Paintable>>,
        #[property(get)]
        pub(super) timeline: RefCell<Timeline>,

        #[property(get, set)]
        pub(super) draft: RefCell<String>,
        #[property(get, set)]
        pub(super) is_active: RefCell<bool>,

        #[property(get = Self::last_message)]
        pub(super) last_message: PhantomData<Option<Message>>,
        #[property(get = Self::is_contact)]
        pub(super) is_contact: PhantomData<bool>,
        #[property(get = Self::is_group)]
        pub(super) is_group: PhantomData<bool>,
        #[property(get = Self::is_self)]
        pub(super) is_self: PhantomData<bool>,
        /// In seconds. A value of 0 means messages don't disappear.
        #[property(get = Self::disappearing_messages_timer)]
        pub(super) disappearing_messages_timer: PhantomData<u32>,
        #[property(get = Self::phone_number)]
        pub(super) phone_number: PhantomData<Option<String>>,
        #[property(get = Self::description)]
        pub(super) description: PhantomData<Option<String>>,
        #[property(get = Self::is_typing)]
        pub(super) is_typing: PhantomData<bool>,
        #[property(get = Self::typing_label)]
        pub(super) typing_label: PhantomData<String>,

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

    impl Channel {
        fn last_message(&self) -> Option<Message> {
            self.timeline
                .borrow()
                .iter_backwards()
                .map(|m| m.dynamic_cast::<Message>().unwrap())
                .find(|m| {
                    !(*m)
                        .clone()
                        .downcast::<Message>()
                        .map(|m| m.is_deleted())
                        .unwrap_or(false)
                })
        }

        fn is_contact(&self) -> bool {
            self.obj().thread().group().is_none()
        }

        fn is_group(&self) -> bool {
            self.obj().thread().group().is_some()
        }

        fn is_self(&self) -> bool {
            Some(self.obj().manager().account_id()) == self.obj().thread().contact()
        }

        fn disappearing_messages_timer(&self) -> u32 {
            match self.obj().inner().disappearing_messages_timer() {
                ftypes::Timer::Off => 0,
                ftypes::Timer::On(time_delta) => time_delta.num_seconds() as u32,
            }
        }

        fn phone_number(&self) -> Option<String> {
            self.obj()
                .inner()
                .contact()
                .and_then(|c| c.phone_number.as_ref())
                .map(|p| p.to_string())
        }

        fn description(&self) -> Option<String> {
            self.obj().inner().description()
        }

        fn is_typing(&self) -> bool {
            !self.typing.borrow().is_empty()
        }

        fn typing_label(&self) -> String {
            if self.is_self() {
                gettextrs::gettext("is typing")
            } else {
                let contacts: BTreeSet<String> = self
                    .typing
                    .borrow()
                    .values()
                    .map(|n| n.sender.title())
                    .collect();
                let num_typing = contacts.len();
                let joined = contacts.into_iter().collect::<Vec<_>>().join(", ");
                gettextrs::ngettext("{} is typing", "{} are typing", num_typing as u32)
                    .replace("{}", &joined)
            }
        }
    }

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

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