# GNU Enterprise Forms - Curses UI Driver - UI specific dialogs
#
# Copyright 2001-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: dialogs.py 9956 2009-10-11 18:54:57Z reinhard $

import curses
import curses.ascii
import sys

from gnue.common.apps import i18n

__all__ = ['OptionsDialog', 'InputDialog']

# =============================================================================
# Dialog box for selection an option from a given options dictionary
# =============================================================================

class OptionsDialog:

    # -------------------------------------------------------------------------
    # Constructor
    # ------------------------------------------------------------------------

    def __init__(self, title, options, attrs, quit=None, left=0, top=0,
            right=80, bottom=24, returnKeys=False):

        curses.noecho()
        curses.cbreak()

        try:
            self.__oldcursor = curses.curs_set(0)
        except:
            self.__oldcursor = 1

        self.__data = [("%s" % val, key) for (key, val) in options.items()]
        self.__data.sort()

        maxString = max ([len (item) for (item, key) in self.__data])

        width  = min(right - left, max((maxString + 4), len(title) + 4))
        height = min(bottom - top, len(options) + 4)
        left   = max(left, (right - left - width) / 2)
        top    = max(top, (bottom - top - height) / 2)

        width  = min(left + width, right) - left
        height = min(top + height, bottom) - top

        self.__window = curses.newwin(height, width, top, left)
        self.__window.keypad(1)
        self.__window.box()

        self.__normal  = attrs.get('window 1', attrs['background'])
        self.__reverse = attrs['focusentry']
        self.__quit    = quit


        self.__dwidth = width - 4
        self.__window.bkgd(' ', self.__normal)
        self.__window.addstr(1, 2, o(title.center(self.__dwidth)))
        self.__window.hline(2, 1, curses.ACS_HLINE, width - 2)

        self.__offset = 0
        self.__index  = 0
        self.__pIndex = 1
        self.__page   = height - 4
        self.__returnKeys = returnKeys

        self.__refresh()


    # -------------------------------------------------------------------------
    # Start the selection
    # -------------------------------------------------------------------------

    def run(self):
        """
        Start the selection dialog.

        @return: selected item or None if canceled
        """

        self.__window.refresh()

        try:
            while 1:
                key = self.__window.getch()

                if key == 27 or key == self.__quit:
                    return None

                elif key == curses.KEY_UP:
                    self.__move(-1)

                elif key == curses.KEY_DOWN:
                    self.__move(+1)

                elif key == 10:
                    return self.__data[self.__index][self.__returnKeys]

        finally:
            try:
                curses.curs_set(self.__oldcursor)
            except:
                pass


    # -------------------------------------------------------------------------
    # Move the selection in a given direction
    # -------------------------------------------------------------------------

    def __move(self, direction):

        self.__pIndex += direction
        self.__index  += direction

        if self.__pIndex > self.__page:
            if self.__index < len(self.__data):
                self.__offset += direction

        elif self.__pIndex < 1:
            self.__offset = max(0, self.__offset - 1)

        # Make sure to stay in bounds
        self.__index = max(0, self.__index)
        self.__index = min(len(self.__data) - 1, self.__index)

        self.__pIndex = max(1, self.__pIndex)
        self.__pIndex = min(self.__page, self.__pIndex)

        self.__refresh()


    # -------------------------------------------------------------------------
    # Refresh the dialog's window
    # -------------------------------------------------------------------------

    def __refresh(self):

        lines = self.__data[self.__offset:self.__offset + self.__page]
        for (line, (value, key)) in enumerate(lines):
            text = " %s " % value.ljust(self.__dwidth)[:self.__dwidth]
            if line == self.__pIndex - 1:
                attr = self.__reverse
            else:
                attr = self.__normal

            self.__window.addnstr(3 + line, 1, o(text), self.__dwidth + 2, attr)

        self.__window.move(3 + self.__pIndex - 1, 1)



# =============================================================================
# Class implementing a versatile input dialog
# =============================================================================

