## plugins/whiteboard/plugin.py
##
## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
##
## 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/>.
##

'''
Whiteboard plugin.

:author: Yann Leboulanger <asterix@lagaule.org>
:since: 1st November 2010
:copyright: Copyright (2010) Yann Leboulanger <asterix@lagaule.org>
:license: GPL
'''


from common import helpers
from common import gajim
from plugins import GajimPlugin
from plugins.plugin import GajimPluginException
from plugins.helpers import log_calls, log
import common.xmpp
import gtk
import chat_control
from common import ged
from common.jingle_session import JingleSession
from common.jingle_content import JingleContent
from common.jingle_transport import JingleTransport, TransportType
import dialogs
from whiteboard_widget import Whiteboard, HAS_GOOCANVAS
from common import xmpp
from common import caps_cache

NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml'
NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe'
NS_SXE = 'urn:xmpp:sxe:0'

class WhiteboardPlugin(GajimPlugin):
    @log_calls('WhiteboardPlugin')
    def init(self):
        self.config_dialog = None
        self.events_handlers = {
            'jingle-request-received': (ged.GUI1, self._nec_jingle_received),
            'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected),
            'jingle-disconnected-received': (ged.GUI1,
                self._nec_jingle_disconnected),
            'raw-message-received': (ged.GUI1, self._nec_raw_message),
        }
        self.gui_extension_points = {
            'chat_control_base' : (self.connect_with_chat_control,
                self.disconnect_from_chat_control),
            'chat_control_base_update_toolbar': (self.update_button_state,
                None),
        }
        self.controls = []
        self.sid = None

    @log_calls('WhiteboardPlugin')
    def _compute_caps_hash(self):
        for a in gajim.connections:
            gajim.caps_hash[a] = caps_cache.compute_caps_hash([
                gajim.gajim_identity], gajim.gajim_common_features + gajim.gajim_optional_features[a])
        # re-send presence with new hash
        connected = gajim.connections[a].connected
        if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
            gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
                gajim.connections[a].status)

    @log_calls('WhiteboardPlugin')
    def activate(self):
        if not HAS_GOOCANVAS:
            raise GajimPluginException('python-pygoocanvas is missing!')
        if NS_JINGLE_SXE not in gajim.gajim_common_features:
            gajim.gajim_common_features.append(NS_JINGLE_SXE)
        if NS_SXE not in gajim.gajim_common_features:
            gajim.gajim_common_features.append(NS_SXE)
        self._compute_caps_hash()

    @log_calls('WhiteboardPlugin')
    def deactivate(self):
        if NS_JINGLE_SXE in gajim.gajim_common_features:
            gajim.gajim_common_features.remove(NS_JINGLE_SXE)
        if NS_SXE in gajim.gajim_common_features:
            gajim.gajim_common_features.remove(NS_SXE)
        self._compute_caps_hash()

    @log_calls('WhiteboardPlugin')
    def connect_with_chat_control(self, control):
        if isinstance(control, chat_control.ChatControl):
            base = Base(self, control)
            self.controls.append(base)

    @log_calls('WhiteboardPlugin')
    def disconnect_from_chat_control(self, chat_control):
        for base in self.controls:
            base.disconnect_from_chat_control()
        self.controls = []

    @log_calls('WhiteboardPlugin')
    def update_button_state(self, control):
        for base in self.controls:
            if base.chat_control == control:
                if control.contact.supports(NS_JINGLE_SXE) and \
                control.contact.supports(NS_SXE):
                    base.button.set_sensitive(True)
                else:
                    base.button.set_sensitive(False)

    @log_calls('WhiteboardPlugin')
    def show_request_dialog(self, account, fjid, jid, sid, content_types):
        def on_ok():
            session = gajim.connections[account].get_jingle_session(fjid, sid)
            self.sid = session.sid
            if not session.accepted:
                session.approve_session()
            for content in content_types:
                session.approve_content(content)
            for _jid in (fjid, jid):
                ctrl = gajim.interface.msg_win_mgr.get_control(_jid, account)
                if ctrl:
                    break
            if not ctrl:
                # create it
                gajim.interface.new_chat_from_jid(account, jid)
                ctrl = gajim.interface.msg_win_mgr.get_control(jid, account)
            session = session.contents[('initiator', 'xhtml')]
            ctrl.draw_whiteboard(session)

        def on_cancel():
            session = gajim.connections[account].get_jingle_session(fjid, sid)
            session.decline_session()

        contact = gajim.contacts.get_first_contact_from_jid(account, jid)
        if contact:
            name = contact.get_shown_name()
        else:
            name = jid
        pritext = _('Incoming Whiteboard')
        sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with '
            'you. Do you want to accept?') % {'name': name, 'jid': jid}
        dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext,
            on_response_ok=on_ok, on_response_cancel=on_cancel)
        dialog.popup()

    @log_calls('WhiteboardPlugin')
    def _nec_jingle_received(self, obj):
        if not HAS_GOOCANVAS:
            return
        content_types = set(c[0] for c in obj.contents)
        if 'xhtml' not in content_types:
            return
        self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid,
            content_types)

    @log_calls('WhiteboardPlugin')
    def _nec_jingle_connected(self, obj):
        if not HAS_GOOCANVAS:
            return
        account = obj.conn.name
        ctrl = (gajim.interface.msg_win_mgr.get_control(obj.fjid, account)
            or gajim.interface.msg_win_mgr.get_control(obj.jid, account))
        if not ctrl:
            return
        session = gajim.connections[obj.conn.name].get_jingle_session(obj.fjid,
            obj.sid)

        if ('initiator', 'xhtml') not in session.contents:
            return

        session = session.contents[('initiator', 'xhtml')]
        ctrl.draw_whiteboard(session)

    @log_calls('WhiteboardPlugin')
    def _nec_jingle_disconnected(self, obj):
        for base in self.controls:
            if base.sid == obj.sid:
                base.stop_whiteboard()

    @log_calls('WhiteboardPlugin')
    def _nec_raw_message(self, obj):
        if not HAS_GOOCANVAS:
            return
        if obj.stanza.getTag('sxe', namespace=NS_SXE):
            account = obj.conn.name

            try:
                fjid = helpers.get_full_jid_from_iq(obj.stanza)
            except helpers.InvalidFormat:
                obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'),
                    _('A message from a non-valid JID arrived, it has been '
                      'ignored.')))

            jid = gajim.get_jid_without_resource(fjid)
            ctrl = (gajim.interface.msg_win_mgr.get_control(fjid, account)
                or gajim.interface.msg_win_mgr.get_control(jid, account))
            if not ctrl:
                return
            sxe = obj.stanza.getTag('sxe')
            if not sxe:
                return
            sid = sxe.getAttr('session')
            if (jid, sid) not in obj.conn._sessions:
                pass
