# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Florian Boucault <florian@fluendo.com>

"""
Widget combining a title, a text entry and a keyboard.
"""

from elisa.core.input_event import EventValue, EventSource

from elisa.plugins.pigment.graph.image import Image
from elisa.plugins.pigment.graph.text import Text
from elisa.plugins.pigment.widgets.box import VBox, HBox
from elisa.plugins.pigment.widgets.widget import Widget
from elisa.plugins.pigment.widgets.button import Button
from elisa.plugins.pigment.widgets.input import TextEntry as StockTextEntry, \
                                                PasswordEntry as StockPasswordEntry
from elisa.plugins.pigment.widgets.keyboard import Keyboard as StockKeyboard
from elisa.plugins.pigment.widgets.theme import Theme
from elisa.plugins.pigment.widgets.panel import PiecePanel
from elisa.plugins.pigment.widgets.const import STATE_NORMAL, \
                                                STATE_PRESSED, \
                                                STATE_SELECTED
from elisa.plugins.pigment.animation import implicit

from elisa.plugins.poblesec.widgets.background import WidgetWithBackground
from elisa.plugins.poblesec.widgets.button import RelookSmoothStatesWidget, RelookSmoothPanel

import gobject


class TextEntry(StockTextEntry):
    """
    DOCME
    """

    def _create_widgets(self):
        super(TextEntry, self)._create_widgets()

        state_widgets = {STATE_NORMAL: TextEntryDecorations,
                         STATE_PRESSED: TextEntryDecorations,
                         STATE_SELECTED: TextEntryDecorations}

        self.decorations = RelookSmoothStatesWidget(state_widgets=state_widgets)
        self.decorations.visible = True
        self.add(self.decorations)
        self.animated.setup_next_animations(duration=200)
        self.label.animated.setup_next_animations(duration=200)

    def do_state_changed(self, previous_state):
        super(TextEntry, self).do_state_changed(previous_state)
        self.decorations.state = self.state


class PasswordTextEntry(StockPasswordEntry, TextEntry):
    pass


class TextEntryDecorations(Widget):

    def __init__(self):
        super(TextEntryDecorations, self).__init__()
        self._create_widgets()
        self.update_style_properties(self.style.get_items())

    def _create_widgets(self):
        self.background = PiecePanel()
        self.background.visible = True
        self.add(self.background)

        self.glyph = Image()
        self.glyph.visible = True
        self.add(self.glyph)

        self.box = PiecePanel()
        self.box.set_name("text_box")
        self.box.visible = True
        self.add(self.box)

    def do_state_changed(self, previous_state):
        super(TextEntryDecorations, self).do_state_changed(previous_state)
        self.background.state = self.state
        self.box.state = self.state


class KeyboardWidget(StockKeyboard):

    """
    Keyboard widget that is aware of a target text entry.

    @ivar entry: the target text entry widget
    @type entry: L{elisa.plugins.pigment.widgets.input.TextEntry}
    """

    def __init__(self, *args, **kwargs):
        super(KeyboardWidget, self).__init__(*args, **kwargs)
        self.entry = None

    def handle_input(self, manager, event):
        # Intercept the backspace event if the current entry is empty
        # to pass it on to the parent.
        if event.value == EventValue.KEY_MENU and \
            event.source == EventSource.KEYBOARD and not self.entry.content:
            return False

        # Fall back to the keyboard's default behaviour.
        return super(KeyboardWidget, self).handle_input(manager, event)


class KeyboardButton(Button):
    """
    DOCME
    """
    def do_state_changed(self, previous_state):
        self.panel.state = self.state
        super(KeyboardButton, self).do_state_changed(previous_state)

    def _get_background_style(self):
        return self.panel.name

    def _set_background_style(self, value):
        if not hasattr(self, 'panel'):
            self.panel = RelookSmoothPanel()
            self.panel.visible = True
            self.add(self.panel, forward_signals=False)
        self.panel.set_name(value)

    background_style = property(_get_background_style, _set_background_style)


class ButtonWithGlyph(KeyboardButton):
    pass


class ButtonWithoutGlyph(KeyboardButton):
    pass


