# -*- coding: utf-8 -*-
# Copyright 2007-2008 One Laptop Per Child
# Copyright 2009-14 Sugar Labs
# Copyright 2014, Walter Bender
#
# This program 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; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('TelepathyGLib', '0.12')

from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import TelepathyGLib
from gi.repository import GLib

try:
    gi.require_version('Gst', '1.0')
    from gi.repository import Gst
    _HAS_SOUND = True
except ImportError:
    _HAS_SOUND = False

OSK_HEIGHT = [400, 300]
SLASH = '-x-SLASH-x-'  # slash safe encoding

import logging
import json
import os
import time
import dbus
from gettext import gettext as _

from sugar3.graphics import style
from sugar3.graphics.icon import EventIcon, Icon
from sugar3.graphics.alert import NotifyAlert
from sugar3.graphics.toolbarbox import ToolbarBox
from sugar3.graphics.toolbutton import ToolButton
from sugar3.activity import activity
from sugar3.activity.activity import get_bundle_path
from sugar3.presence import presenceservice
from sugar3.activity.widgets import ActivityToolbarButton
from sugar3.activity.widgets import StopButton
from sugar3.activity.activity import get_activity_root
from sugar3.activity.activity import show_object_in_journal
from sugar3.datastore import datastore
from sugar3 import profile
from sugar3.graphics import iconentry

from chat import smilies
from chat.box import ChatBox

from utils import EbookModeDetector

logger = logging.getLogger('chat-activity')


if _HAS_SOUND:
    Gst.init([])