class InputDialog:

    # -------------------------------------------------------------------------
    # Create a new input dialog
    # -------------------------------------------------------------------------

    def __init__(self, title, fields, attrs, cancel = True, left = 0, top = 0,
        right = 80, bottom = 24):

        curses.noecho()
        curses.cbreak()

        self.attrs  = attrs
        self.fields = fields
        self.cancel = cancel
        self.__buildWindow(title, left, top, right, bottom)

        self.inputData  = {}

        # A dictionary with the fieldname as key and a triple (master, allowed,
        # current) as data items
        self.lookups    = {}

        y = 2
        self.__entries = []

        for (label, name, ftype, default, master, elements) in fields:
            if ftype == 'image':
                continue

            y += 1

            if ftype in ['label', 'warning']:
                attr = [attrs['background'], attrs['warnmsg']][ftype == 'warning']
                cw   = self.__width - 2
                lh   = label.count('\n') + 1

                for item in label.splitlines():
                    self.__window.addnstr(y, 1, o(item.center(cw)), cw, attr)
                    y += 1

                if lh > 1: y -= 1

            elif ftype in ['string', 'password', 'dropdown']:
                self.__window.addnstr(y, 2, o(label), self.__leftcol)

                win = curses.newwin(1, self.__rightcol,
                              self.__wtop + y, self.__wleft + self.__leftcol + 2)
                self.__entries.append((win, name, default, ftype == 'password'))

                if ftype == 'dropdown':
                    values = elements[0][1]
                    self.lookups[name] = (master, values,
                                           self.__getCurrent(master, values))
                    dispValue = self.lookups[name][2].get(default, '')
                else:
                    dispValue = default

                self.__display(win, dispValue, ftype == 'password')



    # -------------------------------------------------------------------------
    # Run the dialog
    # -------------------------------------------------------------------------

    def run(self):

        old = curses.curs_set(1)
        self.__window.refresh()

        index = 0
        while index < len(self.__entries):
            (win, name, default, hidden) = self.__entries[index]
            cvalue = self.inputData.get(name, default)
            (value, code) = self.__accept(win, cvalue, name, hidden)
            if value is None:
                if name in self.inputData:
                    del self.inputData[name]
            else:
                self.inputData[name] = value

            if code == 27 and self.cancel:
                self.inputData = None
                break

            elif code == curses.KEY_UP:
                index = max(0, index - 1)

            else:
                index += 1

        curses.curs_set(old)


    # -------------------------------------------------------------------------
    # Calculate and build the window
    # -------------------------------------------------------------------------

    def __buildWindow(self, title, left, top, right, bottom):

        leftcol  = 0
        rightcol = 0
        width    = 0
        height   = 5        # box + title + spacer + bottom

        for (label, name, ftype, default, master, elements) in self.fields:
            if ftype in ['label', 'warning']:
                arr = ([width] + [len(part) + 2 for part in label.splitlines()])
                width = max([width] + [len(part) + 2 for part in label.splitlines()])
                height += label.count('\n') + 1

            elif ftype in ['string', 'password']:
                leftcol  = max(leftcol, len(label) + 1)
                rightcol = max(rightcol, 20)
                height  += 1

            elif ftype == 'dropdown':
                leftcol = max(leftcol, len(label) + 1)
                height += 1

                # We introspect only the first control since we do not provide them all
                # for input
                (label, allowed) = elements[0]
                widest = 0
                if master is None:
                    data = [allowed]
                else:
                    data = allowed.values()

                for vdict in data:
                    widest = max([widest] + [len(i) for i in vdict.values()])

                rightcol = max(rightcol, widest)


        self.__width  = min(max(leftcol + rightcol + 4, width, len(title)+2), right - left)
        self.__height = min(height, bottom - top)
        self.__wtop   = (bottom - top - self.__height) / 2
        self.__wleft  = (right - left - self.__width) / 2

        self.__window = curses.newwin(self.__height, self.__width,
                                       self.__wtop, self.__wleft)
        self.__window.bkgd(' ', self.attrs['background'])
        self.__window.box()
        self.__window.keypad(1)

        self.__window.addstr(1, 1, o(title.center(self.__width - 2)))

        self.__leftcol  = leftcol
        self.__rightcol = rightcol



    # -------------------------------------------------------------------------
    # Get input for a given field
    # -------------------------------------------------------------------------

    def __accept(self, win, default, name, hidden = False):

        if name in self.lookups:
            (master, allowed, current) = self.lookups[name]
            current = self.__getCurrent(master, allowed)
            self.lookups[name] = (master, allowed, current)

            reverse = {}
            for (k, v) in current.items():
                reverse[v] = k

            value = current.get(default, '')
        else:
            value   = default or ''
            current = None
            revese  = None

        win.keypad(1)

        while True:
            self.__display(win, value, hidden, True)

            code = self.__window.getch()

            if code in [9, 10, 27, curses.KEY_DOWN, curses.KEY_UP]:
                if code == 27:
                    rvalue = None

                elif code == 9:
                    code = curses.KEY_DOWN

                if code != 27 and name in self.lookups:
                    if reverse and not value in reverse:
                        curses.beep()
                        continue
                    else:
                        rvalue = reverse.get(value)
                else:
                    rvalue = value

                break

            elif code == curses.KEY_BACKSPACE:
                value = value[:-1]

            elif current and code == 23:
                op = OptionsDialog('Select Option', current, self.attrs, 23)
                v = op.run()
                self.__window.redrawwin()
                curses.doupdate()

                if v is not None:
                    value = v
                    curses.ungetch(10)

            elif curses.ascii.isprint(code):
                value += chr(code)

        self.__display(win, value, hidden, False)

        return (rvalue, code)


    # -------------------------------------------------------------------------
    # Display the visible portion of a given value within a window
    # -----------------------------------------------------------------------

    def __display(self, win, value, hidden = False, edit = False):

        data = value or ''
        attr = self.attrs[edit and 'focusentry' or 'entry']
        win.bkgd(' ', attr)

        length = win.getmaxyx()[1] - 1
        (top, left)   = win.getbegyx()
        (mtop, mleft) = self.__window.getbegyx()
        top  -= mtop
        left -= mleft

        dvalue = [data, '*' * len(data)][hidden]
        offset = max(0, len(dvalue) - length)
        pos    = min(len(dvalue) - offset, length)

        out = dvalue[offset:offset + length].ljust(length + 1)
        self.__window.addnstr(top, left, out, length + 1, attr)

        self.__window.move(top, (pos + left))


    # -------------------------------------------------------------------------
    # Get the current value dictionary for a given dropdown
    # -------------------------------------------------------------------------

    def __getCurrent(self, master, values):

        if master is None:
            return values
        else:
            return values.get(self.inputData.get(master), {})


