use std::{cell::Cell, collections::HashSet};

use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use indexmap::map::IndexMap;
use log::error;
use matrix_sdk::{
    ruma::{OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomOrAliasId},
    sync::Rooms as ResponseRooms,
};

use crate::{
    gettext_f,
    session::{room::Room, Session},
    spawn, spawn_tokio, toast,
};

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

    use glib::{object::WeakRef, subclass::Signal};
    use once_cell::sync::Lazy;

    use super::*;

    #[derive(Debug, Default)]
    pub struct RoomList {
        pub list: RefCell<IndexMap<OwnedRoomId, Room>>,
        pub pending_rooms: RefCell<HashSet<OwnedRoomOrAliasId>>,
        pub session: WeakRef<Session>,
    }

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

    impl ObjectImpl for RoomList {
        fn properties() -> &'static [glib::ParamSpec] {
            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
                vec![glib::ParamSpecObject::builder::<Session>("session")
                    .construct_only()
                    .build()]
            });

            PROPERTIES.as_ref()
        }

        fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
            match pspec.name() {
                "session" => self.session.set(value.get().ok().as_ref()),
                _ => unimplemented!(),
            }
        }

        fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
            match pspec.name() {
                "session" => self.obj().session().to_value(),
                _ => unimplemented!(),
            }
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> =
                Lazy::new(|| vec![Signal::builder("pending-rooms-changed").build()]);
            SIGNALS.as_ref()
        }
    }

    impl ListModelImpl for RoomList {
        fn item_type(&self) -> glib::Type {
            Room::static_type()
        }
        fn n_items(&self) -> u32 {
            self.list.borrow().len() as u32
        }
        fn item(&self, position: u32) -> Option<glib::Object> {
            self.list
                .borrow()
                .get_index(position as usize)
                .map(|(_, v)| v.upcast_ref::<glib::Object>())
                .cloned()
        }
    }
}

glib::wrapper! {
    /// List of all joined rooms of the user.
    ///
    /// This is the parent ListModel of the sidebar from which all other models
    /// are derived. If a room is updated in an order-relevant manner, use
    /// `room.emit_by_name::<()>("order-changed", &[])` to fix the sorting.
    ///
    /// The `RoomList` also takes care of all so called *pending rooms*, i.e.
    /// rooms the user requested to join, but received no response from the
    /// server yet.
    pub struct RoomList(ObjectSubclass<imp::RoomList>)
        @implements gio::ListModel;
}

impl RoomList {
    pub fn new(session: &Session) -> Self {
        glib::Object::builder().property("session", session).build()
    }

    /// The current session.
    pub fn session(&self) -> Session {
        self.imp().session.upgrade().unwrap()
    }

    pub fn is_pending_room(&self, identifier: &RoomOrAliasId) -> bool {
        self.imp().pending_rooms.borrow().contains(identifier)
    }

    fn pending_rooms_remove(&self, identifier: &RoomOrAliasId) {
        self.imp().pending_rooms.borrow_mut().remove(identifier);
        self.emit_by_name::<()>("pending-rooms-changed", &[]);
    }

    fn pending_rooms_insert(&self, identifier: OwnedRoomOrAliasId) {
        self.imp().pending_rooms.borrow_mut().insert(identifier);
        self.emit_by_name::<()>("pending-rooms-changed", &[]);
    }

    fn pending_rooms_replace_or_remove(&self, identifier: &RoomOrAliasId, room_id: &RoomId) {
        {
            let mut pending_rooms = self.imp().pending_rooms.borrow_mut();
            pending_rooms.remove(identifier);
            if !self.contains_key(room_id) {
                pending_rooms.insert(room_id.to_owned().into());
            }
        }
        self.emit_by_name::<()>("pending-rooms-changed", &[]);
    }

    pub fn get(&self, room_id: &RoomId) -> Option<Room> {
        self.imp().list.borrow().get(room_id).cloned()
    }

    /// Waits till the Room becomes available
    pub async fn get_wait(&self, room_id: &RoomId) -> Option<Room> {
        if let Some(room) = self.get(room_id) {
            Some(room)
        } else {
            let (sender, receiver) = futures::channel::oneshot::channel();

            let room_id = room_id.to_owned();
            let sender = Cell::new(Some(sender));
            // FIXME: add a timeout
            let handler_id = self.connect_items_changed(move |obj, _, _, _| {
                if let Some(room) = obj.get(&room_id) {
                    if let Some(sender) = sender.take() {
                        sender.send(Some(room)).unwrap();
                    }
                }
            });

            let room = receiver.await.unwrap();
            self.disconnect(handler_id);
            room
        }
    }

    fn get_full(&self, room_id: &RoomId) -> Option<(usize, OwnedRoomId, Room)> {
        self.imp()
            .list
            .borrow()
            .get_full(room_id)
            .map(|(pos, room_id, room)| (pos, room_id.clone(), room.clone()))
    }