class KeyboardList(gobject.GObject):

    """
    An ordered list of keyboard widgets, each associated to a given layout.

    Emit a switched-keyboard signal when the keyboard has been changed.
    """

    __gsignals__ = \
        {'switched-keyboard': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())}

    def __init__(self, layouts):
        """
        @param layouts: a list of layout resource identifiers
        @type layouts:  C{list} of C{str}
        """
        super(KeyboardList, self).__init__()

        if not layouts:
            raise ValueError('At least one layout is required.')

        self.keyboards = []

        for layout in layouts:
            theme = Theme.get_default()
            xml_layout = theme.get_resource(layout)
            keyboard = KeyboardWidget(xml_layout, ButtonWithGlyph,
                                      ButtonWithoutGlyph)
            keyboard.connect('key-press-special', self._key_press_special_cb)
            self.keyboards.append(keyboard)

        self._current_index = 0

    def clean(self):
        for keyboard in self.keyboards:
            keyboard.disconnect_by_func(self._key_press_special_cb)
            keyboard.clean()

    def next(self):
        """
        Switch to the next keyboard in the list (the list is cyclic, when its
        last keyboard is reached it goes back to the first one).
        """
        prev_index = self._current_index
        self._current_index = (self._current_index + 1 ) % len(self.keyboards)
        if self._current_index != prev_index:
            self.emit('switched-keyboard')

    def _key_press_special_cb(self, keyboard, value):
        if value == 'switch_keyboard':
            self.next()


class KeyboardsWidget(Widget):

    """
    A widget that contains a list of keyboard widgets and shows only one at a
    given time.

    @ivar current: a reference to the currently visible keyboard
    @type current: L{elisa.plugins.pigment.widgets.keyboard.Keyboard}
    """

    def __init__(self, keyboards):
        """
        param keyboards: a list of keyboard widgets
        type keyboards:  C{list} of
                         L{elisa.plugins.pigment.widgets.keyboard.Keyboard}
        """
        super(KeyboardsWidget, self).__init__()

        if not keyboards:
            raise ValueError('At least one keyboard is required.')

        self.keyboards = keyboards
        self._current_index = 0

        for keyboard in keyboards:
            keyboard.opacity = 0
            self.add(keyboard)

        # Show the first keyboard.
        keyboard = self.keyboards[self._current_index]
        keyboard.visible = True
        keyboard.opacity = 255
        self.set_focus_proxy(keyboard)

    def _show_keyboard(self, index):
        keyboard = self.keyboards[index]
        self.set_focus_proxy(keyboard)
        keyboard.animated.setup_next_animations(duration=140,
                                             transformation=implicit.DECELERATE)
        keyboard.visible = True
        keyboard.animated.opacity = 255

    def _hide_keyboard(self, index):
        keyboard = self.keyboards[index]
        keyboard.animated.setup_next_animations(duration=250,
                                             transformation=implicit.DECELERATE)
        keyboard.animated.opacity = 0

    @property
    def current(self):
        return self.keyboards[self._current_index]

    def next(self):
        """
        Hide the current keyboard and show the next one.
        """
        self._hide_keyboard(self._current_index)
        self._current_index = (self._current_index + 1) % len(self.keyboards)
        self._show_keyboard(self._current_index)

    def select_key(self, key_name):
        """
        Select a given key for the current keyboard.

        @param key_name: the name of the key to select
        @type key_name:  C{unicode}
        """
        key = self.current.get_key_by_name(key_name)
        self.current.select_key(key)


