# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.

# XEP-0045: Multi-User Chat
# XEP-0249: Direct MUC Invitations

import logging

import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.const import InviteType
from nbxmpp.const import PresenceType
from nbxmpp.const import StatusCode
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError

from gi.repository import GLib

from gajim.common import app
from gajim.common import helpers
from gajim.common import ged
from gajim.common.const import KindConstant
from gajim.common.const import MUCJoinedState
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import get_default_muc_config
from gajim.common.helpers import to_user_string
from gajim.common.helpers import event_filter
from gajim.common.nec import NetworkEvent
from gajim.common.modules.bits_of_binary import store_bob_data
from gajim.common.modules.base import BaseModule


log = logging.getLogger('gajim.c.m.muc')


class MUC(BaseModule):

    _nbxmpp_extends = 'MUC'
    _nbxmpp_methods = [
        'get_affiliation',
        'set_role',
        'set_affiliation',
        'set_config',
        'set_subject',
        'cancel_config',
        'send_captcha',
        'cancel_captcha',
        'decline',
        'invite',
        'request_config',
        'request_voice',
        'approve_voice_request',
        'destroy',
        'request_disco_info'
    ]

    def __init__(self, con):
        BaseModule.__init__(self, con)

        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._on_muc_user_presence,
                          ns=Namespace.MUC_USER,
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._on_error_presence,
                          typ='error',
                          priority=49),
            StanzaHandler(name='message',
                          callback=self._on_subject_change,
                          typ='groupchat',
                          priority=49),
            StanzaHandler(name='message',
                          callback=self._on_config_change,
                          ns=Namespace.MUC_USER,
                          priority=49),
            StanzaHandler(name='message',
                          callback=self._on_invite_or_decline,
                          typ='normal',
                          ns=Namespace.MUC_USER,
                          priority=49),
            StanzaHandler(name='message',
                          callback=self._on_invite_or_decline,
                          ns=Namespace.CONFERENCE,
                          priority=49),
            StanzaHandler(name='message',
                          callback=self._on_captcha_challenge,
                          ns=Namespace.CAPTCHA,
                          priority=49),
            StanzaHandler(name='message',
                          callback=self._on_voice_request,
                          ns=Namespace.DATA,
                          priority=49)
        ]

        self.register_events([
            ('account-disconnected', ged.CORE, self._on_account_disconnected),
        ])

        self._manager = MUCManager(self._log)
        self._rejoin_muc = set()
        self._join_timeouts = {}
        self._rejoin_timeouts = {}
        self._muc_service_jid = None

    @property
    def supported(self):
        return self._muc_service_jid is not None

    @property
    def service_jid(self):
        return self._muc_service_jid

    def get_manager(self):
        return self._manager

    def pass_disco(self, info):
        for identity in info.identities:
            if identity.category != 'conference':
                continue
            if identity.type != 'text':
                continue
            if Namespace.MUC in info.features:
                self._log.info('Discovered MUC: %s', info.jid)
                self._muc_service_jid = info.jid
                raise nbxmpp.NodeProcessed

    def join(self, muc_data):
        if not app.account_is_available(self._account):
            return

        self._manager.add(muc_data)

        disco_info = app.storage.cache.get_last_disco_info(muc_data.jid,
                                                           max_age=60)
        if disco_info is None:
            self._con.get_module('Discovery').disco_muc(
                muc_data.jid,
                callback=self._on_disco_result)
        else:
            self._join(muc_data)

    def create(self, muc_data):
        if not app.account_is_available(self._account):
            return

        self._manager.add(muc_data)
        self._create(muc_data)

    def _on_disco_result(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.info('Disco %s failed: %s', error.jid, error.get_text())
            app.nec.push_incoming_event(
                NetworkEvent('muc-join-failed',
                             account=self._account,
                             room_jid=error.jid.bare,
                             error=error))
            return

        muc_data = self._manager.get(result.info.jid)
        if muc_data is None:
            self._log.warning('MUC Data not found, join aborted')
            return
        self._join(muc_data)

    def _join(self, muc_data):
        presence = self._con.get_module('Presence').get_presence(
            muc_data.occupant_jid,
            show=self._con.status,
            status=self._con.status_message)

        muc_x = presence.setTag(Namespace.MUC + ' x')
        muc_x.setTag('history', {'maxchars': '0'})

        if muc_data.password is not None:
            muc_x.setTagData('password', muc_data.password)

        self._log.info('Join MUC: %s', muc_data.jid)
        self._manager.set_state(muc_data.jid, MUCJoinedState.JOINING)
        self._con.connection.send(presence)

    def _rejoin(self, room_jid):
        muc_data = self._manager.get(room_jid)
        if muc_data.state == MUCJoinedState.NOT_JOINED:
            self._log.info('Rejoin %s', room_jid)
            self._join(muc_data)
        return True

    def _create(self, muc_data):
        presence = self._con.get_module('Presence').get_presence(
            muc_data.occupant_jid,
            show=self._con.status,
            status=self._con.status_message)

        presence.setTag(Namespace.MUC + ' x')

        self._log.info('Create MUC: %s', muc_data.jid)
        self._manager.set_state(muc_data.jid, MUCJoinedState.CREATING)
        self._con.connection.send(presence)

    def leave(self, room_jid, reason=None):
        self._log.info('Leave MUC: %s', room_jid)
        self._remove_join_timeout(room_jid)
        self._remove_rejoin_timeout(room_jid)
        self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
        muc_data = self._manager.get(room_jid)
        self._con.get_module('Presence').send_presence(
            muc_data.occupant_jid,
            typ='unavailable',
            status=reason,
            caps=False)
        # We leave a group chat, disable bookmark autojoin
        self._con.get_module('Bookmarks').modify(room_jid, autojoin=False)

    def configure_room(self, room_jid):
        self._nbxmpp('MUC').request_config(room_jid,
                                           callback=self._on_room_config)

    def _on_room_config(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.info(error)
            app.nec.push_incoming_event(NetworkEvent(
                'muc-configuration-failed',
                account=self._account,
                room_jid=error.jid,
                error=error))
            return

        self._log.info('Configure room: %s', result.jid)

        muc_data = self._manager.get(result.jid)
        self._apply_config(result.form, muc_data.config)
        self.set_config(result.jid,
                        result.form,
                        callback=self._on_config_result)

    @staticmethod
    def _apply_config(form, config=None):
        default_config = get_default_muc_config()
        if config is not None:
            default_config.update(config)
        for var, value in default_config.items():
            try:
                field = form[var]
            except KeyError:
                pass
            else:
                field.value = value

    def _on_config_result(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.info(error)
            app.nec.push_incoming_event(NetworkEvent(
                'muc-configuration-failed',
                account=self._account,
                room_jid=error.jid,
                error=error))
            return

        self._con.get_module('Discovery').disco_muc(
            result.jid, callback=self._on_disco_result_after_config)

        # If this is an automatic room creation
        try:
            invites = app.automatic_rooms[self._account][result.jid]['invities']
        except KeyError:
            return

        user_list = {}
        for jid in invites:
            user_list[jid] = {'affiliation': 'member'}
        self.set_affiliation(result.jid, user_list)

        for jid in invites:
            self.invite(result.jid, jid)

    def _on_disco_result_after_config(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.info('Disco %s failed: %s', error.jid, error.get_text())
            return

        jid = result.info.jid
        muc_data = self._manager.get(jid)
        self._room_join_complete(muc_data)

        self._log.info('Configuration finished: %s', jid)
        app.nec.push_incoming_event(NetworkEvent(
            'muc-configuration-finished',
            account=self._account,
            room_jid=jid))

    def update_presence(self):
        mucs = self._manager.get_mucs_with_state([MUCJoinedState.JOINED,
                                                  MUCJoinedState.JOINING])

        status, message, idle = self._con.get_presence_state()
        for muc_data in mucs:
            self._con.get_module('Presence').send_presence(
                muc_data.occupant_jid,
                show=status,
                status=message,
                idle_time=idle)

    def change_nick(self, room_jid, new_nick):
        status, message, _idle = self._con.get_presence_state()
        self._con.get_module('Presence').send_presence(
            '%s/%s' % (room_jid, new_nick),
            show=status,
            status=message)

    def _on_error_presence(self, _con, _stanza, properties):
        room_jid = properties.jid.bare
        muc_data = self._manager.get(room_jid)
        if muc_data is None:
            return

        if muc_data.state == MUCJoinedState.JOINING:
            if properties.error.condition == 'conflict':
                self._remove_rejoin_timeout(room_jid)
                muc_data.nick += '_'
                self._log.info('Nickname conflict: %s change to %s',
                               muc_data.jid, muc_data.nick)
                self._join(muc_data)
            elif properties.error.condition == 'not-authorized':
                self._remove_rejoin_timeout(room_jid)
                self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
                self._raise_muc_event('muc-password-required', properties)
            else:
                self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
                if room_jid not in self._rejoin_muc:
                    app.nec.push_incoming_event(
                        NetworkEvent('muc-join-failed',
                                     account=self._account,
                                     room_jid=room_jid,
                                     error=properties.error))

        elif muc_data.state == MUCJoinedState.CREATING:
            self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
            app.nec.push_incoming_event(
                NetworkEvent('muc-creation-failed',
                             account=self._account,
                             room_jid=room_jid,
                             error=properties.error))

        elif muc_data.state == MUCJoinedState.CAPTCHA_REQUEST:
            app.nec.push_incoming_event(
                NetworkEvent('muc-captcha-error',
                             account=self._account,
                             room_jid=room_jid,
                             error_text=to_user_string(properties.error)))
            self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
            self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)

        elif muc_data.state == MUCJoinedState.CAPTCHA_FAILED:
            self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)

        else:
            self._raise_muc_event('muc-presence-error', properties)

    def _on_muc_user_presence(self, _con, stanza, properties):
        if properties.type == PresenceType.ERROR:
            return

        room_jid = str(properties.muc_jid)
        if room_jid not in self._manager:
            self._log.warning('Presence from unknown MUC')
            self._log.warning(stanza)
            return

        muc_data = self._manager.get(room_jid)

        if properties.is_muc_destroyed:
            for contact in app.contacts.get_gc_contact_list(
                    self._account, room_jid):
                contact.presence = PresenceType.UNAVAILABLE
            self._log.info('MUC destroyed: %s', room_jid)
            self._remove_join_timeout(room_jid)
            self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
            self._raise_muc_event('muc-destroyed', properties)
            return

        contact = app.contacts.get_gc_contact(self._account,
                                              room_jid,
                                              properties.muc_nickname)

        if properties.is_nickname_changed:
            if properties.is_muc_self_presence:
                muc_data.nick = properties.muc_user.nick
                self._con.get_module('Bookmarks').modify(muc_data.jid,
                                                         nick=muc_data.nick)
            app.contacts.remove_gc_contact(self._account, contact)
            contact.name = properties.muc_user.nick
            app.contacts.add_gc_contact(self._account, contact)
            initiator = 'Server' if properties.is_nickname_modified else 'User'
            self._log.info('%s nickname changed: %s to %s',
                           initiator,
                           properties.jid,
                           properties.muc_user.nick)
            self._raise_muc_event('muc-nickname-changed', properties)
            return

        if contact is None and properties.type.is_available:
            self._add_new_muc_contact(properties)
            if properties.is_muc_self_presence:
                self._log.info('Self presence: %s', properties.jid)
                if muc_data.state == MUCJoinedState.JOINING:
                    if (properties.is_nickname_modified or
                            muc_data.nick != properties.muc_nickname):
                        muc_data.nick = properties.muc_nickname
                        self._log.info('Server modified nickname to: %s',
                                       properties.muc_nickname)

                elif muc_data.state == MUCJoinedState.CREATING:
                    if properties.is_new_room:
                        self.configure_room(room_jid)

                self._start_join_timeout(room_jid)
                self._raise_muc_event('muc-self-presence', properties)

            else:
                self._log.info('User joined: %s', properties.jid)
                self._raise_muc_event('muc-user-joined', properties)
            return

        if properties.is_muc_self_presence and properties.is_kicked:
            self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
            self._raise_muc_event('muc-self-kicked', properties)
            status_codes = properties.muc_status_codes or []
            if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
                self._start_rejoin_timeout(room_jid)
            return

        if properties.is_muc_self_presence and properties.type.is_unavailable:
            # Its not a kick, so this is the reflection of our own
            # unavailable presence, because we left the MUC
            return

        if properties.type.is_unavailable:
            for _event in app.events.get_events(self._account,
                                                jid=str(properties.jid),
                                                types=['pm']):
                contact.show = properties.show
                contact.presence = properties.type
                contact.status = properties.status
                contact.affiliation = properties.affiliation
                app.interface.handle_event(self._account,
                                           str(properties.jid),
                                           'pm')
                # Handle only the first pm event, the rest will be
                # handled by the opened ChatControl
                break

            if contact is None:
                # If contact is None, its probably that a user left from a not
                # insync MUC, can happen on older servers
                self._log.warning('Unknown contact left groupchat: %s',
                                  properties.jid)
            else:
                # We remove the contact from the MUC, but there could be
                # a PrivateChatControl open, so we update the contacts presence
                contact.presence = properties.type
                app.contacts.remove_gc_contact(self._account, contact)
            self._log.info('User %s left', properties.jid)
            self._raise_muc_event('muc-user-left', properties)
            return

        if contact.affiliation != properties.affiliation:
            contact.affiliation = properties.affiliation
            self._log.info('Affiliation changed: %s %s',
                           properties.jid,
                           properties.affiliation)
            self._raise_muc_event('muc-user-affiliation-changed', properties)

        if contact.role != properties.role:
            contact.role = properties.role
            self._log.info('Role changed: %s %s',
                           properties.jid,
                           properties.role)
            self._raise_muc_event('muc-user-role-changed', properties)

        if (contact.status != properties.status or
                contact.show != properties.show):
            contact.status = properties.status
            contact.show = properties.show
            self._log.info('Show/Status changed: %s %s %s',
                           properties.jid,
                           properties.status,
                           properties.show)
            self._raise_muc_event('muc-user-status-show-changed', properties)

    def _start_rejoin_timeout(self, room_jid):
        self._remove_rejoin_timeout(room_jid)
        self._rejoin_muc.add(room_jid)
        self._log.info('Start rejoin timeout for: %s', room_jid)
        id_ = GLib.timeout_add_seconds(2, self._rejoin, room_jid)
        self._rejoin_timeouts[room_jid] = id_

    def _remove_rejoin_timeout(self, room_jid):
        self._rejoin_muc.discard(room_jid)
        id_ = self._rejoin_timeouts.get(room_jid)
        if id_ is not None:
            self._log.info('Remove rejoin timeout for: %s', room_jid)
            GLib.source_remove(id_)
            del self._rejoin_timeouts[room_jid]

    def _start_join_timeout(self, room_jid):
        self._remove_join_timeout(room_jid)
        self._log.info('Start join timeout for: %s', room_jid)
        id_ = GLib.timeout_add_seconds(
            10, self._fake_subject_change, room_jid)
        self._join_timeouts[room_jid] = id_

    def _remove_join_timeout(self, room_jid):
        id_ = self._join_timeouts.get(room_jid)
        if id_ is not None:
            self._log.info('Remove join timeout for: %s', room_jid)
            GLib.source_remove(id_)
            del self._join_timeouts[room_jid]

    def _raise_muc_event(self, event_name, properties):
        app.nec.push_incoming_event(
            NetworkEvent(event_name,
                         account=self._account,
                         room_jid=properties.jid.bare,
                         properties=properties))
        self._log_muc_event(event_name, properties)

    def _log_muc_event(self, event_name, properties):
        if event_name not in ['muc-user-joined',
                              'muc-user-left',
                              'muc-user-status-show-changed']:
            return

        if (not app.settings.get('log_contact_status_changes') or
                not helpers.should_log(self._account, properties.jid)):
            return

        additional_data = AdditionalDataDict()
        if properties.muc_user is not None:
            if properties.muc_user.jid is not None:
                additional_data.set_value(
                    'gajim', 'real_jid', str(properties.muc_user.jid))

        # TODO: Refactor
        if properties.type == PresenceType.UNAVAILABLE:
            show = 'offline'
        else:
            show = properties.show.value
        show = app.storage.archive.convert_show_values_to_db_api_values(show)

        app.storage.archive.insert_into_logs(
            self._account,
            properties.jid.bare,
            properties.timestamp,
            KindConstant.GCSTATUS,
            contact_name=properties.muc_nickname,
            message=properties.status or None,
            show=show,
            additional_data=additional_data)

    def _add_new_muc_contact(self, properties):
        real_jid = None
        if properties.muc_user.jid is not None:
            real_jid = str(properties.muc_user.jid)
        contact = app.contacts.create_gc_contact(
            room_jid=properties.jid.bare,
            account=self._account,
            name=properties.muc_nickname,
            show=properties.show,
            status=properties.status,
            presence=properties.type,
            role=properties.role,
            affiliation=properties.affiliation,
            jid=real_jid,
            avatar_sha=properties.avatar_sha)
        app.contacts.add_gc_contact(self._account, contact)

    def _on_subject_change(self, _con, _stanza, properties):
        if not properties.is_muc_subject:
            return

        self._handle_subject_change(str(properties.muc_jid),
                                    properties.subject,
                                    properties.muc_nickname,
                                    properties.user_timestamp)

        raise nbxmpp.NodeProcessed

    def _fake_subject_change(self, room_jid):
        # This is for servers which don’t send empty subjects as part of the
        # event order on joining a MUC. For example jabber.ru
        self._log.warning('Fake subject received for %s', room_jid)
        del self._join_timeouts[room_jid]
        self._handle_subject_change(room_jid, None, None, None)

    def _handle_subject_change(self, room_jid, subject, nickname, timestamp):
        contact = app.contacts.get_groupchat_contact(self._account, room_jid)
        if contact is None:
            return

        contact.status = subject

        app.nec.push_incoming_event(
            NetworkEvent('muc-subject',
                         account=self._account,
                         room_jid=room_jid,
                         subject=subject,
                         nickname=nickname,
                         user_timestamp=timestamp,
                         is_fake=subject is None))

        muc_data = self._manager.get(room_jid)
        if muc_data.state == MUCJoinedState.JOINING:
            self._room_join_complete(muc_data)
            app.nec.push_incoming_event(
                NetworkEvent('muc-joined',
                             account=self._account,
                             room_jid=muc_data.jid))

    def _room_join_complete(self, muc_data):
        self._remove_join_timeout(muc_data.jid)
        self._manager.set_state(muc_data.jid, MUCJoinedState.JOINED)
        self._remove_rejoin_timeout(muc_data.jid)

        # We successfully joined a MUC, set add bookmark with autojoin
        self._con.get_module('Bookmarks').add_or_modify(
            muc_data.jid,
            autojoin=True,
            password=muc_data.password,
            nick=muc_data.nick)

    def _on_voice_request(self, _con, _stanza, properties):
        if not properties.is_voice_request:
            return

        jid = str(properties.jid)
        contact = app.contacts.get_groupchat_contact(self._account, jid)
        if contact is None:
            return

        app.nec.push_incoming_event(
            NetworkEvent('muc-voice-request',
                         account=self._account,
                         room_jid=str(properties.muc_jid),
                         voice_request=properties.voice_request))
        raise nbxmpp.NodeProcessed

    def _on_captcha_challenge(self, _con, _stanza, properties):
        if not properties.is_captcha_challenge:
            return

        if properties.is_mam_message:
            # Some servers store captcha challenges in MAM, don’t process them
            self._log.warning('Ignore captcha challenge received from MAM')
            raise nbxmpp.NodeProcessed

        muc_data = self._manager.get(properties.jid)
        if muc_data is None:
            return

        if muc_data.state != MUCJoinedState.JOINING:
            self._log.warning('Received captcha request but state != %s',
                              MUCJoinedState.JOINING)
            return

        contact = app.contacts.get_groupchat_contact(self._account,
                                                     str(properties.jid))
        if contact is None:
            return

        self._log.info('Captcha challenge received from %s', properties.jid)
        store_bob_data(properties.captcha.bob_data)
        muc_data.captcha_id = properties.id

        self._manager.set_state(properties.jid, MUCJoinedState.CAPTCHA_REQUEST)
        self._remove_rejoin_timeout(properties.jid)

        app.nec.push_incoming_event(
            NetworkEvent('muc-captcha-challenge',
                         account=self._account,
                         room_jid=properties.jid.bare,
                         form=properties.captcha.form))
        raise nbxmpp.NodeProcessed

    def cancel_captcha(self, room_jid):
        muc_data = self._manager.get(room_jid)
        if muc_data is None:
            return

        if muc_data.captcha_id is None:
            self._log.warning('No captcha message id available')
            return
        self._nbxmpp('MUC').cancel_captcha(room_jid, muc_data.captcha_id)
        self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
        self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)

    def send_captcha(self, room_jid, form_node):
        self._manager.set_state(room_jid, MUCJoinedState.JOINING)
        self._nbxmpp('MUC').send_captcha(room_jid,
                                         form_node,
                                         callback=self._on_captcha_result)

    def _on_captcha_result(self, task):
        try:
            task.finish()
        except StanzaError as error:
            muc_data = self._manager.get(error.jid)
            if muc_data is None:
                return
            self._manager.set_state(error.jid, MUCJoinedState.CAPTCHA_FAILED)
            app.nec.push_incoming_event(
                NetworkEvent('muc-captcha-error',
                             account=self._account,
                             room_jid=str(error.jid),
                             error_text=to_user_string(error)))

    def _on_config_change(self, _con, _stanza, properties):
        if not properties.is_muc_config_change:
            return

        room_jid = str(properties.muc_jid)
        self._log.info('Received config change: %s %s',
                       room_jid, properties.muc_status_codes)
        app.nec.push_incoming_event(
            NetworkEvent('muc-config-changed',
                         account=self._account,
                         room_jid=room_jid,
                         status_codes=properties.muc_status_codes))
        raise nbxmpp.NodeProcessed

    def _on_invite_or_decline(self, _con, _stanza, properties):
        if properties.muc_decline is not None:
            data = properties.muc_decline
            if helpers.ignore_contact(self._account, data.from_):
                raise nbxmpp.NodeProcessed

            self._log.info('Invite declined from: %s, reason: %s',
                           data.from_, data.reason)

            app.nec.push_incoming_event(
                NetworkEvent('muc-decline',
                             account=self._account,
                             **data._asdict()))
            raise nbxmpp.NodeProcessed

        if properties.muc_invite is not None:
            data = properties.muc_invite
            if helpers.ignore_contact(self._account, data.from_):
                raise nbxmpp.NodeProcessed

            self._log.info('Invite from: %s, to: %s', data.from_, data.muc)

            if app.in_groupchat(self._account, data.muc):
                # We are already in groupchat. Ignore invitation
                self._log.info('We are already in this room')
                raise nbxmpp.NodeProcessed

            self._con.get_module('Discovery').disco_muc(
                data.muc,
                request_vcard=True,
                callback=self._on_disco_result_after_invite,
                user_data=data)

            raise nbxmpp.NodeProcessed

    def _on_disco_result_after_invite(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.warning(error)
            return

        invite_data = task.get_user_data()
        app.nec.push_incoming_event(
            NetworkEvent('muc-invitation',
                         account=self._account,
                         info=result.info,
                         **invite_data._asdict()))

    def invite(self, room, to, reason=None, continue_=False):
        type_ = InviteType.MEDIATED
        contact = app.contacts.get_contact_from_full_jid(self._account, to)
        if contact and contact.supports(Namespace.CONFERENCE):
            type_ = InviteType.DIRECT

        password = app.gc_passwords.get(room, None)
        self._log.info('Invite %s to %s', to, room)
        return self._nbxmpp('MUC').invite(room, to, reason, password,
                                          continue_, type_)

    @event_filter(['account'])
    def _on_account_disconnected(self, _event):
        for room_jid in list(self._rejoin_timeouts.keys()):
            self._remove_rejoin_timeout(room_jid)

        for room_jid in list(self._join_timeouts.keys()):
            self._remove_join_timeout(room_jid)


class MUCManager:
    def __init__(self, logger):
        self._log = logger
        self._mucs = {}

    def add(self, muc):
        self._mucs[muc.jid] = muc

    def remove(self, muc):
        self._mucs.pop(muc.jid, None)

    def get(self, room_jid):
        return self._mucs.get(room_jid)

    def set_state(self, room_jid, state):
        muc = self._mucs.get(room_jid)
        if muc is not None:
            if muc.state == state:
                return
            self._log.info('Set MUC state: %s %s', room_jid, state)
            muc.state = state

    def get_joined_mucs(self):
        mucs = self._mucs.values()
        return [muc.jid for muc in mucs if muc.state == MUCJoinedState.JOINED]

    def get_mucs_with_state(self, states):
        return [muc for muc in self._mucs.values() if muc.state in states]

    def reset_state(self):
        for muc in self._mucs.values():
            self.set_state(muc.jid, MUCJoinedState.NOT_JOINED)

    def __contains__(self, room_jid):
        return room_jid in self._mucs


def get_instance(*args, **kwargs):
    return MUC(*args, **kwargs), 'MUC'