# pylint: disable-msg=W0223
class Chat(activity.Activity):

    def __init__(self, handle):
        pservice = presenceservice.get_instance()
        self.owner = pservice.get_owner()

        self._ebook_mode_detector = EbookModeDetector()

        self.chatbox = ChatBox(
            self.owner, self._ebook_mode_detector.get_ebook_mode())
        self.chatbox.connect('open-on-journal', self.__open_on_journal)
        self.chatbox.connect('new-message',
                             self._search_entry_on_new_message_cb)

        super(Chat, self).__init__(handle)

        self._entry = None
        self._has_alert = False
        self._has_osk = False

        self._setup_canvas()

        self._entry.grab_focus()

        toolbar_box = ToolbarBox()
        self.set_toolbar_box(toolbar_box)

        self._activity_toolbar_button = ActivityToolbarButton(self)
        self._activity_toolbar_button.connect('clicked', self._fixed_resize_cb)

        toolbar_box.toolbar.insert(self._activity_toolbar_button, 0)
        self._activity_toolbar_button.show()

        self.search_entry = iconentry.IconEntry()
        self.search_entry.set_size_request(Gdk.Screen.width() / 3, -1)
        self.search_entry.set_icon_from_name(
            iconentry.ICON_ENTRY_PRIMARY, 'entry-search')
        self.search_entry.add_clear_button()
        self.search_entry.connect('activate', self._search_entry_activate_cb)
        self.search_entry.connect('changed', self._search_entry_activate_cb)

        self.connect('key-press-event', self._search_entry_key_press_cb)

        self._search_item = Gtk.ToolItem()
        self._search_item.add(self.search_entry)
        toolbar_box.toolbar.insert(self._search_item, -1)

        self._search_prev = ToolButton('go-previous-paired')
        self._search_prev.set_tooltip(_('Previous'))
        self._search_prev.props.accelerator = "<Shift><Ctrl>g"
        self._search_prev.connect('clicked', self._search_prev_cb)
        self._search_prev.props.sensitive = False
        toolbar_box.toolbar.insert(self._search_prev, -1)

        self._search_next = ToolButton('go-next-paired')
        self._search_next.set_tooltip(_('Next'))
        self._search_next.props.accelerator = "<Ctrl>g"
        self._search_next.connect('clicked', self._search_next_cb)
        self._search_next.props.sensitive = False
        toolbar_box.toolbar.insert(self._search_next, -1)

        separator = Gtk.SeparatorToolItem()
        separator.props.draw = False
        separator.set_expand(True)
        toolbar_box.toolbar.insert(separator, -1)

        toolbar_box.toolbar.insert(StopButton(self), -1)
        toolbar_box.show_all()

        # Chat is room or one to one:
        self._chat_is_room = False
        self.text_channel = None

        if _HAS_SOUND:
            self.element = Gst.ElementFactory.make('playbin', 'Player')

        if self.shared_activity:
            # we are joining the activity following an invite
            self._alert(_('Off-line'), _('Joining the Chat.'))
            self._entry.props.placeholder_text = \
                _('Please wait for a connection before starting to chat.')
            self.connect('joined', self._joined_cb)
            if self.get_shared():
                # we have already joined
                self._joined_cb(self)
        elif handle.uri:
            # XMPP non-sugar3 incoming chat, not sharable
            self._activity_toolbar_button.props.page.share.props.visible = \
                False
            self._one_to_one_connection(handle.uri)
        else:
            # we are creating the activity
            if not self.metadata or self.metadata.get(
                    'share-scope', activity.SCOPE_PRIVATE) == \
                    activity.SCOPE_PRIVATE:
                # if we are in private session
                self._alert(_('Off-line'), _('Share, or invite someone.'))
            else:
                # resume of shared activity from journal object without invite
                self._entry.props.placeholder_text = \
                    _('Please wait for a connection before starting to chat.')
            self.connect('shared', self._shared_cb)

    def _search_entry_key_press_cb(self, activity, event):
        keyname = Gdk.keyval_name(event.keyval).lower()
        if keyname == 'f':
            if Gdk.ModifierType.CONTROL_MASK & event.state:
                self.search_entry.grab_focus()
        elif keyname == 'escape':
            self.search_entry.props.text = ''
            self._entry.grab_focus()

    def _search_entry_on_new_message_cb(self, chatbox):
        self._search_entry_activate_cb(self.search_entry)

    def _search_entry_activate_cb(self, entry):
        for i in range(0, self.chatbox.number_of_textboxes()):
            textbox = self.chatbox.get_textbox(i)
            _buffer = textbox.get_buffer()
            start_mark = _buffer.get_mark('start')
            end_mark = _buffer.get_mark('end')
            if start_mark is None or end_mark is None:
                continue
            _buffer.delete_mark(start_mark)
            _buffer.delete_mark(end_mark)
            self.chatbox.highlight_text = (None, None, None)
        self.chatbox.set_search_text(entry.props.text)
        self._update_search_buttons()

    def _update_search_buttons(self,):
        if len(self.chatbox.search_text) == 0:
            self._search_prev.props.sensitive = False
            self._search_next.props.sensitive = False
        else:
            # If next or previous result exists
            self._search_prev.props.sensitive = \
                self.chatbox.check_next('backward')
            self._search_next.props.sensitive = \
                self.chatbox.check_next('forward')

    def _search_prev_cb(self, button):
        if button.props.sensitive:
            self.chatbox.search('backward')
            self._update_search_buttons()

    def _search_next_cb(self, button):
        if button.props.sensitive:
            self.chatbox.search('forward')
            self._update_search_buttons()

    def _fixed_resize_cb(self, widget=None, rect=None):
        ''' If a toolbar opens or closes, we need to resize the vbox
        holding out scrolling window. '''
        if self._has_alert:
            dy = style.GRID_CELL_SIZE
        else:
            dy = 0
        if self._has_osk:
            if Gdk.Screen.width() > Gdk.Screen.height():
                dy += OSK_HEIGHT[0]
            else:
                dy += OSK_HEIGHT[1]

        if self._toolbar_expanded():
            self.chatbox.set_size_request(
                self._chat_width,
                self._chat_height - style.GRID_CELL_SIZE - dy)
            self._fixed.move(self._entry_grid, style.GRID_CELL_SIZE,
                             self._chat_height - style.GRID_CELL_SIZE - dy)
        else:
            self.chatbox.set_size_request(self._chat_width,
                                          self._chat_height - dy)
            self._fixed.move(self._entry_grid, style.GRID_CELL_SIZE,
                             self._chat_height - dy)

        self.chatbox.resize_conversation(dy)

    def _setup_canvas(self):
        ''' Create a canvas '''
        self._fixed = Gtk.Fixed()
        self._fixed.set_size_request(
            Gdk.Screen.width(), Gdk.Screen.height() - style.GRID_CELL_SIZE)
        self._fixed.connect('size-allocate', self._fixed_resize_cb)
        self.set_canvas(self._fixed)
        self._fixed.show()

        self._entry_widgets = self._make_entry_widgets()
        self._fixed.put(self.chatbox, 0, 0)
        self.chatbox.show()

        self._fixed.put(self._entry_grid, style.GRID_CELL_SIZE,
                        self._chat_height)
        self._entry_grid.show()

        Gdk.Screen.get_default().connect('size-changed', self._configure_cb)

    def _configure_cb(self, event):
        self._fixed.set_size_request(
            Gdk.Screen.width(), Gdk.Screen.height() - style.GRID_CELL_SIZE)
        if self._ebook_mode_detector.get_ebook_mode():
            self._entry_height = int(style.GRID_CELL_SIZE * 1.5)
        else:
            self._entry_height = style.GRID_CELL_SIZE
        entry_width = Gdk.Screen.width() - \
            2 * (self._entry_height + style.GRID_CELL_SIZE)
        self._entry.set_size_request(entry_width, self._entry_height)
        self._entry_grid.set_size_request(
            Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE,
            self._entry_height)

        self._chat_height = Gdk.Screen.height() - self._entry_height - \
            style.GRID_CELL_SIZE
        self._chat_width = Gdk.Screen.width()
        self.chatbox.set_size_request(self._chat_width, self._chat_height)
        self.chatbox.resize_all()

        width = int(Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE)
        if self._ebook_mode_detector.get_ebook_mode():
            height = int(Gdk.Screen.height() - 8 * style.GRID_CELL_SIZE)
        else:
            height = int(Gdk.Screen.height() - 5 * style.GRID_CELL_SIZE)
        self._smiley_table.set_size_request(width, height)
        self._smiley_toolbar.set_size_request(width, -1)
        self._smiley_window.set_size_request(width, -1)

        self._fixed_resize_cb()

    def _create_smiley_table(self, width):
        pixel_size = (style.STANDARD_ICON_SIZE + style.LARGE_ICON_SIZE) / 2
        spacing = style.DEFAULT_SPACING
        button_size = pixel_size + spacing
        smilies_columns = int(width / button_size)
        pad = (width - smilies_columns * button_size) / 2

        table = Gtk.Grid()
        table.set_row_spacing(spacing)
        table.set_column_spacing(spacing)
        table.set_border_width(pad)

        queue = []

        def _create_smiley_icon_idle_cb():
            try:
                x, y, path, code = queue.pop()
            except IndexError:
                self.unbusy()
                return False
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path,
                                                            pixel_size,
                                                            pixel_size)
            image = Gtk.Image.new_from_pixbuf(pixbuf)
            box = Gtk.EventBox()
            box.add(image)
            box.connect('button-press-event', self._add_smiley_to_entry, code)
            table.attach(box, x, y, 1, 1)
            box.show_all()
            return True

        x = 0
        y = 0
        smilies.init()
        for i in range(len(smilies.THEME)):
            path, hint, codes = smilies.THEME[i]
            queue.append([x, y, path, codes[0]])

            x += 1
            if x == smilies_columns:
                y += 1
                x = 0

        queue.reverse()
        GLib.idle_add(_create_smiley_icon_idle_cb)
        return table

    def _add_smiley_to_entry(self, icon, event, text):
        pos = self._entry.props.cursor_position
        self._entry.insert_text(text, pos)
        self._entry.grab_focus()
        self._entry.set_position(pos + len(text))
        self._hide_smiley_window()

    def _shared_cb(self, sender):
        self._setup()

    def _one_to_one_connection(self, tp_channel):
        '''Handle a private invite from a non-sugar3 XMPP client.'''
        if self.shared_activity or self.text_channel:
            return
        bus_name, connection, channel = json.loads(tp_channel)
        logger.debug('GOT XMPP: %s %s %s', bus_name, connection, channel)
        conn = {}
        conn_proxy = dbus.Bus().get_object(bus_name, connection)
        conn[TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING] = \
            dbus.Interface(
                conn_proxy, TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING)
        self._one_to_one_connection_ready_cb(bus_name, channel, conn)

    def _one_to_one_connection_ready_cb(self, bus_name, channel, conn):
        '''Callback for Connection for one to one connection'''
        text_channel = {}
        text_proxy = dbus.Bus().get_object(bus_name, channel)
        text_channel[TelepathyGLib.IFACE_CHANNEL] = \
            dbus.Interface(text_proxy, TelepathyGLib.IFACE_CHANNEL)
        text_channel[TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT] = \
            dbus.Interface(text_proxy, TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT)
        text_channel[TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP] = \
            dbus.Interface(
                text_proxy, TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP)
        self.text_channel = TextChannelWrapper(text_channel, conn)
        self.text_channel.set_received_callback(self._received_cb)
        self.text_channel.handle_pending_messages()
        self.text_channel.set_closed_callback(
            self._one_to_one_connection_closed_cb)
        self._chat_is_room = False
        self._alert(_('On-line'), _('Private chat.'))

        # XXX How do we detect the sender going offline?
        self._entry.set_sensitive(True)
        self._entry.props.placeholder_text = None
        self._entry.grab_focus()

    def _one_to_one_connection_closed_cb(self):
        '''Callback for when the text channel closes.'''
        self._alert(_('Off-line'), _('Left the chat.'))

    def _setup(self):
        self.text_channel = TextChannelWrapper(
            self.shared_activity.telepathy_text_chan,
            self.shared_activity.telepathy_conn)
        self.text_channel.set_received_callback(self._received_cb)
        self._alert(_('On-line'), _('Connected.'))
        self.shared_activity.connect('buddy-joined', self._buddy_joined_cb)
        self.shared_activity.connect('buddy-left', self._buddy_left_cb)
        self._chat_is_room = True
        self._entry.set_sensitive(True)
        self._entry.props.placeholder_text = None
        self._entry.grab_focus()

    def _joined_cb(self, sender):
        '''Joined a shared activity.'''
        if not self.shared_activity:
            return
        logger.debug('Joined a shared chat')
        for buddy in self.shared_activity.get_joined_buddies():
            self._buddy_already_exists(buddy)
        self._setup()

    def _received_cb(self, buddy, text):
        '''Show message that was received.'''
        if buddy:
            if type(buddy) is dict:
                nick = buddy['nick']
            else:
                nick = buddy.props.nick
        else:
            nick = '???'
        logger.debug('Received message from %s: %s', nick, text)
        self.chatbox.add_text(buddy, text)

        if self.owner.props.nick in text:
            self.play_sound('said_nick')

        '''
        vscroll = self.chatbox.get_vadjustment()
        if vscroll.get_property('value') != vscroll.get_property('upper'):
            self._alert(_('New message'), _('New message from %s' % nick))
        '''
        if not self.has_focus:
            self.notify_user(_('Message from %s') % buddy, text)

    def _toolbar_expanded(self):
        if self._activity_toolbar_button.is_expanded():
            return True
        return False

    def _alert(self, title, text=None):
        alert = NotifyAlert(timeout=5)
        alert.props.title = title
        alert.props.msg = text
        self.add_alert(alert)
        alert.connect('response', self._alert_cancel_cb)
        alert.show()
        self._has_alert = True
        self._fixed_resize_cb()

    def _alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)
        self._has_alert = False
        self._fixed_resize_cb()

    def __open_on_journal(self, widget, url):
        '''Ask the journal to display a URL'''
        logger.debug('Create journal entry for URL: %s', url)
        jobject = datastore.create()
        metadata = {
            'title': '%s: %s' % (_('URL from Chat'), url),
            'title_set_by_user': '1',
            'icon-color': profile.get_color().to_string(),
            'mime_type': 'text/uri-list',
        }
        for k, v in list(metadata.items()):
            jobject.metadata[k] = v
        file_path = os.path.join(get_activity_root(), 'instance',
                                 '%i_' % time.time())
        open(file_path, 'w').write(url + '\r\n')
        os.chmod(file_path, 0o755)
        jobject.set_file_path(file_path)
        datastore.write(jobject)
        show_object_in_journal(jobject.object_id)
        jobject.destroy()
        os.unlink(file_path)

    def _buddy_joined_cb(self, sender, buddy):
        '''Show a buddy who joined'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(
            buddy, _('%s joined the chat') % buddy.props.nick,
            status_message=True)

        self.play_sound('login')

    def _buddy_left_cb(self, sender, buddy):
        '''Show a buddy who joined'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(
            buddy, _('%s left the chat') % buddy.props.nick,
            status_message=True)

        self.play_sound('logout')

    def _buddy_already_exists(self, buddy):
        '''Show a buddy already in the chat.'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(
            buddy, _('%s is here') % buddy.props.nick,
            status_message=True)

    def can_close(self):
        '''Perform cleanup before closing.
        Close text channel of a one to one XMPP chat.
        '''
        if self._chat_is_room is False:
            if self.text_channel is not None:
                self.text_channel.close()
        return True

    def _make_entry_widgets(self):
        '''We need to create a button for the smiley, a text entry, and a
        send button.

        All of this, along with the chatbox, goes into a grid.

        ---------------------------------------
        | chat box                            |
        | smiley button | entry | send button |
        ---------------------------------------
        '''
        if self._ebook_mode_detector.get_ebook_mode():
            self._entry_height = int(style.GRID_CELL_SIZE * 1.5)
        else:
            self._entry_height = style.GRID_CELL_SIZE
        entry_width = Gdk.Screen.width() - \
            2 * (self._entry_height + style.GRID_CELL_SIZE)
        self._chat_height = Gdk.Screen.height() - self._entry_height - \
            style.GRID_CELL_SIZE
        self._chat_width = Gdk.Screen.width()

        self.chatbox.set_size_request(self._chat_width, self._chat_height)

        self._entry_grid = Gtk.Grid()
        self._entry_grid.set_size_request(
            Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE,
            self._entry_height)

        smiley_button = EventIcon(icon_name='smilies',
                                  pixel_size=self._entry_height)
        smiley_button.connect('button-press-event', self._smiley_button_cb)
        self._entry_grid.attach(smiley_button, 0, 0, 1, 1)
        smiley_button.show()

        self._entry = Gtk.Entry()
        self._entry.set_size_request(entry_width, self._entry_height)
        self._entry.modify_bg(Gtk.StateType.INSENSITIVE,
                              style.COLOR_WHITE.get_gdk_color())
        self._entry.modify_base(Gtk.StateType.INSENSITIVE,
                                style.COLOR_WHITE.get_gdk_color())

        self._entry.set_sensitive(False)
        self._entry.props.placeholder_text = \
            _('You must be connected to a friend before starting to chat.')
        self._entry.connect('focus-in-event', self._entry_focus_in_cb)
        self._entry.connect('focus-out-event', self._entry_focus_out_cb)
        self._entry.connect('activate', self._entry_activate_cb)
        self._entry.connect('key-press-event', self._entry_key_press_cb)
        self._entry_grid.attach(self._entry, 1, 0, 1, 1)
        self._entry.show()

        send_button = EventIcon(icon_name='send',
                                pixel_size=self._entry_height)
        send_button.connect('button-press-event', self._send_button_cb)
        self._entry_grid.attach(send_button, 2, 0, 1, 1)
        send_button.show()

    def _get_icon_pixbuf(self, name):
        icon_theme = Gtk.IconTheme.get_default()
        icon_info = icon_theme.lookup_icon(
            name, style.LARGE_ICON_SIZE, 0)
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            icon_info.get_filename(), style.LARGE_ICON_SIZE,
            style.LARGE_ICON_SIZE)
        del icon_info
        return pixbuf

    def _entry_focus_in_cb(self, entry, event):
        self._hide_smiley_window()

        if self._ebook_mode_detector.get_ebook_mode():
            self._has_osk = True
            self._fixed_resize_cb()

    def _entry_focus_out_cb(self, entry, event):
        if self._ebook_mode_detector.get_ebook_mode():
            self._has_osk = False
            self._fixed_resize_cb()

    def _entry_key_press_cb(self, widget, event):
        '''Check for scrolling keys.

        Check if the user pressed Page Up, Page Down, Home or End and
        scroll the window according the pressed key.
        '''
        vadj = self.chatbox.get_vadjustment()
        if event.keyval == Gdk.KEY_Page_Down:
            value = vadj.get_value() + vadj.page_size
            if value > vadj.upper - vadj.page_size:
                value = vadj.upper - vadj.page_size
            vadj.set_value(value)
        elif event.keyval == Gdk.KEY_Page_Up:
            vadj.set_value(vadj.get_value() - vadj.page_size)
        elif event.keyval == Gdk.KEY_Home and \
                event.get_state() & Gdk.ModifierType.CONTROL_MASK:
            vadj.set_value(vadj.lower)
        elif event.keyval == Gdk.KEY_End and \
                event.get_state() & Gdk.ModifierType.CONTROL_MASK:
            vadj.set_value(vadj.upper - vadj.page_size)

    def _smiley_button_cb(self, widget, event):
        self._show_smiley_window()

    def _send_button_cb(self, widget, event):
        self._entry_activate_cb(self._entry)

    def _entry_activate_cb(self, entry):
        self.chatbox._scroll_auto = True

        text = entry.props.text
        if text:
            logger.debug('Adding text to chatbox: %s: %s' % (self.owner, text))
            self.chatbox.add_text(self.owner, text)
            entry.props.text = ''
            if self.text_channel:
                logger.debug('sending to text_channel: %s' % (text))
                self.text_channel.send(text)
            else:
                logger.debug('Tried to send message but text channel '
                             'not connected.')

    def write_file(self, file_path):
        '''Store chat log in Journal.

        Handling the Journal is provided by Activity - we only need
        to define this method.
        '''
        logger.debug('write_file: writing %s' % file_path)
        self.chatbox.add_log_timestamp()
        f = open(file_path, 'w')
        try:
            f.write(self.chatbox.get_log())
        finally:
            f.close()
        self.metadata['mime_type'] = 'text/plain'

    def read_file(self, file_path):
        '''Load a chat log from the Journal.
        Handling the Journal is provided by Activity - we only need
        to define this method.
        '''
        logger.debug('read_file: reading %s' % file_path)
        log = open(file_path).readlines()
        last_line_was_timestamp = False
        for line in log:
            if line.endswith('\t\t\n'):
                if last_line_was_timestamp is False:
                    timestamp = line.strip().split('\t')[0]
                    self.chatbox.add_separator(timestamp)
                    last_line_was_timestamp = True
            else:
                timestamp, nick, color, status, text = line.strip().split('\t')
                status_message = bool(int(status))
                self.chatbox.add_text({'nick': nick, 'color': color},
                                      text, status_message)
                last_line_was_timestamp = False

    def play_sound(self, event):
        if _HAS_SOUND:
            SOUNDS_PATH = os.path.join(get_bundle_path(), 'sounds')
            SOUNDS = {'said_nick': os.path.join(SOUNDS_PATH, 'alert.wav'),
                      'login': os.path.join(SOUNDS_PATH, 'login.wav'),
                      'logout': os.path.join(SOUNDS_PATH, 'logout.wav')}

            self.element.set_state(Gst.State.NULL)
            self.element.set_property('uri', 'file://%s' % SOUNDS[event])
            self.element.set_state(Gst.State.PLAYING)

    def _create_smiley_window(self):
        grid = Gtk.Grid()
        width = int(Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE)

        self._smiley_toolbar = SmileyToolbar(self)
        height = style.GRID_CELL_SIZE
        self._smiley_toolbar.set_size_request(width, height)
        grid.attach(self._smiley_toolbar, 0, 0, 1, 1)
        self._smiley_toolbar.show()

        self._smiley_table = Gtk.ScrolledWindow()
        self._smiley_table.set_policy(Gtk.PolicyType.NEVER,
                                      Gtk.PolicyType.AUTOMATIC)
        self._smiley_table.modify_bg(
            Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color())
        if self._ebook_mode_detector.get_ebook_mode():
            height = int(Gdk.Screen.height() - 8 * style.GRID_CELL_SIZE)
        else:
            height = int(Gdk.Screen.height() - 4 * style.GRID_CELL_SIZE)
        self._smiley_table.set_size_request(width, height)

        table = self._create_smiley_table(width)
        self._smiley_table.add_with_viewport(table)
        table.show_all()

        grid.attach(self._smiley_table, 0, 1, 1, 1)
        self._smiley_table.show()

        self._smiley_window = Gtk.ScrolledWindow()
        self._smiley_window.set_policy(Gtk.PolicyType.NEVER,
                                       Gtk.PolicyType.NEVER)
        self._smiley_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self._smiley_window.set_size_request(width, -1)

        self._smiley_window.add_with_viewport(grid)

        def _key_press_event_cb(widget, event):
            if event.keyval == Gdk.KEY_Escape:
                self._hide_smiley_window()
                return True
            return False
        self.connect('key-press-event', _key_press_event_cb)

        grid.show()

        self._fixed.put(self._smiley_window, style.GRID_CELL_SIZE, 0)

    def _show_smiley_window(self):
        if not hasattr(self, '_smiley_window'):
            self.busy()
            self._create_smiley_window()
        self._smiley_window.show()

    def _hide_smiley_window(self):
        if hasattr(self, '_smiley_window'):
            self._smiley_window.hide()


class TextChannelWrapper(object):
    '''Wrap a telepathy Text Channfel to make usage simpler.'''

    def __init__(self, text_chan, conn):
        '''Connect to the text channel'''
        self._activity_cb = None
        self._activity_close_cb = None
        self._text_chan = text_chan
        self._conn = conn
        self._logger = logging.getLogger(
            'chat-activity.TextChannelWrapper')
        self._signal_matches = []
        m = self._text_chan[TelepathyGLib.IFACE_CHANNEL].connect_to_signal(
            'Closed', self._closed_cb)
        self._signal_matches.append(m)

    def send(self, text):
        '''Send text over the Telepathy text channel.'''
        # XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION
        logging.debug('sending %s' % text)

        text = text.replace('/', SLASH)

        if self._text_chan is not None:
            self._text_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT].Send(
                TelepathyGLib.ChannelTextMessageType.NORMAL, text)

    def close(self):
        '''Close the text channel.'''
        self._logger.debug('Closing text channel')
        try:
            self._text_chan[TelepathyGLib.IFACE_CHANNEL].Close()
        except Exception:
            self._logger.debug('Channel disappeared!')
            self._closed_cb()

    def _closed_cb(self):
        '''Clean up text channel.'''
        self._logger.debug('Text channel closed.')
        for match in self._signal_matches:
            match.remove()
        self._signal_matches = []
        self._text_chan = None
        if self._activity_close_cb is not None:
            self._activity_close_cb()

    def set_received_callback(self, callback):
        '''Connect the function callback to the signal.

        callback -- callback function taking buddy and text args
        '''
        if self._text_chan is None:
            return
        self._activity_cb = callback
        ct = TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT
        m = self._text_chan[ct].connect_to_signal(
            'Received', self._received_cb)
        self._signal_matches.append(m)

    def handle_pending_messages(self):
        '''Get pending messages and show them as received.'''
        ct = TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT
        pm = self._text_chan[ct].ListPendingMessages(False)
        for identity, timestamp, sender, type_, flags, text in pm:
            self._received_cb(identity, timestamp, sender, type_, flags, text)

    def _received_cb(self, identity, timestamp, sender, type_, flags, text):
        '''Handle received text from the text channel.

        Converts sender to a Buddy.
        Calls self._activity_cb which is a callback to the activity.
        '''
        logging.debug('received_cb %r %s' % (type_, text))
        if type_ != 0:
            # Exclude any auxiliary messages
            return

        text = text.replace(SLASH, '/')

        if self._activity_cb:
            try:
                self._text_chan[TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP]
            except Exception:
                # One to one XMPP chat
                co = TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING
                nick = self._conn[co].RequestAliases([sender])[0]
                buddy = {'nick': nick, 'color': '#000000,#808080'}
            else:
                # Normal sugar3 MUC chat
                # XXX: cache these
                buddy = self._get_buddy(sender)
            self._activity_cb(buddy, text)
            ct = TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT
            self._text_chan[ct].AcknowledgePendingMessages([identity])
        else:
            self._logger.debug('Throwing received message on the floor'
                               ' since there is no callback connected. See'
                               ' set_received_callback')

    def set_closed_callback(self, callback):
        '''Connect a callback for when the text channel is closed.

        callback -- callback function taking no args

        '''
        self._activity_close_cb = callback

    def _get_buddy(self, cs_handle):
        '''Get a Buddy from a (possibly channel-specific) handle.'''
        # XXX This will be made redundant once Presence Service
        # provides buddy resolution
        # Get the Presence Service
        pservice = presenceservice.get_instance()
        # Get the Telepathy Connection
        tp_name, tp_path = pservice.get_preferred_connection()
        conn_proxy = dbus.Bus().get_object(tp_name, tp_path)
        conn = dbus.Interface(conn_proxy, TelepathyGLib.IFACE_CONNECTION)
        group = self._text_chan[TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP]
        my_csh = group.GetSelfHandle()
        if my_csh == cs_handle:
            handle = conn.GetSelfHandle()
        elif group.GetGroupFlags() & \
                TelepathyGLib.ChannelGroupFlags.CHANNEL_SPECIFIC_HANDLES:

            handle = group.GetHandleOwners([cs_handle])[0]
        else:
            handle = cs_handle

            # XXX: deal with failure to get the handle owner
            assert handle != 0

        return pservice.get_buddy_by_telepathy_handle(
            tp_name, tp_path, handle)


class SmileyToolbar(Gtk.Toolbar):

    def __init__(self, activity):
        Gtk.Toolbar.__init__(self)

        self._activity = activity
        self._add_separator()

        self._icon = Icon(icon_name='smilies-white')
        self._add_widget(self._icon)

        self._add_separator()

        self._title = Gtk.Label(_('Insert a smiley'))
        self._add_widget(self._title)

        self._add_separator(True)

        self.cancel_button = ToolButton('dialog-cancel')
        self.cancel_button.set_tooltip(_('Cancel'))
        self.cancel_button.connect('clicked', self.__cancel_button_clicked_cb)
        self.insert(self.cancel_button, -1)
        self.cancel_button.show()

    def _add_separator(self, expand=False):
        separator = Gtk.SeparatorToolItem()
        separator.props.draw = False
        if expand:
            separator.set_expand(True)
        else:
            separator.set_size_request(style.DEFAULT_SPACING, -1)
        self.insert(separator, -1)
        separator.show()

    def _add_widget(self, widget, expand=False):
        tool_item = Gtk.ToolItem()
        tool_item.set_expand(expand)

        tool_item.add(widget)
        widget.show()

        self.insert(tool_item, -1)
        tool_item.show()

    def __cancel_button_clicked_cb(self, widget, data=None):
        self._activity._hide_smiley_window()