class Keyboard(VBox):

    """
    An all in-one keyboard widget that contains a title, any number of text
    entries, and an OSK.

    At instantiation the widget doesn't contain any text entry, use its
    C{add_entry} method.

    Emit the 'validated' signal when the contents of all the entries are ready
    to be validated against whatever processor implemented by the client code.
    """

    __gsignals__ = {'validated': (gobject.SIGNAL_RUN_LAST,
                                  gobject.TYPE_NONE, ())
                   }

    def __init__(self, layouts=['elisa.plugins.poblesec.osk_qwerty']):
        self.layouts = layouts
        self._entries = []
        self._active_entry = None
        super(Keyboard, self).__init__()

    def _create_keyboard(self):
        keyboard_box = HBox()

        self.left_spacer = Image()
        self.left_spacer.visible = True
        keyboard_box.pack_start(self.left_spacer, expand=True)

        self.keyboard_list = KeyboardList(self.layouts)
        self.keyboards = KeyboardsWidget(self.keyboard_list.keyboards)
        self.keyboard_list.connect('switched-keyboard',
                                   self._switch_keyboard_cb)
        self.keyboards.visible = True
        keyboard_box.pack_start(self.keyboards)
        keyboard_box.set_focus_proxy(self.keyboards)

        self.right_spacer = Image()
        self.right_spacer.visible = True
        keyboard_box.pack_start(self.right_spacer, expand=True)
        return keyboard_box

    def create_widgets(self):
        self.navigable = True

        self.title = WidgetWithBackground(foreground=Text())
        self.title.visible = True
        self.pack_start(self.title)

        self.keyboard_layout = self._create_keyboard()
        self.keyboard_layout.visible = True
        self.pack_end(self.keyboard_layout)
        self.set_focus_proxy(self.keyboard_layout)

        for keyboard in self.keyboards.keyboards:
            keyboard.connect('key-press-char', self._key_press_char_cb)
            keyboard.connect('key-press-special', self._key_press_special_cb)

    def add_entry(self, entry):
        """
        Pack a text entry widget after the other text entries.

        @param entry: the text entry widget to add
        @type entry:  L{elisa.plugins.poblesec.widgets.keyboard.TextEntry}
        """
        self._entries.append(entry)
        if self._active_entry is None:
            self._set_active_entry(entry)
        else:
            entry.set_name('passive_entry')
        entry.connect('activated', self._entry_activated)
        self.pack_start(entry)

    def _set_active_entry(self, entry):
        # We use widget names to express what in fact would better be
        # represented by combined states (e.g. NORMAL and (IN)ACTIVE).
        if self._active_entry is not None:
            self._active_entry.set_name('passive_entry')
        self._active_entry = entry
        entry.set_name('active_entry')

        for keyboard in self.keyboards.keyboards:
            keyboard.entry = entry

    def _entry_activated(self, entry):
        if entry is not self._active_entry:
            self._set_active_entry(entry)
        else:
            # The entry was already active.
            if entry is self._entries[-1]:
                # This is the last entry.
                empty_entries = \
                    [entry for entry in self._entries if not entry.content]
                if not empty_entries:
                    # All entries are filled, the user wants to validate.
                    self.keyboards.select_key('enter')
            else:
                # This is not the last entry, make the next one active.
                index = self._entries.index(self._active_entry) + 1
                self._set_active_entry(self._entries[index])

        self.keyboards.set_focus()

    def update_style_properties(self, props=None):
        if props is None:
            return

        remaining_props = {}

        for key, value in props.iteritems():
            try:
                subwidget, attribute = key.split('-', 1)
            except ValueError:
                remaining_props[key] = value
                continue
            if subwidget == 'entry':
                # This property applies to all entries.
                for entry in self._entries:
                    entry.update_style_properties({attribute: value})
            else:
                remaining_props[key] = value

        if remaining_props:
            return super(Keyboard, self).update_style_properties(remaining_props)

    def clean(self):
        for entry in self._entries:
            entry.disconnect_by_func(self._entry_activated)
        self.keyboard_list.disconnect_by_func(self._switch_keyboard_cb)
        for keyboard in self.keyboards.keyboards:
            keyboard.disconnect_by_func(self._key_press_special_cb)
            keyboard.disconnect_by_func(self._key_press_char_cb)
        self.keyboard_list.clean()
        return super(Keyboard, self).clean()

    def set_focus(self):
        # Overridden from elisa.plugins.pigment.widgets.box.Box
        if self._last_focused_widget is None:
            # First time we get the focus, transfer it to the OSK.
            self.keyboard_layout.set_focus()
            return True
        return super(Keyboard, self).set_focus()

    def _key_press_char_cb(self, keyboard, char):
        self._active_entry.content += char

    def _key_press_special_cb(self, keyboard, value):
        if value == 'delete':
            self._active_entry.content = self._active_entry.content[:-1]
        elif value == 'enter':
            self._active_entry.emit('activated')
            index = self._entries.index(self._active_entry) + 1
            try:
                # Make the next entry active
                self._entry_activated(self._entries[index])
            except IndexError:
                # Validate the contents of the entries
                self.emit('validated')

    def _switch_keyboard_cb(self, keyboard_list):
        focus = self.keyboards.focus
        self.keyboards.next()
        if focus:
            # Grab the focus back if the keyboard has it previously.
            self.keyboards.set_focus()

    def handle_input(self, manager, event):
        if event.value == EventValue.KEY_TAB:
            if self.keyboards.focus:
                # The OSK has the focus: the TAB key changes the active entry
                # cycling through all the entries.
                index = self._entries.index(self._active_entry) + 1
                entry = self._entries[index % len(self._entries)]
                if entry is not self._active_entry:
                    self._set_active_entry(entry)
                    return True
                else:
                    return False
            else:
                # One of the entries has the focus: the TAB key changes the
                # selected entry cycling through all the entries.
                return self.cycle_focus(excludes=[self.keyboard_layout])

        return super(Keyboard, self).handle_input(manager, event)

    @classmethod
    def _demo_widget(cls, *args, **kwargs):
        widget = cls()
        widget.title.foreground.label = "TYPE SOMETHING IN"
        widget.size = (400.0, 250.0)
        return widget


if __name__ == '__main__':
    from elisa.plugins.poblesec.widgets.keyboard import Keyboard
    Keyboard.demo()
    try:
        __IPYTHON__
    except NameError:
        import pgm
        pgm.main()
