# -*- 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.
#
# Authors: Olivier Tilloy <olivier@fluendo.com>
#          Florian Boucault <florian@fluendo.com>

"""
A configurable on-screen keyboard.
"""

from elisa.core.input_event import EventValue, EventSource, UnicodeInputEvent

from elisa.plugins.pigment.graph.image import Image
from elisa.plugins.pigment.graph.group import Group

from elisa.plugins.pigment.widgets.widget import Widget
from elisa.plugins.pigment.widgets.button import Button
from elisa.plugins.pigment.widgets.const import *
from elisa.plugins.pigment.widgets.theme import Theme
from elisa.plugins.pigment.widgets.box import HBox, VBox, ALIGNMENT

import gobject
import string
import os.path

import pgm

from xml.dom import minidom



class Key(object):

    """
    One given key of a virtual keyboard.
    A key has a type that determines its behaviour when activated:
     - L{Key.CHAR}: a normal character
     - L{Key.SWITCH}: switch between caps
     - L{Key.DELETE}: backspace
     - L{Key.PLACEHOLDER}: an invisible key
     - L{Key.MISC}: a key with a miscellaneous behaviour
    """

    CHAR = 'char'
    SWITCH = 'switch'
    DELETE = 'delete'
    PLACEHOLDER = 'placeholder'
    MISC = 'misc'

    def __init__(self, ktype, kname, kwidth, kvalues=None,
                 label=None, image=None):
        """
        Constructor.

        @param ktype:   the type of the key (one of (L{Key.CHAR},
                        L{Key.SWITCH}, L{Key.DELETE}, L{Key.PLACEHOLDER},
                        L{Key.MISC}))
        @type ktype:    C{str}
        @param kname:   a name for the key (may not be unique)
        @type kname:    C{str}
        @param kwidth:  the number of columns a key should occupy, default is 1
        @type kwidth:   C{int}
        @param kvalues: the value of the key (depends on its type).
                         - if ktype is L{Key.CHAR}, then kvalues is a C{dict}
                           associating each cap to a unicode value
                         - if ktype is L{Key.SWITCH}, then kvalues is a C{list}
                           of caps to cycle through when switching
                         - if ktype is L{Key.DELETE}, then kvalues is ignored
                         - if ktype is L{Key.PLACEHOLDER}, then kvalues is
                           ignored
                         - if ktype is L{Key.MISC}, then kvalues is a
                           miscellaneous string value
        @type kvalues:  C{dict} of C{str}, or C{list} of C{str}, or C{None}, or
                        C{str}
        @param label:   the label to display on the graphical representation of
                        the key
        @type label:    C{str}
        @param image:   an optional image resource to display on the graphical
                        representation of the key
        @type image:    C{str}
        """
        self.ktype = ktype
        self.kname = kname
        self.kwidth = kwidth
        self.kvalues = kvalues
        self.label = label
        self.image = image

    def to_xml(self, document):
        """
        Dump the key to an XML node.

        @param document: the XML document for which to generate the node
        @type document:  L{xml.dom.minidom.Document}

        @return: an XML node representing the key
        @rtype:  L{xml.dom.minidom.Element}
        """
        node = document.createElement('key')
        node.setAttribute('type', self.ktype)
        node.setAttribute('name', self.kname)
        node.setAttribute('width', '%i' % self.kwidth)
        if self.kvalues is not None:
            if self.ktype == Key.CHAR:
                for key, value in self.kvalues.iteritems():
                    node.setAttribute(key, value)
            elif self.ktype == Key.SWITCH:
                node.setAttribute('caps', ', '.join(self.kvalues))
            elif self.ktype == Key.MISC:
                node.setAttribute('value', self.kvalues)
        if self.label is not None:
            node.setAttribute('label', self.label)
        if self.image is not None:
            node.setAttribute('image', self.image)
        return node

    @classmethod
    def from_xml(cls, key_node, caps):
        """
        Instantiate a key from its XML representation.

        @param key_node: the XML representation of the key
        @type key_node:  L{xml.dom.minidom.Element}
        @param caps:     the list of caps of the keyboard
        @type caps:      C{list} of C{str}

        @return:         a key
        @rtype:          L{Key}
        """
        attr = key_node.attributes
        ktype = attr['type'].nodeValue
        try:
            kname = attr['name'].nodeValue
        except KeyError:
            kname = ''
        try:
            kwidth = int(attr['width'].nodeValue)
        except KeyError:
            kwidth = 1
        kvalues = None
        if ktype == cls.CHAR:
            kvalues = {}
            for cap in caps:
                kvalues[cap] = attr[cap].nodeValue
        elif ktype == cls.SWITCH:
            kvalues = attr['caps'].nodeValue.split(', ')
        elif ktype == cls.MISC:
            kvalues = attr['value'].nodeValue
        label = None
        if attr.has_key('label'):
            label = attr['label'].nodeValue
        image = None
        if attr.has_key('image'):
            image = attr['image'].nodeValue
        key = cls(ktype, kname, kwidth, kvalues, label, image)
        return key