    pub fn contains_key(&self, room_id: &RoomId) -> bool {
        self.imp().list.borrow().contains_key(room_id)
    }

    pub fn remove(&self, room_id: &RoomId) {
        let removed = {
            let mut list = self.imp().list.borrow_mut();

            list.shift_remove_full(room_id)
        };

        if let Some((position, ..)) = removed {
            self.items_changed(position as u32, 1, 0);
        }
    }

    fn items_added(&self, added: usize) {
        let list = self.imp().list.borrow();

        let position = list.len() - added;

        for (_room_id, room) in list.iter().skip(position) {
            room.connect_order_changed(clone!(@weak self as obj => move |room| {
                if let Some((position, _, _)) = obj.get_full(room.room_id()) {
                    obj.items_changed(position as u32, 1, 1);
                }
            }));
            room.connect_room_forgotten(clone!(@weak self as obj => move |room| {
                obj.remove(room.room_id());
            }));
        }

        self.items_changed(position as u32, 0, added as u32);
    }

    /// Loads the state from the `Store`.
    ///
    /// Note that the `Store` currently doesn't store all events, therefore, we
    /// aren't really loading much via this function.
    pub fn load(&self) {
        let session = self.session();
        let client = session.client();
        let matrix_rooms = client.rooms();
        let added = matrix_rooms.len();

        if added > 0 {
            {
                let mut list = self.imp().list.borrow_mut();
                for matrix_room in matrix_rooms {
                    let room_id = matrix_room.room_id().to_owned();
                    let room = Room::new(&session, &room_id);
                    list.insert(room_id, room);
                }
            }

            self.items_added(added);
        }
    }

    pub fn handle_response_rooms(&self, rooms: ResponseRooms) {
        let imp = self.imp();
        let session = self.session();

        let mut added = 0;

        for (room_id, left_room) in rooms.leave {
            let room = imp
                .list
                .borrow_mut()
                .entry(room_id.clone())
                .or_insert_with_key(|room_id| {
                    added += 1;
                    Room::new(&session, room_id)
                })
                .clone();

            self.pending_rooms_remove((*room_id).into());
            room.handle_left_response(left_room);
        }

        for (room_id, joined_room) in rooms.join {
            let room = imp
                .list
                .borrow_mut()
                .entry(room_id.clone())
                .or_insert_with_key(|room_id| {
                    added += 1;
                    Room::new(&session, room_id)
                })
                .clone();

            self.pending_rooms_remove((*room_id).into());
            room.handle_joined_response(joined_room);
        }

        for (room_id, invited_room) in rooms.invite {
            let room = imp
                .list
                .borrow_mut()
                .entry(room_id.clone())
                .or_insert_with_key(|room_id| {
                    added += 1;
                    Room::new(&session, room_id)
                })
                .clone();

            self.pending_rooms_remove((*room_id).into());
            room.handle_invited_response(invited_room);
        }

        if added > 0 {
            self.items_added(added);
        }
    }

    pub fn join_by_id_or_alias(&self, identifier: OwnedRoomOrAliasId, via: Vec<OwnedServerName>) {
        let client = self.session().client();
        let identifier_clone = identifier.clone();

        self.pending_rooms_insert(identifier.clone());

        let handle = spawn_tokio!(async move {
            client
                .join_room_by_id_or_alias(&identifier_clone, &via)
                .await
        });

        spawn!(
            glib::PRIORITY_DEFAULT_IDLE,
            clone!(@weak self as obj => async move {
                match handle.await.unwrap() {
                    Ok(matrix_room) => obj.pending_rooms_replace_or_remove(&identifier, matrix_room.room_id()),
                    Err(error) => {
                        obj.pending_rooms_remove(&identifier);
                        error!("Joining room {} failed: {}", identifier, error);

                        let error = gettext_f(
                            // Translators: Do NOT translate the content between '{' and '}', this is a variable name.
                            "Failed to join room {room_name}. Try again later.",
                            &[("room_name", identifier.as_str())]
                        );

                        toast!(obj.session(), error);
                    }
                }
            })
        );
    }

    pub fn connect_pending_rooms_changed<F: Fn(&Self) + 'static>(
        &self,
        f: F,
    ) -> glib::SignalHandlerId {
        self.connect_local("pending-rooms-changed", true, move |values| {
            let obj = values[0].get::<Self>().unwrap();

            f(&obj);

            None
        })
    }

    pub fn find_joined_room(&self, room_id: &RoomOrAliasId) -> Option<Room> {
        let room_id = room_id.as_str();
        self.imp()
            .list
            .borrow()
            .values()
            .find(|room| {
                (room.room_id() == room_id
                    || room
                        .matrix_room()
                        .canonical_alias()
                        .filter(|id| id == room_id)
                        .is_some())
                    && room.is_joined()
            })
            .cloned()
    }
}
