use anyhow::{Context, Result};
use gtk::{
    gio,
    glib::{self, clone},
    prelude::*,
    subclass::prelude::*,
};
use zbus::{fdo::DBusProxy, names::BusName, proxy::Defaults};

use crate::{
    bus_name_item::{BusNameItem, LookupPoint},
    every_filter::EveryFilter,
    filtered_bus_name_model::FilteredBusNameModel,
    message::Message,
    message_list::MessageList,
    message_tag::MessageTag,
    multi_set::MultiSet,
};

mod imp {
    use std::cell::RefCell;

    use indexmap::IndexSet;

    use super::*;

    #[derive(Default)]
    pub struct FilteredMessageModel {
        pub(super) inner: gtk::FilterListModel,
        pub(super) inner_index: RefCell<IndexSet<Message>>,

        pub(super) filtered_bus_names: FilteredBusNameModel,

        pub(super) used_bus_names: RefCell<MultiSet<BusName<'static>>>,

        pub(super) message_tag_filter: EveryFilter,
        pub(super) bus_name_filter: EveryFilter,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for FilteredMessageModel {
        const NAME: &'static str = "BustleFilteredMessageModel";
        type Type = super::FilteredMessageModel;
        type Interfaces = (gio::ListModel,);
    }

    impl ObjectImpl for FilteredMessageModel {
        fn constructed(&self) {
            self.parent_constructed();

            let obj = self.obj();

            let bus_names_filter = gtk::CustomFilter::new(clone!(
                #[weak]
                obj,
                #[upgrade_or_panic]
                move |bus_name_item| {
                    let bus_name_item = bus_name_item.downcast_ref::<BusNameItem>().unwrap();
                    obj.bus_names_filter_func(bus_name_item)
                }
            ));
            self.inner.connect_items_changed(clone!(
                #[weak]
                obj,
                #[weak]
                bus_names_filter,
                move |_, position, removed, added| {
                    obj.update_inner_index(position, removed, added);
                    bus_names_filter.changed(gtk::FilterChange::Different);
                    obj.items_changed(position, removed, added);
                }
            ));
            self.filtered_bus_names.set_filter(Some(&bus_names_filter));

            let filter = gtk::EveryFilter::new();
            filter.append(gtk::CustomFilter::new(|message| {
                let message = message.downcast_ref::<Message>().unwrap();

                // TODO: define the exact rules and document why are these filtered out
                &message.destination() != DBusProxy::DESTINATION
                    && message.sender().as_deref() != DBusProxy::DESTINATION.as_deref()
            }));
            filter.append(self.message_tag_filter.clone());
            filter.append(self.bus_name_filter.clone());
            self.inner.set_filter(Some(&filter));
        }
    }

    impl ListModelImpl for FilteredMessageModel {
        fn item_type(&self) -> glib::Type {
            Message::static_type()
        }

        fn n_items(&self) -> u32 {
            self.inner.n_items()
        }

        fn item(&self, position: u32) -> Option<glib::Object> {
            self.inner.item(position)
        }
    }
}

glib::wrapper! {
    pub struct FilteredMessageModel(ObjectSubclass<imp::FilteredMessageModel>)
        @implements gio::ListModel;
}

impl FilteredMessageModel {
    /// This also resets the filters
    pub fn set_message_list(&self, message_list: Option<&MessageList>) {
        self.remove_all_bus_name_filters();
        self.remove_all_message_tag_filters();

        let imp = self.imp();

        // Must set the model for `inner` first to have used names updated
        imp.inner.set_model(message_list);

        imp.filtered_bus_names
            .set_bus_name_list(message_list.map(|l| l.bus_names()));
    }

    pub fn message_list(&self) -> Option<MessageList> {
        self.imp()
            .inner
            .model()
            .map(|model| model.downcast().unwrap())
    }

    pub fn get_index_of(&self, message: &Message) -> Option<usize> {
        self.imp().inner_index.borrow().get_index_of(message)
    }

    pub fn iter(&self) -> impl ExactSizeIterator<Item = Message> + DoubleEndedIterator + '_ {
        ListModelExtManual::iter(self).map(|item| item.unwrap())
    }

    /// Returns the filtered bus names from self's message list with respect to
    /// self's filters
    pub fn filtered_bus_names(&self) -> &FilteredBusNameModel {
        &self.imp().filtered_bus_names
    }

    /// Adds filter that filters out messages with the given tag
    pub fn add_message_tag_filter(&self, message_tag: MessageTag) {
        let imp = self.imp();

        let filter_id = message_tag.to_string();

        if imp.message_tag_filter.contains(&filter_id) {
            return;
        }

        let custom_filter = gtk::CustomFilter::new(move |message| {
            let message = message.downcast_ref::<Message>().unwrap();
            message.message_tag() != message_tag
        });
        let is_unique = imp.message_tag_filter.insert(filter_id, custom_filter);
        debug_assert!(is_unique);
    }

