# 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/>.

# Presence handler

import time

import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import PresenceType

from gajim.common import app
from gajim.common import idle
from gajim.common.i18n import _
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import should_log
from gajim.common.const import KindConstant
from gajim.common.const import ShowConstant
from gajim.common.modules.base import BaseModule


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

        self.handlers = [
            StanzaHandler(name='presence',
                          callback=self._presence_received,
                          priority=50),
            StanzaHandler(name='presence',
                          callback=self._subscribe_received,
                          typ='subscribe',
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._subscribed_received,
                          typ='subscribed',
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._unsubscribe_received,
                          typ='unsubscribe',
                          priority=49),
            StanzaHandler(name='presence',
                          callback=self._unsubscribed_received,
                          typ='unsubscribed',
                          priority=49),
        ]

        # keep the jids we auto added (transports contacts) to not send the
        # SUBSCRIBED event to GUI
        self.automatically_added = []

        # list of jid to auto-authorize
        self._jids_for_auto_auth = set()

    def _presence_received(self, _con, stanza, properties):
        if properties.from_muc:
            # MUC occupant presences are already handled in MUC module
            return

        muc = self._con.get_module('MUC').get_manager().get(properties.jid)
        if muc is not None:
            # Presence from the MUC itself, used for MUC avatar
            # handled in VCardAvatars module
            return

        self._log.info('Received from %s', properties.jid)

        if properties.type == PresenceType.ERROR:
            self._log.info('Error: %s %s', properties.jid, properties.error)
            return

        if self._account == 'Local':
            app.nec.push_incoming_event(
                NetworkEvent('raw-pres-received',
                             conn=self._con,
                             stanza=stanza))
            return

        if properties.is_self_presence:
            app.nec.push_incoming_event(
                NetworkEvent('our-show',
                             account=self._account,
                             show=properties.show.value))
            return

        jid = properties.jid.bare
        roster_item = self._con.get_module('Roster').get_item(jid)
        if not properties.is_self_bare and roster_item is None:
            # Handle only presence from roster contacts
            self._log.warning('Unknown presence received')
            self._log.warning(stanza)
            return

        show = properties.show.value
        if properties.type.is_unavailable:
            show = 'offline'

        event_attrs = {
            'conn': self._con,
            'stanza': stanza,
            'prio': properties.priority,
            'need_add_in_roster': False,
            'popup': False,
            'ptype': properties.type.value,
            'jid': properties.jid.bare,
            'resource': properties.jid.resource,
            'id_': properties.id,
            'fjid': str(properties.jid),
            'timestamp': properties.timestamp,
            'avatar_sha': properties.avatar_sha,
            'user_nick': properties.nickname,
            'idle_time': properties.idle_timestamp,
            'show': show,
            'new_show': show,
            'old_show': 0,
            'status': properties.status,
            'contact_list': [],
            'contact': None,
        }

        event_ = NetworkEvent('presence-received', **event_attrs)

        # TODO: Refactor
        self._update_contact(event_, properties)

        app.nec.push_incoming_event(event_)

    def _update_contact(self, event, properties):
        # Note: A similar method also exists in connection_zeroconf
        jid = properties.jid.bare
        resource = properties.jid.resource

        status_strings = ['offline', 'error', 'online', 'chat', 'away',
                          'xa', 'dnd']

        event.new_show = status_strings.index(event.show)

        # Update contact
        contact_list = app.contacts.get_contacts(self._account, jid)
        if not contact_list:
            self._log.warning('No contact found')
            return

        event.contact_list = contact_list

        contact = app.contacts.get_contact_strict(self._account,
                                                  properties.jid.bare,
                                                  properties.jid.resource)
        if contact is None:
            contact = app.contacts.get_first_contact_from_jid(self._account,
                                                              jid)
            if contact is None:
                self._log.warning('First contact not found')
                return

            if (self._is_resource_known(contact_list) and
                    not app.jid_is_transport(jid)):
                # Another resource of an existing contact connected
                # Add new contact
                event.old_show = 0
                contact = app.contacts.copy_contact(contact)
                contact.resource = resource
                app.contacts.add_contact(self._account, contact)
            else:
                # Convert the initial roster contact to a contact with resource
                contact.resource = resource
                event.old_show = 0
                if contact.show in status_strings:
                    event.old_show = status_strings.index(contact.show)

            event.need_add_in_roster = True

        elif contact.show in status_strings:
            event.old_show = status_strings.index(contact.show)

        # Update contact with presence data
        contact.show = event.show
        contact.status = properties.status
        contact.priority = properties.priority
        contact.idle_time = properties.idle_timestamp

        event.contact = contact

        if not app.jid_is_transport(jid) and len(contact_list) == 1:
            # It's not an agent
            if event.old_show == 0 and event.new_show > 1:
                if not jid in app.newly_added[self._account]:
                    app.newly_added[self._account].append(jid)
                if jid in app.to_be_removed[self._account]:
                    app.to_be_removed[self._account].remove(jid)
            elif event.old_show > 1 and event.new_show == 0 and \
            self._con.state.is_available:
                if not jid in app.to_be_removed[self._account]:
                    app.to_be_removed[self._account].append(jid)
                if jid in app.newly_added[self._account]:
                    app.newly_added[self._account].remove(jid)

        if app.jid_is_transport(jid):
            return

        if properties.type.is_unavailable:
            # TODO: This causes problems when another
            # resource signs off!
            self._con.get_module('Bytestream').stop_all_active_file_transfers(
                contact)
        self._log_presence(properties)

    @staticmethod
    def _is_resource_known(contact_list):
        if len(contact_list) > 1:
            return True

        if contact_list[0].resource == '':
            return False
        return contact_list[0].show not in ('not in roster', 'offline')

    def _log_presence(self, properties):
        if not app.settings.get('log_contact_status_changes'):
            return
        if not should_log(self._account, properties.jid.bare):
            return

        show = ShowConstant[properties.show.name]
        if properties.type.is_unavailable:
            show = ShowConstant.OFFLINE

        app.storage.archive.insert_into_logs(self._account,
                                             properties.jid.bare,
                                             time.time(),
                                             KindConstant.STATUS,
                                             message=properties.status,
                                             show=show)

    def _subscribe_received(self, _con, _stanza, properties):
        jid = properties.jid.bare
        fjid = str(properties.jid)

        is_transport = app.jid_is_transport(fjid)
        auto_auth = app.settings.get_account_setting(self._account, 'autoauth')

        self._log.info('Received Subscribe: %s, transport: %s, '
                       'auto_auth: %s, user_nick: %s',
                       properties.jid, is_transport,
                       auto_auth, properties.nickname)

        if auto_auth or jid in self._jids_for_auto_auth:
            self.send_presence(fjid, 'subscribed')
            self._jids_for_auto_auth.discard(jid)
            self._log.info('Auto respond with subscribed: %s', jid)
            return

        status = (properties.status or
                  _('I would like to add you to my roster.'))

        app.nec.push_incoming_event(NetworkEvent(
            'subscribe-presence-received',
            conn=self._con,
            jid=jid,
            fjid=fjid,
            status=status,
            user_nick=properties.nickname,
            is_transport=is_transport))

        raise nbxmpp.NodeProcessed

    def _subscribed_received(self, _con, _stanza, properties):
        jid = properties.jid.bare
        self._log.info('Received Subscribed: %s', properties.jid)
        if jid in self.automatically_added:
            self.automatically_added.remove(jid)
            raise nbxmpp.NodeProcessed

        app.nec.push_incoming_event(NetworkEvent(
            'subscribed-presence-received',
            account=self._account,
            jid=properties.jid))
        raise nbxmpp.NodeProcessed

    def _unsubscribe_received(self, _con, _stanza, properties):
        self._log.info('Received Unsubscribe: %s', properties.jid)
        raise nbxmpp.NodeProcessed

    def _unsubscribed_received(self, _con, _stanza, properties):
        self._log.info('Received Unsubscribed: %s', properties.jid)
        app.nec.push_incoming_event(NetworkEvent(
            'unsubscribed-presence-received',
            conn=self._con, jid=properties.jid.bare))
        raise nbxmpp.NodeProcessed

    def subscribed(self, jid):
        if not app.account_is_available(self._account):
            return
        self._log.info('Subscribed: %s', jid)
        self.send_presence(jid, 'subscribed')

    def unsubscribed(self, jid):
        if not app.account_is_available(self._account):
            return

        self._log.info('Unsubscribed: %s', jid)
        self._jids_for_auto_auth.discard(jid)
        self.send_presence(jid, 'unsubscribed')

    def unsubscribe(self, jid, remove_auth=True):
        if not app.account_is_available(self._account):
            return
        if remove_auth:
            self._con.get_module('Roster').del_item(jid)
        else:
            self._log.info('Unsubscribe from %s', jid)
            self._jids_for_auto_auth.discard(jid)
            self._con.get_module('Roster').unsubscribe(jid)
            self._con.get_module('Roster').set_item(jid)

    def subscribe(self, jid, msg=None, name='', groups=None, auto_auth=False):
        if not app.account_is_available(self._account):
            return
        if groups is None:
            groups = []

        self._log.info('Request Subscription to %s', jid)

        if auto_auth:
            self._jids_for_auto_auth.add(jid)

        infos = {'jid': jid}
        if name:
            infos['name'] = name
        iq = nbxmpp.Iq('set', Namespace.ROSTER)
        query = iq.setQuery()
        item = query.addChild('item', attrs=infos)
        for group in groups:
            item.addChild('group').setData(group)
        self._con.connection.send(iq)

        self.send_presence(jid,
                           'subscribe',
                           status=msg,
                           nick=app.nicks[self._account])

    def get_presence(self, to=None, typ=None, priority=None,
                     show=None, status=None, nick=None, caps=True,
                     idle_time=False):
        if show not in ('chat', 'away', 'xa', 'dnd'):
            # Gajim sometimes passes invalid show values here
            # until this is fixed this is a workaround
            show = None
        presence = nbxmpp.Presence(to, typ, priority, show, status)
        if nick is not None:
            nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
            nick_tag.setData(nick)

        if (idle_time and
                app.is_installed('IDLE') and
                app.settings.get('autoaway')):
            idle_sec = idle.Monitor.get_idle_sec()
            time_ = time.strftime('%Y-%m-%dT%H:%M:%SZ',
                                  time.gmtime(time.time() - idle_sec))

            idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
            idle_node.setAttr('since', time_)

        caps = self._con.get_module('Caps').caps
        if caps is not None and typ != 'unavailable':
            presence.setTag('c',
                            namespace=Namespace.CAPS,
                            attrs=caps._asdict())

        return presence

    def send_presence(self, *args, **kwargs):
        if not app.account_is_connected(self._account):
            return
        presence = self.get_presence(*args, **kwargs)
        app.plugin_manager.extension_point(
            'send-presence', self._account, presence)
        self._log.debug('Send presence:\n%s', presence)
        self._con.connection.send(presence)


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