# GNU Enterprise Forms - Curses UI Driver - Widget Base
#
# 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: _base.py 10007 2009-10-26 21:19:19Z reinhard $

import curses

from gnue.forms.input.GFKeyMapper import KeyMapper

from gnue.forms.uidrivers._base.widgets._base import UIWidget

__all__ = ['UIHelper', 'box', 'ManagedBox']

# =============================================================================
# Widget helper class
# =============================================================================

class UIHelper(UIWidget):
    """
    The base class for all user interface widgets in the curses driver.
    All UI widgets have a top/left coordinate as well as a width and height.
    """

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

    def __init__(self, event):

        UIWidget.__init__(self, event)
        self.__focus_index = None

        self.top = 0
        self.left = 0
        self.width = 0
        self.height = 0
        self.last_position = None
        self.last_selection = None


    # -------------------------------------------------------------------------
    # Create a widget
    # -------------------------------------------------------------------------

    def _create_widget_(self, event, spacer):
        """
        Initialize the coordinates of a widget.  These coordinates will be set
        later using set_size_and_fit().
        """
        self._parent = event.container


    # -------------------------------------------------------------------------
    # Virtual methods
    # -------------------------------------------------------------------------

    def _container_is_ready_(self):
        """
        Descendants implement this method to determine wether their container
        is ready for output or not.
        """
        parent = self.getParent()
        if parent:
            result = parent.ready()
        else:
            result = False

        return result

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

    def ready(self):
        """ Returns wether the container is ready for output or not """
        return self._container_is_ready_()


    # -------------------------------------------------------------------------
    # Call a method of a given widget (if it exists)
    # -------------------------------------------------------------------------

    def _call_widget_(self, index, method, *args, **kwargs):

        func = getattr(self.widgets[index], method, None)
        if func:
            func(*args, **kwargs)

    
    # -------------------------------------------------------------------------
    # Focus handling
    # -------------------------------------------------------------------------

    def _ui_set_focus_(self, index):

        self._uiDriver._focus_widget = self
        self.__focus_index = index

        # If there was a cursor position and a selection set while the ui was
        # not ready for painting, restore to that position/selection before
        # moving the focus
        if self.last_position is not None:
            self._ui_set_cursor_position_(index, self.last_position)
        if self.last_selection is not None:
            self._ui_set_selected_area_(index, *self.last_selection)

        self._call_widget_(index, '_ui_set_focus_')

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

    def _ui_focus_in_(self, index):

        self._call_widget_(index, '_ui_focus_in_')

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

    def _ui_focus_out_(self, index):

        self._call_widget_(index, '_ui_focus_out_')
        self.__focus_index = None

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

    def _ui_set_current_row_(self, index):
        
        pass


    # -------------------------------------------------------------------------
    # Set text for widget
    # -------------------------------------------------------------------------

    def _set_text(self, index, text, attr, selection = None):

        if self.getParent().ready():
            line = self.top + index * (self._gfObject._gap + 1)

            if selection:
                (s1, s2) = selection
                self._parent.write(self.left, line, text[:s1], attr)
                self._parent.write(self.left + s1, line, text[s1:s2],
                                    attr + curses.A_STANDOUT)
                self._parent.write(self.left + s2, line, text[s2:], attr)
            else:
                self._parent.write(self.left, line, text, attr)


    # -------------------------------------------------------------------------
    # Keypress
    # -------------------------------------------------------------------------

    def _keypress(self, key):

        if self.__focus_index is not None:
            widget = self.widgets[self.__focus_index]

            # If the widget has an implementation for a keypress handler, let
            # it try to handle the key
            func = getattr(widget, '_keypress', None)
            if func and func(key):
                return

        ord_c = ord(key)
        # Tab and Enter shouldn't be control keys
        if ord_c in [9, 10]:
            (evt, arg) = KeyMapper.getEvent(ord_c, False, False, False)
            self._request(evt, triggerName=arg)

        # control key
        elif ord_c < 32:
            (evt, arg) = KeyMapper.getEvent(ord_c + 96, False, True, False)
            self._request(evt, triggerName=arg)

        else:
            self._request('KEYPRESS', text=key)


    # -------------------------------------------------------------------------
    # Function keypress
    # -------------------------------------------------------------------------

    def _fkeypress(self, key, shift, ctrl, meta):

        if self.__focus_index is not None:
            widget = self.widgets[self.__focus_index]

            # If the widget has an implementation for a keypress handler, let
            # it try to handle the key
            func = getattr(widget, '_fkeypress', None)
            if func and func(key, shift, ctrl, meta):
                return

        (evt, args) = KeyMapper.getEvent(key, shift, ctrl, meta)
        self._request(evt, triggerName = args)


    # -------------------------------------------------------------------------
    # Get the minimum size of a widget
    # -------------------------------------------------------------------------

    def get_size_hints(self, vertical=None):
        """
        Returns the minimal space needed by a widget as well as it's proportion
        within it's container and the size of it's label (if any).  For a
        positioned layout this is always the width and height specified in the
        GFD file.  Descendants will likely override this method.

        @param vertical: if True, the widget is used in a vertical context,
            otherwise in a horizontal.

        @returns: tuple (min-width, min-height, label-width, proportion)
        """

        if not self.managed:
            return (self.chr_w, self.chr_h, 0, 0)
        else:
            return (20, 1, 0, 0)

    # -------------------------------------------------------------------------
    # Set the size for a widget and it's children
    # -------------------------------------------------------------------------

    def set_size_and_fit(self, width, height):
        """
        Set the size (width and height) of a widget.
        """

        self.width = width
        self.height = height


    # -------------------------------------------------------------------------
    # Properties
    # -------------------------------------------------------------------------

    def __get_right(self):
        return self.left + self.width - 1

    def __get_bottom(self):
        return self.top + self.height - 1

    right = property(__get_right, None, None)
    bottom = property(__get_bottom, None, None)