# =============================================================================
# Module self test
# =============================================================================

if __name__ == '__main__':
    curses.initscr()
    curses.cbreak()
    curses.raw()
    curses.start_color()

    curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_CYAN)
    curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_WHITE)
    curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_RED)
    curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_CYAN)
    curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)

    attrs = {'background': curses.color_pair(4),
             'focusentry': curses.color_pair(5),
             'entry'     : curses.color_pair(6),
             'window 1'  : curses.color_pair(3),
             'warnmsg'   : curses.color_pair(9) + curses.A_BOLD}

    opts = {'foo': 'Foobar', 'baz': 'Barbaz and the Gang!',
            2: 'something', 3: 'quite', 4: 'interesting stuff', 5: 'hey ho',
            7: 'number 7'}

    opts = {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7, 8:8, 9:9, 10:10, 11:11, 12:12,
    13:13, 14:14, "k15":15, 16:16, 17:17, 18:18, 19:19, 20:20}
    #d = OptionsDialog('Foos', opts, attrs, quit = 23, top = 2, bottom = 22,
            #returnKeys = True)
    #r = d.run()

    #curses.endwin()



    #print "RESULT:", r

    #sys.exit()

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

    cname = {'c1': 'demoa', 'c2': 'demob'}
    ckey  = {'c1': 'ck-A', 'c2': 'ck-B'}

    wija = {'c1': {'04': '2004', '05': '2005'},
            'c2': {'24': '2024', '25': '2025', '26': '2026'}}

    codes = {'24': {'241': 'c-24-1', '242': 'c-24-2'},
             '25': {'251': 'c-25-1'}}

    descr = 'Hier ist ein langer Text\nMit einem Zeilenumbruch'
    fields = [('Foo!', '/home/johannes/gnue/share/gnue/images/gnue.png', 'image',
               None, None, []),
              ('Username', '_username', 'string', 'frodo', None, \
                [('Name of the user', None)]),
              ('', None, 'label', None, None, []),
              ('Password', '_password', 'password', 'foo', None, [('yeah',1)]),
              ('Foobar', '_foobar', 'dropdown', 'frob', None, \
                  [('single', {'trash': 'Da Trash', 'frob': 'Frob'})]),
              ('Multi', '_multi', 'dropdown', '100', None, \
                  [('name', {'50': 'A 50', '100': 'B 100', '9': 'C 9'}),
                  ('sepp', {'50': 'se 50', '100': 'se 100', '9': 'se 9'})]),
              (descr, '_depp', 'label', 'furz', None, []),
              ('Das ist jetzt ein Fehler', None, 'warning', None, None, []),
              ('Firma', 'company', 'dropdown', 'c1', None,
                  [('Name', cname), ('Code', ckey)]),
              ('Wirtschaftsjahr', 'wija', 'dropdown', '05', 'company',
                  [('Jahr', wija)]),
              ('Codes', 'codes', 'dropdown', None, 'wija',
                  [('Code', codes)]),
              (u"Dre\xf6ksau 'bl\xf6sepp'", None, 'warning', None, None, [])]

    dlg = InputDialog('foobar', fields, attrs)
    dlg.run()

    curses.endwin()

    print "RES:", dlg.inputData
