# GNU Enterprise Forms - Curses UI Driver - User Interface
#
# Copyright 2000-2009 Free Software Foundation
#
# This file is part of GNU Enterprise.
#
# GNU Enterprise 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 3, or (at your option) any later version.
#
# GNU Enterprise 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 program; see the file COPYING. If not,
# write to the Free Software Foundation, Inc., 59 Temple Place
# - Suite 330, Boston, MA 02111-1307, USA.
#
# $Id: UIdriver.py 10001 2009-10-23 12:29:30Z reinhard $

import atexit
import curses
import sys

from gnue.common.apps import i18n
from gnue.common.datasources import GLoginHandler
from gnue.forms.input.GFKeyMapper import vk, KeyMapper
from gnue.forms.uidrivers._base.UIdriver import GFUserInterfaceBase
from gnue.forms.uidrivers.curses import dialogs

__all__ = ['GFUserInterface']

# =============================================================================
# User interface object
# =============================================================================

class GFUserInterface(GFUserInterfaceBase):
    """
    An implementation of the base UI toolkit interface using curses
    """

    # -------------------------------------------------------------------------
    # Initialize library
    # -------------------------------------------------------------------------

    def __init__(self, eventHandler, name="Undefined", disableSplash=None,
                parentContainer=None, moduleName=None):

        GFUserInterfaceBase.__init__(self, eventHandler, name, disableSplash,
                                  parentContainer, moduleName)

        self.__screen = curses.initscr()
        atexit.register(curses.endwin)
        curses.raw()
        curses.noecho()
        curses.start_color()

        # Define colors
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
        curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_CYAN)
        curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_WHITE)
        curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_CYAN)
        curses.init_pair(6, curses.COLOR_WHITE, curses.COLOR_RED)
        curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLUE)
        curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_YELLOW)
        curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)

        self.attr = {}
        self.attr['title']       = curses.color_pair(1)
        self.attr['page']        = curses.color_pair(2)
        self.attr['currentpage'] = curses.color_pair(3)
        self.attr['background']  = curses.color_pair(4)
        self.attr['entry']       = curses.color_pair(5)
        self.attr['focusentry']  = curses.color_pair(6) + curses.A_BOLD
        self.attr['disabled']    = curses.color_pair(5) + curses.A_BOLD
        self.attr['status']      = curses.color_pair(7)
        self.attr['fkeys']       = curses.color_pair(4)
        self.attr['infomsg']     = curses.color_pair(8) + curses.A_BOLD
        self.attr['warnmsg']     = curses.color_pair(9) + curses.A_BOLD
        self.attr['errormsg']    = curses.color_pair(9) + curses.A_BOLD \
                                                        + curses.A_BLINK
        self.attr['window 1']    = curses.color_pair(5)
        self.attr['focusdisabled'] = curses.color_pair(6)

        self.__current_form = None

        # TODO: Add a concept of 'lookups' to the GF* layer.
        # Meanwhile we're using Ctrl-W (hardcoded)
        self.lookupKey = 23

        self.widgetWidth = 1
        self.widgetHeight = 1
        self.textWidth = 1
        self.textHeight = 1

        self.__exiting = False

        KeyMapper.setUIKeyMap(self._keymap)
        KeyMapper.loadUserKeyMap({'JUMPPROMPT': 'Ctrl-r',
                                  'NEXTPAGE': 'PAGEDOWN',
                                  'PREVPAGE': 'PAGEUP'})

    # -------------------------------------------------------------------------
    # Activate the given form
    # -------------------------------------------------------------------------

    def show_form(self, form, modal):

        self.__current_form = form

    # -------------------------------------------------------------------------
    # Key mapping for converting e.g. F13 into Shift-F3
    # -------------------------------------------------------------------------

    __shiftkeys = {
        #                   unshifted         Shift  Ctrl   Meta
        #                   ------------------------------------
        curses.KEY_F13:    (curses.KEY_F3,    True,  False, False),
        curses.KEY_F14:    (curses.KEY_F4,    True,  False, False),
        curses.KEY_F15:    (curses.KEY_F5,    True,  False, False),
        curses.KEY_F16:    (curses.KEY_F6,    True,  False, False),
        curses.KEY_F17:    (curses.KEY_F7,    True,  False, False),
        curses.KEY_F18:    (curses.KEY_F8,    True,  False, False),
        curses.KEY_F19:    (curses.KEY_F9,    True,  False, False),
        curses.KEY_F20:    (curses.KEY_F10,   True,  False, False),
        curses.KEY_SRIGHT: (curses.KEY_RIGHT, True,  False, False),
        curses.KEY_SLEFT:  (curses.KEY_LEFT,  True,  False, False),
        curses.KEY_SIC:    (curses.KEY_IC,    True,  False, False),
        curses.KEY_SDC:    (curses.KEY_DC,    True,  False, False),
        curses.KEY_SHOME:  (curses.KEY_HOME,  True,  False, False),
        curses.KEY_SEND:   (curses.KEY_END,   True,  False, False),
        curses.KEY_BTAB:   (9             ,   True,  False, False)
    }

    # -------------------------------------------------------------------------
    # Key mapping between curses codes and gnue codes
    # -------------------------------------------------------------------------

    _keymap = {
        vk.F1        : curses.KEY_F1,
        vk.F2        : curses.KEY_F2,
        vk.F3        : curses.KEY_F3,
        vk.F4        : curses.KEY_F4,
        vk.F5        : curses.KEY_F5,
        vk.F6        : curses.KEY_F6,
        vk.F7        : curses.KEY_F7,
        vk.F8        : curses.KEY_F8,
        vk.F9        : curses.KEY_F9,
        vk.F10       : curses.KEY_F10,
        vk.F11       : curses.KEY_F11,
        vk.F12       : curses.KEY_F12,
        vk.INSERT    : curses.KEY_IC,
        vk.DELETE    : curses.KEY_DC,
        vk.HOME      : curses.KEY_HOME,
        vk.END       : curses.KEY_END,
        vk.PAGEUP    : curses.KEY_PPAGE,
        vk.PAGEDOWN  : curses.KEY_NPAGE,
        vk.UP        : curses.KEY_UP,
        vk.DOWN      : curses.KEY_DOWN,
        vk.LEFT      : curses.KEY_LEFT,
        vk.RIGHT     : curses.KEY_RIGHT,
        vk.TAB       : 9,
        vk.ENTER     : 10,
        vk.RETURN    : 10,
        vk.BACKSPACE : curses.KEY_BACKSPACE
    }

    # -------------------------------------------------------------------------
    # Main loop
    # -------------------------------------------------------------------------

    def mainLoop(self):
        """
        The main loop of the curses application
        """

        while not self.__exiting:
            try:
                key = self.__current_form.wait()

                if key == curses.KEY_RESIZE:
                    (width, height) = self.screen_size()
                    self.__current_form.set_size_and_fit(width, height)

                elif isinstance(key, basestring):
                    self._focus_widget._keypress(key)
                else:
                    if self.__shiftkeys.has_key(key): # translate shifted f-key
                        (key, shift, ctrl, meta) = self.__shiftkeys[key]
                    else:
                        (shift, ctrl, meta) = (False, False, False)

                    self._focus_widget._fkeypress(key, shift, ctrl, meta)

            except Exception:
                sys.excepthook(*sys.exc_info())


    # -------------------------------------------------------------------------
    # Show a message
    # -------------------------------------------------------------------------

    def show_message(self, message, kind, cancel):

        (x, y) = self.screen_size()
        screen = curses.newpad(2, x)

        attr = {
            'Question': self.attr['infomsg'],
            'Info':     self.attr['infomsg'],
            'Warning':  self.attr['warnmsg'],
            'Error':    self.attr['errormsg']
        }

        yes = u_("Yes")
        no  = u_("No")
        ok  = u_("Ok")

        screen.bkgdset(' ', attr[kind])

        screen.move(0, 0)
        screen.clrtobot()

        screen.addstr(0, 1, o(message)[:x-2])

        if kind == 'Question':
            screen.addstr(1, 1, '(' + yes[:1] + ')' + yes[1:] + '/' + \
                                '(' + no[:1] + ')' + no[1:] + ' ? ')
        else:
            screen.addstr(1, 1, '[' + ok + ']')
            screen.move(1, 2)

        if kind == 'Question':
            validKeys = {ord(yes[0].upper()): True,
                         ord(yes[0].lower()): True,
                         ord(no[0].upper()): False,
                         ord(no[0].lower()): False}
        else:
            validKeys = {10: True}

        if cancel:
            validKeys[27] = None

        screen.refresh(0, 0, y-2, 0, y, x)

        while True:
            key = screen.getch()
            if validKeys.has_key(key):
                result = validKeys[key]
                break

        if self.__current_form:              # repaint status line of form
            self.__current_form.status_message(None)

        return result


    # -------------------------------------------------------------------------
    # Show a simple error message
    # -------------------------------------------------------------------------

    def _ui_show_error_(self, message):

        assert gDebug(2, "Error: %s"  % message)
        self.show_message(message, 'Error', False)

    # -------------------------------------------------------------------------
    # Show exception information
    # -------------------------------------------------------------------------

    def _ui_show_exception_(self, group, name, message, detail):

        # Give us a chance to debug exceptions until we have a 'good' exception
        # dialog
        assert gDebug(2, "MESSAGE: %s" % message)
        assert gDebug(2, "Detail : %s" % repr(detail))
        self.show_message(message, 'Error', False)


    # -------------------------------------------------------------------------
    # Select one of the given options
    # -------------------------------------------------------------------------

    def getOption(self, title, options):
        """
        Create a dialog box for selecting one of the given options
        @param title: title for the dialog box
        @param options: dictionary to select a value from

        @return: the selected option (dict-value) or None if cancelled
        """

        if self.__current_form:
            (left, top, right, bottom) = self.__current_form.get_canvas()
        else:
            (right, bottom) = self.__screen.getmaxyx()
            left = top = 0

        dialog = dialogs.OptionsDialog(title, options, self.attr,
                self.lookupKey, left, top, right, bottom)
        try:
            return dialog.run()

        finally:
            if self.__current_form:
                self.__current_form.refresh()
            else:
                self.__screen.refresh()


    # -------------------------------------------------------------------------
    # Clean up everything
    # -------------------------------------------------------------------------

    def _ui_exit_(self):

        self.__exiting = True


    # -------------------------------------------------------------------------
    # Get input data for a given set of fields
    # -------------------------------------------------------------------------

    def _getInput(self, title, fields, cancel=True):

        if self.__current_form:
            (left, top, right, bottom) = self.__current_form.get_canvas()
        else:
            (right, bottom) = self.__screen.getmaxyx()
            left = top = 0

        dialog = dialogs.InputDialog(title, fields, self.attr, cancel, left,
                top, right, bottom)
        dialog.run()
        return dialog.inputData


    # -------------------------------------------------------------------------
    # Helper method for forms to get screen size
    # -------------------------------------------------------------------------

    def screen_size(self):

        (y, x) = self.__screen.getmaxyx()
        return (x, y)

    # -------------------------------------------------------------------------
    # Get a key from keyboard
    # -------------------------------------------------------------------------

    def get_key(self, window):
        """
        Get keyboard input from the given window.  Function keys are returned
        as curses.KEY_* constant (or a corresponding numeric value).  Other
        (textual) input is returned as unicode.

        @param window: the curses window to get the input from
        @returns: curses.KEY_* constant for function keys or unicode string for
            the given input
        """

        key = self.__get_next_key(window)

        # Function keys are returned as key-code
        if key > 255:
            return key

        result = [key]
        if i18n.encoding.lower().startswith('utf'):
            for i in range(self.__get_hibits(key)-1):
                result.append(self.__get_next_key(window))

        return unicode("".join([chr(i) for i in result]), i18n.encoding)

    # -------------------------------------------------------------------------

    def __get_next_key(self, window):

        # In nodelay mode the result of getch() is -1 if there is no input.
        # This appears to be returned after a window resize (RESIZE event) too.
        code = -1
        while code < 0:
            code = window.getch()

        return code

    # -------------------------------------------------------------------------

    def __get_hibits(self, value):
        """ Returns the number of set bits starting at the MSB """

        result = 0
        for i in range(7, -1, -1):
            if value & (1 << i):
                result += 1
            else:
                break

        return result