class ButtonWithoutGlyph(Button):
    pass

class ButtonWithGlyph(Button):
    pass


class Keyboard(Widget):
    """
    A configurable on-screen keyboard widget.

    Its layout is defined in an XML file.
    It defines several caps that can be cycled through, changing the value
    emitted by each key.

    @ivar current_key: DOCME
    @ivar caps: DOCME
    @ivar current_caps: DOCME
    """

    __gsignals__ = {'key-press-char': (gobject.SIGNAL_RUN_LAST,
                                       gobject.TYPE_BOOLEAN,
                                       (gobject.TYPE_STRING,)),
                    'key-press-special': (gobject.SIGNAL_RUN_LAST,
                                          gobject.TYPE_BOOLEAN,
                                          (gobject.TYPE_STRING,)),
                   }

    def __init__(self, xml_file, button_with_glyph_class=ButtonWithGlyph,
                 button_without_glyph_class=ButtonWithoutGlyph):
        """
        Constructor.

        @param xml_file: the path to an XML file containing the keyboard layout
        @type xml_file:  C{str}
        """
        super(Keyboard, self).__init__()
        self._button_with_glyph_class = button_with_glyph_class
        self._button_without_glyph_class = button_without_glyph_class
        self._keys = []
        self._keys_by_name = {}
        self._keys_widgets = {}
        self.current_key = None
        # FIXME: does caps need to be public?
        self.caps = []
        # FIXME: does current_caps need to be public?
        self.current_caps = None

        self._signal_handler_ids = []

        # Store the motions in an internal cache to avoid computing them every
        # time.
        self._motion_cache = {}
        for direction in (LEFT, RIGHT, TOP, BOTTOM):
            self._motion_cache[direction] = {}

        self._load_layout(xml_file)
        self._render_keys()

        self.update_style_properties(self.style.get_items())

    def clean(self):
        for widget, signal_id in self._signal_handler_ids:
            widget.disconnect(signal_id)
        self._signal_handler_ids = []

        if self.current_key:
            self.current_key = None
        self._keys = []
        self._clean_keys_widgets()
        return super(Keyboard, self).clean()

    def do_mapped(self):
        self._layout_keys()

    def _to_xml(self):
        """
        Dump the keyboard to an XML document.

        @return: an XML document representing the keyboard
        @rtype:  L{xml.dom.minidom.Document}
        """
        implementation = minidom.getDOMImplementation()
        document = implementation.createDocument(None, 'osk', None)
        document.documentElement.setAttribute('caps', ', '.join(self.caps))
        for row in self._keys:
            row_node = document.createElement('row')
            document.documentElement.appendChild(row_node)
            for key in row:
                key_node = key.to_xml(document)
                row_node.appendChild(key_node)
        return document

    def _load_layout(self, xml_file):
        """
        Load the layout of the keyboard from an XML file.

        @param xml_file: the path to an XML file containing the keyboard layout
        @type xml_file:  C{str}
        """
        dom = minidom.parse(xml_file)
        osk = dom.firstChild
        self.caps = osk.attributes['caps'].nodeValue.split(', ')
        self.current_caps = self.caps[0]
        rows = osk.getElementsByTagName('row')
        for row in rows:
            self._keys.append([])
            keys = row.getElementsByTagName('key')
            for key_node in keys:
                key = Key.from_xml(key_node, self.caps)
                self._keys[-1].append(key)
                # Cache the key by name for faster subsequent access
                try:
                    self._keys_by_name[key.kname].append(key)
                except KeyError:
                    self._keys_by_name[key.kname] = [key]
        self.current_key = self._keys[0][0]
        self._clean_keys_widgets()

    def _clean_keys_widgets(self):
        # FIXME: implement
        self._keys_widgets.clear()

    def _render_keys(self):
        for row in self._keys:
            self._render_row(row)

    def _render_row(self, row):
        for key in row:
            try:
                widget = self._keys_widgets[key]
            except KeyError:
                widget = self._create_key_widget(key)
                self._keys_widgets[key] = widget
            widget = self._render_key(key, widget)

    def _create_key_widget(self, key):
        resource = key.image
        if key.ktype == Key.PLACEHOLDER:
            widget = Image()
            # FIXME: hardcoded value
            widget.bg_color = (77, 77, 77, 191)
        else:
            if resource != None:
                widget = self._button_with_glyph_class()
                theme = Theme.get_default()
                image_file = theme.get_resource(resource)
                widget.icon.set_from_file(image_file)
            else:
                widget = self._button_without_glyph_class()
            widget_clicked_id = widget.connect('clicked', self._key_clicked_cb, key)
            self._signal_handler_ids.append((widget, widget_clicked_id))

        widget.set_name('keyboard_key_' + key.kname)
        return widget

    def _render_key(self, key, widget):
        label = ''
        if key.label is not None:
            label = key.label
        elif key.ktype == Key.CHAR:
            label = key.kvalues[self.current_caps]

        if label != '':
            widget.text.label = label

    def _layout_keys(self):
        self.empty()

        rows_box = VBox()
        rows_box.alignment = ALIGNMENT.START
        rows_box.spacing = self._spacing
        rows_box.visible = True
        for row_no, row in enumerate(self._keys):
            row_box = self._layout_row(row_no, row)
            row_box.visible = True
            rows_box.pack_start(row_box, expand=True)

        self.add(rows_box)
        self._rows_box = rows_box

    def _layout_row(self, row_no, row):
        row_box = HBox()
        row_box.alignment = ALIGNMENT.START
        row_box.spacing = self._spacing

        def add_key_widths(result, key):
            return result+key.kwidth

        fx, fy = self.get_factors_relative(self._spacing.unit)
        column_number = reduce(add_key_widths, row, 0)
        total_spacing = (column_number-1)*self._spacing.value*fx
        default_key_width = (1.0-total_spacing)/column_number

        for column_no, key in enumerate(row):
            widget = self._keys_widgets[key]
            c = float(key.kwidth)
            widget.width = default_key_width*c+self._spacing.value*fx*(c-1)
            row_box.pack_start(widget)
            widget.visible = True

        return row_box

    def _set_spacing(self, spacing):
        self._spacing = spacing
        if not self.is_mapped:
            return
        self._layout_keys()
        self._rows_box.spacing = spacing
        for row_box in self._rows_box:
            row_box.spacing = spacing

    def _get_spacing(self):
        return self._spacing

    spacing = property(fget=_get_spacing, fset=_set_spacing)

    def _switch(self):
        index = (self.caps.index(self.current_caps) + 1) % len(self.caps)
        self.current_caps = self.caps[index]
        self._render_keys()

    def _key_clicked_cb(self, button, x, y, z, mbutton, time, data, key):
        if key != self.current_key:
            self.select_key(key)
        self.activate_key(key)


    def _compute_key_row_and_index(self, key):
        for row_no, row in enumerate(self._keys):
            try:
                return row_no, row, row.index(key)
            except ValueError:
                continue

    def _compute_key_column(self, row, index):
        def add_key_widths(result, key):
            return result+key.kwidth

        return reduce(add_key_widths, row[:index], 0)

    def _iterate_vertical(self, key, direction):
        def get_key_at_column(row, column_no):
            c = 0
            for key in row:
                if c <= column_no and column_no < c+key.kwidth:
                    return key
                c += key.kwidth

        row_no, row, index = self._compute_key_row_and_index(key)
        column_no = self._compute_key_column(row, index)

        if direction == BOTTOM:
            iterator = self._keys[row_no:]
        if direction == TOP:
            iterator = reversed(self._keys[:row_no])

        for row in iterator:
            key = get_key_at_column(row, column_no)
            yield key

    def _iterate_horizontal(self, key, direction):
        row_no, row, index = self._compute_key_row_and_index(key)
        while True:
            if direction == RIGHT:
                index = (index+1)%len(row)
            elif direction == LEFT:
                index = (index-1)%len(row)
            yield row[index]

    def _get_closest_selectable_key(self, orig_key, direction):
        if direction in (RIGHT, LEFT):
            i = self._iterate_horizontal(orig_key, direction)
        elif direction in (TOP, BOTTOM):
            i = self._iterate_vertical(orig_key, direction)

        for key in i:
            if key.ktype != Key.PLACEHOLDER and key != orig_key:
                return key

    def move_selector(self, direction):
        """
        Move the selector in the specified direction.

        @param direction: where to move the cursor
        @type direction:  one of (L{elisa.plugins.pigment.widgets.const.LEFT},
                                  L{elisa.plugins.pigment.widgets.const.RIGHT},
                                  L{elisa.plugins.pigment.widgets.const.TOP},
                                  L{elisa.plugins.pigment.widgets.const.BOTTOM})

        @return: C{True} if the movement was successful, C{False} otherwise
        @rtype:  C{bool}
        """
        new_key = self._get_closest_selectable_key(self.current_key, direction)
        if new_key != None:
            self.select_key(new_key)
            return True
        else:
            return False

    def select_key(self, key):
        """
        Move the selector to a given key on the keyboard.

        @param key: the new key to select
        @type key:  L{Key}
        """
        previous_key = self.current_key
        previous_widget = self._keys_widgets[previous_key]
        previous_widget.state = STATE_NORMAL

        self.current_key = key
        if self.focus:
            current_widget = self._keys_widgets[key]
            current_widget.state = STATE_SELECTED

    def activate_key(self, key):
        """
        Activate a key on the keyboard.

        @param key: the key to activate
        @type key:  L{Key}
        """
        if key.ktype == Key.CHAR:
            self.emit('key-press-char', key.kvalues[self.current_caps])
        elif key.ktype == Key.DELETE:
            self.emit('key-press-special', 'delete')
        elif key.ktype == Key.MISC:
            self.emit('key-press-special', key.kvalues)
        elif key.ktype == Key.SWITCH:
            self._switch()

    def do_state_changed(self, state):
        # FIXME: deactivated styles enforcing on state change because it was
        # causing ugly and useless relayouts
        pass

    def do_focus(self, value):
        current_widget = self._keys_widgets[self.current_key]
        if value:
            current_widget.state = STATE_SELECTED
        else:
            current_widget.state = STATE_NORMAL

    def handle_input(self, manager, event):
        if event.value in (EventValue.KEY_OK, EventValue.KEY_RETURN):
            self.activate_key(self.current_key)
            return True
        elif event.value == EventValue.KEY_GO_LEFT:
            if self.move_selector(LEFT):
                return True
        elif event.value == EventValue.KEY_GO_RIGHT:
            if self.move_selector(RIGHT):
                return True
        elif event.value == EventValue.KEY_GO_UP:
            if self.move_selector(TOP):
                return True
        elif event.value == EventValue.KEY_GO_DOWN:
            if self.move_selector(BOTTOM):
                return True
        elif event.value == EventValue.KEY_SPACE:
            self.emit('key-press-char', u' ')
            return True
        elif event.value == EventValue.KEY_MENU:
            if event.source == EventSource.KEYBOARD:
                self.emit('key-press-special', 'delete')
                return True
        elif event.value in (EventValue.KEY_ESCAPE, EventValue.KEY_TAB):
            pass
        elif isinstance(event, UnicodeInputEvent):
            self.emit('key-press-char', event.value)
            return True
        elif str(event.value).startswith('KEY_'):
            letter = unicode(event.value)[4:]
            if letter.isalnum():
                self.emit('key-press-char', letter)
                return True

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

    def get_key_by_name(self, name):
        """
        Get the first key that matches a given name.

        @param name: the name of the key
        @type name:  C{unicode}

        @return: the first key that matches the name
        @type:   L{elisa.plugins.pigment.widgets.keyboard.Key}

        @warning: if several keys have the same name, the first one found is
                  returned and the others are ignored

        @raise KeyError: if no key matches the given name
        """
        return self._keys_by_name[name][0]

    @classmethod
    def _demo_widget(cls, *args, **kwargs):
        xml_file = os.path.join(os.path.dirname(__file__),
                                'data', 'osk_qwerty.xml')
        widget = cls(xml_file)
        widget.visible = True

        def on_key_press(osk, value):
            print 'OSK key-press: %s' % value

        widget.connect('key-press-char', on_key_press)
        widget.connect('key-press-special', on_key_press)

        return widget

    @classmethod
    def _set_demo_widget_defaults(cls, widget, canvas, viewport):
        Widget._set_demo_widget_defaults(widget, canvas, viewport)
        widget.size = (400.0, 250.0)


if __name__ == '__main__':
    from elisa.plugins.pigment.widgets.keyboard import Keyboard

    osk = Keyboard.demo()
    try:
        __IPYTHON__
    except NameError:
        pgm.main()