# =============================================================================
# Base class for managed boxes
# =============================================================================

class ManagedBox(UIHelper):
    """
    This class provides the basic algorithms for layout management in curses.

    @cvar vertical: if True, the box is treated as a vertical container,
        otherwise as a horizontal one
    @cvar _hints_: dictionary with the size hints of the container's children
        using the child-index (into self._children) as key
    """

    vertical = True
    _hints_ = {}

    # -------------------------------------------------------------------------
    # Order the size hints according to their proportion
    # -------------------------------------------------------------------------

    def order_size_hints(self, hints):
        """
        Order the size hints according to their proportion starting with the
        biggest one.

        @param hints: dictionary with the size hints, where the child-index is
            used as key

        @returns: tuple with the ordered size hints and the minimum space
            required (in the requested direction).  Each item of the ordered
            sequence consists of a tuple (proportion, child-index).
        """

        result = []
        needed = 0

        for key, (minw, minh, label, proportion) in hints.items():
            result.append((proportion, key))
            if self.vertical:
                needed += minh
            else:
                needed += minw

        # Make sure to have the gap (space) between widgets also available
        if not self.vertical:
            needed += (len(result) - 1)
            needed += self._horizontal_offset_()

        result.sort()
        result.reverse()

        return result, needed


    # -------------------------------------------------------------------------
    # Get the minimum space required by this box
    # -------------------------------------------------------------------------

    def get_size_hints(self, vertical=None):
        """
        Returns the minimum space needed by this managed container.  As a side
        effect this method populates the size-hints dictionary (_hints_).

        @returns: tuple of (min. width, min. height, widest label, proportion)
        """

        self._hints_ = {}
        for (index, child) in enumerate(self._children):
            self._hints_[index] = child.get_size_hints(self.vertical)

        if not self._hints_:
            return (2, 2, 0, 0)

        hints = self._hints_.values()

        prop = sum([i[3] for i in hints])

        if self.vertical:
            mx_label = max([i[2] for i in hints])
            # min. width: space + widest label + space + widest widget + space
            minw = mx_label + max([i[0] for i in hints]) + 3
            # min. height: widget + widget + widget + ...
            minh = sum([i[1] for i in hints])
        else:
            # min. width: space + max(widget/label) + space + ... + space
            minw = sum([max(i[0], i[2]) for i in hints]) + len(hints) + 1
            # min. height: highest widget
            minh = max([i[1] for i in hints])

        # If a box has a label we have to add the border to the minimum space
        (decw, dech) = self._decoration_size_()
        minw += decw
        minh += dech

        if self._gfObject.has_label:
            label_width = len(self._gfObject.label)
        else:
            label_width = 0

        return (minw, minh, label_width, prop)


    # -------------------------------------------------------------------------
    # Set the size of a box and arrange it's children
    # -------------------------------------------------------------------------

    def set_size_and_fit(self, width, height):
        """
        Set the size of a managed container and layout it's child widgets
        according to this space.
        """

        self.width = width
        self.height = height

        (decw, dech) = self._decoration_size_()

        (ordered, needed) = self.order_size_hints(self._hints_)
        if self.vertical:
            available = self.height - dech - needed
        else:
            available = self.width - decw - needed

        # Distribute available space among the stretchable children
        self._sizes_ = {}
        if self._hints_:
            sum_prop = sum([i[3] for i in self._hints_.values()]) or 1
            max_label= max([i[2] for i in self._hints_.values()])
        else:
            sum_prop = 0
            max_label = 0

        for (i, index) in ordered:
            (minw, current_h, label, proportion) = self._hints_[index]

            if self.vertical:
                current_w = self.width - decw - self._horizontal_offset_()
                if max_label:
                    current_w -= (max_label + 1)
            else:
                current_w = max(minw, label)

            if proportion and available:
                add = min(int(available / sum_prop * proportion), available)
                available -= add
                if self.vertical:
                    current_h += add
                else:
                    current_w += add

            self._sizes_[index] = (current_w, current_h)

        # If some of the available space is left, add it to the widget with the
        # biggest proportion
        if available and ordered:
            index = ordered[0][1]
            (cwidth, cheight) = self._sizes_[index]

            if self.vertical:
                cheight += available
            else:
                cwidth += available
            self._sizes_[index] = (cwidth, cheight)

        # Layout the children
        self._add_decoration_()

        last_x, last_y = self._get_upper_left_()

        for (index, child) in enumerate(self._children):
            cwidth, cheight = self._sizes_[index]

            (inc_w, inc_h) = self._add_child_(child, index, last_x, last_y)
            last_x += inc_w
            last_y += inc_h

            child.set_size_and_fit(cwidth, cheight)


    # -------------------------------------------------------------------------
    # Virtual methods
    # -------------------------------------------------------------------------

    def _decoration_size_(self):
        """
        Get the size for decorations (i.e. border) needed by this object.
        Descendants might override this method.

        @returns: tuple with (width, height) of the decoration
        """
        return (0, 0)

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

    def _add_decoration_(self):
        """
        Add the decoration (i.e. border) to the box
        """
        pass

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

    def _vertical_offset_(self):
        """
        Return the vertical offset of a child widget from the top left corner.
        Descendants might override this method to take decorations into
        account.
        """
        return 0

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

    def _horizontal_offset_(self):
        """
        Return the horizontal offset of a child widget from the top left
        corner.  Descendants might override this method to take decorations
        into account.
        """
        return 0

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

    def _get_upper_left_(self):
        """
        Return the upper left corner of this widget within it's parent.  This
        provides a mapping between the logical coordinate 0/0 within the widget
        and the outside world.

        @returns: tuple (x, y)
        """
        return (self.left, self.top)

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

    def _add_child_(self, child, index, last_x, last_y):
        """
        Add a child widget to the managed box
        """

        cwidth, cheight = self._sizes_[index]
        max_label= max([i[2] for i in self._hints_.values()])

        if self.vertical:
            add = max_label != 0 and (max_label + 1) or 0
            child.left = self.left + self._horizontal_offset_() + add
            child.top = last_y + self._vertical_offset_()

            result = (0, cheight)
        else:
            add = max_label != 0 and 1 or 0
            child.top = self.top + self._vertical_offset_() + add
            child.left = last_x + self._horizontal_offset_()

            result = (cwidth + 1, 0)

        self._add_label_(child, index)

        return result

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

    def _add_label_(self, child, index):

        label = getattr(child._gfObject, 'label', None)
        if label and self._hints_[index][2]:
            attr = self._uiDriver.attr['background']

            if self.vertical:
                left = self.left + self._horizontal_offset_()
                top  = child.top
            else:
                left = child.left
                top  = self.top + self._vertical_offset_()

            self._parent.write(left, top, label, attr)


# =============================================================================
# Draw a box on a given parent
# =============================================================================

def box(parent, attr, left, top, right, bottom, label=None):
    """
    Draw a box with the given bounds on a parent (page) having an optional
    label.  The parent must implement a putchar method
    """

    for pos in range(left+1, right):
        parent.putchar(pos, top, curses.ACS_HLINE, attr)
        parent.putchar(pos, bottom, curses.ACS_HLINE, attr)

        for line in range(top+1, bottom):
            parent.putchar(left, line, curses.ACS_VLINE, attr)
            parent.putchar(right, line, curses.ACS_VLINE, attr)

        parent.putchar(left, top, curses.ACS_ULCORNER, attr)
        parent.putchar(right, top, curses.ACS_URCORNER, attr)
        parent.putchar(left, bottom, curses.ACS_LLCORNER, attr)
        parent.putchar(right, bottom, curses.ACS_LRCORNER, attr)

        if label is not None:
            parent.write(left+2, top, label, attr)