#                newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid)
#                self.addJingle(newjingle)

            # we already have such session in dispatcher...
            session = obj.conn.get_jingle_session(fjid, sid)
            cn = session.contents[('initiator', 'xhtml')]
            error = obj.stanza.getTag('error')
            if error:
                action = 'iq-error'
            else:
                action = 'edit'

            cn.on_stanza(obj.stanza, sxe, error, action)
#        def __editCB(self, stanza, content, error, action):
            #new_tags = sxe.getTags('new')
            #remove_tags = sxe.getTags('remove')

            #if new_tags is not None:
                ## Process new elements
                #for tag in new_tags:
                    #if tag.getAttr('type') == 'element':
                        #ctrl.whiteboard.recieve_element(tag)
                    #elif tag.getAttr('type') == 'attr':
                        #ctrl.whiteboard.recieve_attr(tag)
                #ctrl.whiteboard.apply_new()

            #if remove_tags is not None:
                ## Delete rids
                #for tag in remove_tags:
                    #target = tag.getAttr('target')
                    #ctrl.whiteboard.image.del_rid(target)

            # Stop propagating this event, it's handled
            return True


class Base(object):
    def __init__(self, plugin, chat_control):
        self.plugin = plugin
        self.chat_control = chat_control
        self.chat_control.draw_whiteboard = self.draw_whiteboard
        self.contact = self.chat_control.contact
        self.account = self.chat_control.account
        self.jid = self.contact.get_full_jid()
        self.create_buttons()
        self.whiteboard = None
        self.sid = None

    def create_buttons(self):
        # create juick button
        actions_hbox = self.chat_control.xml.get_object('actions_hbox')
        self.button = gtk.ToggleButton(label=None, use_underline=True)
        self.button.set_property('relief', gtk.RELIEF_NONE)
        self.button.set_property('can-focus', False)
        img = gtk.Image()
        img_path = self.plugin.local_file_path('whiteboard.png')
        pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
        iconset = gtk.IconSet(pixbuf=pixbuf)
        factory = gtk.IconFactory()
        factory.add('whiteboard', iconset)
        factory.add_default()
        img.set_from_stock('whiteboard', gtk.ICON_SIZE_BUTTON)
        self.button.set_image(img)
        send_button = self.chat_control.xml.get_object('send_button')
        send_button_pos = actions_hbox.child_get_property(send_button,
            'position')
        actions_hbox.add_with_properties(self.button, 'position',
            send_button_pos - 1, 'expand', False)
        id_ = self.button.connect('toggled', self.on_whiteboard_button_toggled)
        self.chat_control.handlers[id_] = self.button
        self.button.show()

    def draw_whiteboard(self, content):
        hbox = self.chat_control.xml.get_object('chat_control_hbox')
        if len(hbox.get_children()) == 1:
            self.whiteboard = Whiteboard(self.account, self.contact, content,
                self.plugin)
            # set minimum size
            self.whiteboard.hbox.set_size_request(300, 0)
            hbox.pack_start(self.whiteboard.hbox, expand=False, fill=False)
            self.whiteboard.hbox.show_all()
            self.button.set_active(True)
            content.control = self
            self.sid = content.session.sid

    def on_whiteboard_button_toggled(self, widget):
        """
        Popup whiteboard
        """
        if widget.get_active():
            if not self.whiteboard:
                self.start_whiteboard()
        else:
            self.stop_whiteboard()

    def start_whiteboard(self):
        conn = gajim.connections[self.chat_control.account]
        jingle = JingleSession(conn, weinitiate=True, jid=self.jid)
        self.sid = jingle.sid
        conn._sessions[jingle.sid] = jingle
        content = JingleWhiteboard(jingle)
        content.control = self
        jingle.add_content('xhtml', content)
        jingle.start_session()

    def stop_whiteboard(self):
        conn = gajim.connections[self.chat_control.account]
        self.sid = None
        session = conn.get_jingle_session(self.jid, media='xhtml')
        if session:
            session.end_session()
        if not self.whiteboard:
            return
        hbox = self.chat_control.xml.get_object('chat_control_hbox')
        for child in hbox.get_children():
            if child == self.whiteboard.hbox:
                self.button.set_active(False)
                hbox.remove(child)
                self.whiteboard = None
                break

    def disconnect_from_chat_control(self):
        actions_hbox = self.chat_control.xml.get_object('actions_hbox')
        actions_hbox.remove(self.button)

