# 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-0030: Service Discovery

import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from nbxmpp.errors import is_error

from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.nec import NetworkEvent
from gajim.common.modules.util import as_task
from gajim.common.modules.base import BaseModule


class Discovery(BaseModule):

    _nbxmpp_extends = 'Discovery'
    _nbxmpp_methods = [
        'disco_info',
        'disco_items',
    ]

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

        self.handlers = [
            StanzaHandler(name='iq',
                          callback=self._answer_disco_info,
                          typ='get',
                          ns=Namespace.DISCO_INFO),
            StanzaHandler(name='iq',
                          callback=self._answer_disco_items,
                          typ='get',
                          ns=Namespace.DISCO_ITEMS),
        ]

        self._account_info = None
        self._server_info = None

    @property
    def account_info(self):
        return self._account_info

    @property
    def server_info(self):
        return self._server_info

    def discover_server_items(self):
        server = self._con.get_own_jid().domain
        self.disco_items(server, callback=self._server_items_received)

    def _server_items_received(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.warning('Server disco failed')
            self._log.error(error)
            return

        self._log.info('Server items received')
        self._log.debug(result)
        for item in result.items:
            if item.node is not None:
                # Only disco components
                continue
            self.disco_info(item.jid, callback=self._server_items_info_received)

    def _server_items_info_received(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.warning('Server item disco info failed')
            self._log.warning(error)
            return

        self._log.info('Server item info received: %s', result.jid)
        self._parse_transports(result)
        try:
            self._con.get_module('MUC').pass_disco(result)
            self._con.get_module('HTTPUpload').pass_disco(result)
            self._con.get_module('Bytestream').pass_disco(result)
        except nbxmpp.NodeProcessed:
            pass

        app.nec.push_incoming_event(
            NetworkIncomingEvent('server-disco-received'))

    def discover_account_info(self):
        own_jid = self._con.get_own_jid().bare
        self.disco_info(own_jid, callback=self._account_info_received)

    def _account_info_received(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.warning('Account disco info failed')
            self._log.warning(error)
            return

        self._log.info('Account info received: %s', result.jid)

        self._account_info = result

        self._con.get_module('MAM').pass_disco(result)
        self._con.get_module('PEP').pass_disco(result)
        self._con.get_module('PubSub').pass_disco(result)
        self._con.get_module('Bookmarks').pass_disco(result)
        self._con.get_module('VCardAvatars').pass_disco(result)

        self._con.get_module('Caps').update_caps()

    def discover_server_info(self):
        # Calling this method starts the connect_maschine()
        server = self._con.get_own_jid().domain
        self.disco_info(server, callback=self._server_info_received)

    def _server_info_received(self, task):
        try:
            result = task.finish()
        except StanzaError as error:
            self._log.error('Server disco info failed')
            self._log.error(error)
            return

        self._log.info('Server info received: %s', result.jid)

        self._server_info = result

        self._con.get_module('SecLabels').pass_disco(result)
        self._con.get_module('Blocking').pass_disco(result)
        self._con.get_module('VCardTemp').pass_disco(result)
        self._con.get_module('Carbons').pass_disco(result)
        self._con.get_module('HTTPUpload').pass_disco(result)
        self._con.get_module('Register').pass_disco(result)

        self._con.connect_machine(restart=True)

    def _parse_transports(self, info):
        for identity in info.identities:
            if identity.category not in ('gateway', 'headline'):
                continue

            self._log.info('Found transport: %s %s %s',
                           info.jid, identity.category, identity.type)

            jid = str(info.jid)
            if jid not in app.transport_type:
                app.transport_type[jid] = identity.type

            if identity.type in self._con.available_transports:
                self._con.available_transports[identity.type].append(jid)
            else:
                self._con.available_transports[identity.type] = [jid]

    def _answer_disco_items(self, _con, stanza, _properties):
        from_ = stanza.getFrom()
        self._log.info('Answer disco items to %s', from_)

        if self._con.get_module('AdHocCommands').command_items_query(stanza):
            raise nbxmpp.NodeProcessed

        node = stanza.getTagAttr('query', 'node')
        if node is None:
            result = stanza.buildReply('result')
            self._con.connection.send(result)
            raise nbxmpp.NodeProcessed

        if node == Namespace.COMMANDS:
            self._con.get_module('AdHocCommands').command_list_query(stanza)
            raise nbxmpp.NodeProcessed

    def _answer_disco_info(self, _con, stanza, _properties):
        from_ = stanza.getFrom()
        self._log.info('Answer disco info %s', from_)
        if str(from_).startswith('echo.'):
            # Service that echos all stanzas, ignore it
            raise nbxmpp.NodeProcessed

        if self._con.get_module('AdHocCommands').command_info_query(stanza):
            raise nbxmpp.NodeProcessed

    @as_task
    def disco_muc(self,
                  jid,
                  request_vcard=False,
                  allow_redirect=False):

        _task = yield

        self._log.info('Request MUC info for %s', jid)

        result = yield self._nbxmpp('MUC').request_info(
            jid,
            request_vcard=request_vcard,
            allow_redirect=allow_redirect)

        if is_error(result):
            raise result

        if result.redirected:
            self._log.info('MUC info received after redirect: %s -> %s',
                           jid, result.info.jid)
        else:
            self._log.info('MUC info received: %s', result.info.jid)

        app.storage.cache.set_last_disco_info(result.info.jid, result.info)

        if result.vcard is not None:
            avatar, avatar_sha = result.vcard.get_avatar()
            if avatar is not None:
                if not app.interface.avatar_exists(avatar_sha):
                    app.interface.save_avatar(avatar)

                app.storage.cache.set_muc_avatar_sha(result.info.jid,
                                                     avatar_sha)
                app.interface.avatar_storage.invalidate_cache(result.info.jid)

        self._con.get_module('VCardAvatars').muc_disco_info_update(result.info)
        app.nec.push_incoming_event(NetworkEvent(
            'muc-disco-update',
            account=self._account,
            room_jid=result.info.jid))

        yield result

    @as_task
    def disco_contact(self, contact):
        _task = yield

        fjid = contact.get_full_jid()

        result = yield self.disco_info(fjid)
        if is_error(result):
            raise result

        self._log.info('Disco Info received: %s', fjid)

        app.storage.cache.set_last_disco_info(result.jid,
                                              result,
                                              cache_only=True)

        app.nec.push_incoming_event(
            NetworkEvent('caps-update',
                         account=self._account,
                         fjid=fjid,
                         jid=contact.jid))


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