    /// Removes the filter that filters out messages with the given tag
    pub fn remove_message_tag_filter(&self, message_tag: MessageTag) {
        let imp = self.imp();

        let filter_id = message_tag.to_string();

        let was_removed = imp.message_tag_filter.remove(&filter_id);
        debug_assert!(was_removed);
    }

    pub fn remove_all_message_tag_filters(&self) {
        let imp = self.imp();

        imp.message_tag_filter.clear();
    }

    /// Adds filter that filters out messages relevant to `BusNameItem` with
    /// `bus_name` equals given `name`
    ///
    /// Returns an error if self has no `message_list`
    pub fn add_bus_name_filter(&self, name: &BusName<'_>) -> Result<()> {
        let imp = self.imp();

        if imp.bus_name_filter.contains(name) {
            return Ok(());
        }

        let bus_name_item = self
            .message_list()
            .context("Message list was not set")?
            .bus_names()
            .get(name)
            .unwrap();
        let custom_filter = gtk::CustomFilter::new(move |message| {
            let message = message.downcast_ref::<Message>().unwrap();
            let name = bus_name_item.name();
            message.sender().is_none_or(|sender| *name != sender)
                && !message.destination().is_some_and(|destination| {
                    *name == destination
                        || match destination {
                            BusName::Unique(_) => false,
                            BusName::WellKnown(wk_name) => bus_name_item
                                .wk_names(message.receive_index().into())
                                .contains(&wk_name),
                        }
                })
        });

        let is_unique = imp.bus_name_filter.insert(name.to_string(), custom_filter);
        debug_assert!(is_unique);

        Ok(())
    }

    /// Removes the filter that is relevant to `BusNameItem` with `bus_name`
    /// equals given `name`
    ///
    /// Returns true if the filter existed and removed
    pub fn remove_bus_name_filter(&self, name: &BusName<'static>) {
        let imp = self.imp();

        let was_removed = imp.bus_name_filter.remove(name);
        debug_assert!(was_removed);
    }

    pub fn remove_all_bus_name_filters(&self) {
        let imp = self.imp();

        imp.bus_name_filter.clear();
    }

    fn bus_names_filter_func(&self, bus_name_item: &BusNameItem) -> bool {
        let used_names = self.imp().used_bus_names.borrow();

        used_names.contains(bus_name_item.name())
            || bus_name_item
                .wk_names(LookupPoint::All)
                .iter()
                .any(|wk_name| used_names.contains(&BusName::from(wk_name.as_ref())))
    }

    fn update_inner_index(&self, position: u32, removed: u32, added: u32) {
        let imp = self.imp();

        let mut inner_index = imp.inner_index.borrow_mut();
        let mut used_bus_names = imp.used_bus_names.borrow_mut();

        let added_items = (0..added)
            .map(|i| {
                imp.inner
                    .item(position + i)
                    .unwrap()
                    .downcast::<Message>()
                    .unwrap()
            })
            .collect::<Vec<_>>();
        let used_sender_names = added_items
            .iter()
            .filter_map(|message| message.sender().map(|s| BusName::from(s.to_owned())));
        let used_destination_names = added_items
            .iter()
            .filter_map(|message| message.destination().map(|d| d.to_owned()));
        used_bus_names.extend(used_sender_names.chain(used_destination_names));

        let removed_items = inner_index
            .splice(
                position as usize..(removed + position) as usize,
                added_items,
            )
            .collect::<Vec<_>>();
        let removed_sender_names = removed_items
            .iter()
            .filter_map(|message| message.sender().map(|s| BusName::from(s.to_owned())));
        let removed_destination_names = removed_items
            .iter()
            .filter_map(|message| message.destination().map(|d| d.to_owned()));
        removed_sender_names
            .chain(removed_destination_names)
            .for_each(|bus_name| {
                used_bus_names.remove(bus_name);
            });

        // Ensure that `inner_index` items are exactly the same as the items in `inner`
        debug_assert_eq!(inner_index.len(), imp.inner.n_items() as usize);
        debug_assert!(
            self.iter()
                .zip(inner_index.iter())
                .all(|(inner_item, inner_index_item)| inner_item == *inner_index_item)
        );

        // Ensure that all bus names in `inner_index` are in `used_bus_names`.
        debug_assert!(self.iter().all(|message| {
            [message.sender().map(BusName::from), message.destination()]
                .iter()
                .flatten()
                .all(|bus_name| used_bus_names.contains(bus_name))
        }));

        // Ensure that only bus names in `inner_index` are in `used_bus_names`. Note:
        // This assertion is very expensive.
        debug_assert!(used_bus_names.iter_unique().all(|bus_name| {
            self.iter().any(|message| {
                message.sender().is_some_and(|s| *bus_name == s)
                    || message.destination().is_some_and(|d| *bus_name == d)
            })
        }));
    }
}