class JingleWhiteboard(JingleContent):
    ''' Jingle Whiteboard sessions consist of xhtml content'''
    def __init__(self, session, transport=None):
        if not transport:
            transport = JingleTransportSXE()
        JingleContent.__init__(self, session, transport)
        self.media = 'xhtml'
        self.negotiated = True # there is nothing to negotiate
        self.last_rid = 0
        self.callbacks['session-accept'] += [self._sessionAcceptCB]
        self.callbacks['session-terminate'] += [self._stop]
        self.callbacks['session-terminate-sent'] += [self._stop]
        self.callbacks['edit'] = [self._EditCB]

    def _EditCB(self, stanza, content, error, action):
        new_tags = content.getTags('new')
        remove_tags = content.getTags('remove')

        if new_tags is not None:
            # Process new elements
            for tag in new_tags:
                if tag.getAttr('type') == 'element':
                    self.control.whiteboard.recieve_element(tag)
                elif tag.getAttr('type') == 'attr':
                    self.control.whiteboard.recieve_attr(tag)
            self.control.whiteboard.apply_new()

        if remove_tags is not None:
            # Delete rids
            for tag in remove_tags:
                target = tag.getAttr('target')
                self.control.whiteboard.image.del_rid(target)

    def _sessionAcceptCB(self, stanza, content, error, action):
        log.debug('session accepted')
        self.session.connection.dispatch('WHITEBOARD_ACCEPTED',
            (self.session.peerjid, self.session.sid))

    def generate_rids(self, x):
        # generates x number of rids and returns in list
        rids = []
        for x in range(x):
            rids.append(str(self.last_rid))
            self.last_rid += 1
        return rids

    def send_whiteboard_node(self, items, rids):
        # takes int rid and dict items and sends it as a node
        # sends new item
        jid = self.session.peerjid
        sid = self.session.sid
        message = xmpp.Message(to=jid)
        sxe = message.addChild(name='sxe', attrs={'session': sid},
            namespace=NS_SXE)

        for x in rids:
            if items[x]['type'] == 'element':
                parent = x
                attrs = {'rid': x,
                     'name': items[x]['data'][0].getName(),
                     'type': items[x]['type']}
                sxe.addChild(name='new', attrs=attrs)
            if items[x]['type'] == 'attr':
                attr_name = items[x]['data']
                chdata = items[parent]['data'][0].getAttr(attr_name)
                attrs = {'rid': x,
                     'name': attr_name,
                     'type': items[x]['type'],
                     'chdata': chdata,
                     'parent': parent}
                sxe.addChild(name='new', attrs=attrs)
        self.session.connection.connection.send(message)

    def delete_whiteboard_node(self, rids):
        message = xmpp.Message(to=self.session.peerjid)
        sxe = message.addChild(name='sxe', attrs={'session': self.session.sid},
            namespace=NS_SXE)

        for x in rids:
            sxe.addChild(name='remove', attrs = {'target': x})
        self.session.connection.connection.send(message)

    def send_items(self, items, rids):
        # recieves dict items and a list of rids of items to send
        # TODO: is there a less clumsy way that doesn't involve passing
        # whole list
        self.send_whiteboard_node(items, rids)

    def del_item(self, rids):
        self.delete_whiteboard_node(rids)

    def encode(self, xml):
        # encodes it sendable string
        return 'data:text/xml,' + urllib.quote(xml)

    def _fill_content(self, content):
        content.addChild(NS_JINGLE_XHTML + ' description')

    def _stop(self, *things):
        pass

    def __del__(self):
        pass

def get_content(desc):
    return JingleWhiteboard

common.jingle_content.contents[NS_JINGLE_XHTML] = get_content

class JingleTransportSXE(JingleTransport):
    def __init__(self):
        JingleTransport.__init__(self, TransportType.streaming)

    def make_transport(self, candidates=None):
        transport = JingleTransport.make_transport(self, candidates)
        transport.setNamespace(NS_JINGLE_SXE)
        transport.setTagData('host', 'TODO')
        return transport

common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE