# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2007 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public License
## as published by the Free Software Foundation; either version 2
## of the License, or (at your option) any later version.
##
## This program 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 Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##

# TODO:
#  Focus could be improved, arrows shouldn't focus other widgets
#  Column/List field

"""Widget containing a Grid of fields
"""

import pickle

import gobject
import pango
import gtk
from gtk import gdk
from gtk import keysyms

from kiwi.python import clamp
from kiwi.utils import gsignal

(FIELD_NONE,
 FIELD_MOVE,
 FIELD_RESIZE) = range(3)

# Bindings
(FIELD_MOVEMENT_HORIZONTAL,
 FIELD_MOVEMENT_VERTICAL,
 FIELD_DELETION) = range(3)

#_CURSOR_LEFT_SIDE = gdk.Cursor(gdk.LEFT_SIDE)
_CURSOR_RIGHT_SIDE = gdk.Cursor(gdk.RIGHT_SIDE)
#_CURSOR_TOP_SIDE = gdk.Cursor(gdk.TOP_SIDE)
_CURSOR_BOTTOM_SIDE = gdk.Cursor(gdk.BOTTOM_SIDE)
#_CURSOR_BOTTOM_LEFT = gdk.Cursor(gdk.BOTTOM_LEFT_CORNER)
_CURSOR_BOTTOM_RIGHT = gdk.Cursor(gdk.BOTTOM_RIGHT_CORNER)
#_CURSOR_TOP_LEFT = gdk.Cursor(gdk.TOP_LEFT_CORNER)
#_CURSOR_TOP_RIGHT = gdk.Cursor(gdk.TOP_RIGHT_CORNER)


class Range(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __contains__(self, x):
        return self.start <= x <= self.end


class FieldInfo(object):
    def __init__(self, grid, text, field, x, y, width=-1, height=1):
        if width == -1:
            width = len(text)
        self.grid = grid
        self.text = text
        self.widget = field
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def allocate(self, width, height):
        req_width, req_height = self.widget.size_request()
        self.widget.size_allocate(((self.x * width) - 1,
                                   (self.y * height) - 1,
                                   (self.width * width) + 2,
                                   (self.height * height) + 3))

    def find_at(self, x, y):
        wx, wy, ww, wh = self.widget.allocation
        return (x in Range(wx, wx + ww) and
                y in Range(wy, wy + wh))

    def show(self):
        self.widget.show()

    def get_cursor(self, x, y):
        a = self.widget.allocation
        cx = a.x + 1
        cy = a.y + 1
        cw = a.width
        ch = a.height
        intop = y in Range(cy - 1, cy + 1)
        inbottom = y in Range(cy + ch - 3, cy + ch)

        if x in Range(cx - 1, cx + 1):
            if intop:
                return #_CURSOR_TOP_LEFT
            elif inbottom:
                return #_CURSOR_BOTTOM_LEFT
            else:
                return #_CURSOR_LEFT_SIDE
        elif x in Range(cx + cw - 2, cx + cw + 1):
            if intop:
                return # _CURSOR_TOP_RIGHT
            elif inbottom:
                return _CURSOR_BOTTOM_RIGHT
            else:
                return _CURSOR_RIGHT_SIDE
        elif intop:
            return #_CURSOR_TOP_SIDE
        elif inbottom:
            return _CURSOR_BOTTOM_SIDE


class FieldGrid(gtk.Layout):
    """FieldGrid is a Grid like widget which you can add fields to

    Signals
    =======
      - B{field-added} (object):
        - Emitted when a field is added to the grid
      - B{field-removed} (object):
        - Emitted when a field is removed from the grid
      - B{selection-changed} (object):
        - Emitted when a field is selected or deselected by the user.
    """

    gsignal('selection-changed', object,
            flags=gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION)
    gsignal('field-added', object)
    gsignal('field-removed', object)

    def __init__(self, font, width, height):
        gtk.Layout.__init__(self)
        self.set_can_focus(True)
        self.drag_dest_set(
            gtk.DEST_DEFAULT_ALL,
            [('OBJECTLIST_ROW', 0, 10),
             ('text/uri-list', 0, 11),
             ('_NETSCAPE_URL', 0, 12)],
            gdk.ACTION_LINK | gdk.ACTION_COPY | gdk.ACTION_MOVE)

        self.font = pango.FontDescription(font)
        self.width = width
        self.height = height
        self._fields = []
        self._moving_field = None
        self._moving_start_x_pointer = 0
        self._moving_start_y_pointer = 0
        self._moving_start_x_position = 0
        self._moving_start_y_position = 0
        self._action_type = FIELD_NONE
        self._selected_field = None

        self._draw_grid = True
        TEXT = '1234567890ABCDEFTI'
        self._layout = self.create_pango_layout(TEXT)
        self._layout.set_font_description(self.font)
        self._field_width = (self._layout.get_pixel_size()[0] / len(TEXT)) + 2
        self._field_height = self._layout.get_pixel_size()[1] + 2

    #
    # Private API
    #

    def _pick_field(self, window_x, window_y):
        for field in self._fields:
            if field.find_at(window_x, window_y):
                return field
        return None

    def _remove_selected_field(self):
        field = self._selected_field
        if field:
            self._remove_field(field)

    def _remove_field(self, field):
        if field == self._selected_field:
            self.select_field(None)

        self._fields.remove(field)
        self.remove(field.widget)
        self.emit('field-removed', field)

    def _add_field(self, text, description, x, y, width=-1, height=1):
        label = gtk.Label()
        label.set_alignment(0, 0)
        label.set_padding(2, 4)
        if not description:
            description = text
        label.set_markup(
            '<span letter_spacing="3072">%s</span>' % (description, ))
        label.modify_font(self.font)
        field = FieldInfo(self, text, label, x, y, width, height)
        self._fields.append(field)
        self.emit('field-added', field)

        label.connect('size-allocate',
                      self._on_field__size_allocate, field)
        self.put(label, -1, -1)
        return field

    def _set_field_position(self, field, x, y):
        x = clamp(x, 0, self.width - field.width - 1)
        y = clamp(y, 0, self.height - field.height - 1)
        if field.x == x and field.y == y:
            return

        field.x, field.y = x, y

        if field.widget.props.visible:
            self.queue_resize()
        self.emit('selection-changed', field)

    def _resize_field(self, field, width, height):
        width = clamp(width, 1, self.width - field.x - 1)
        height = clamp(height, 1, self.height - field.y - 1)
        if field.width == width and field.height == height:
            return

        field.width, field.height = width, height

        if field.widget.props.visible:
            self.queue_resize()
        self.emit('selection-changed', field)

    def _get_field_from_widget(self, widget):
        for field in self._fields:
            if field.widget == widget:
                return field
        else:
            raise AssertionError

    def _begin_move_field(self, field, x, y, time):
        if self._moving_field != None:
            raise AssertionError("can't move two fields at once")

        mask = (gdk.BUTTON_RELEASE_MASK | gdk.BUTTON_RELEASE_MASK |
                gdk.POINTER_MOTION_MASK)
        grab = gdk.pointer_grab(self.window, False, mask, None, None,
                                long(time))
        if grab != gdk.GRAB_SUCCESS:
            raise AssertionError("grab failed")

        self._moving_field = field
        self._moving_start_x_pointer = x
        self._moving_start_y_pointer = y
        self._moving_start_x_position = field.x
        self._moving_start_y_position = field.y
        self._moving_start_width = field.width
        self._moving_start_height = field.height
        w, h = field.widget.get_size_request()
        self._moving_start_w, self._moving_start_h = w, h

    def _update_move_field(self, x, y):
        field = self._moving_field
        if not field:
            return

        if self._action_type == FIELD_MOVE:
            dx, dy = self._get_coords(
                x - self._moving_start_x_pointer,
                y - self._moving_start_y_pointer)
            self._set_field_position(field,
                                     self._moving_start_x_position + dx,
                                     self._moving_start_y_position + dy)
        elif self._action_type == FIELD_RESIZE:
            dx, dy = self._get_coords(
                x - self._moving_start_x_pointer,
                y - self._moving_start_y_pointer)
            self._resize_field(field,
                               self._moving_start_width + dx,
                               self._moving_start_height + dy)

    def _end_move_field(self, time):
        if not self._moving_field:
            return

        gdk.pointer_ungrab(long(time))
        self._moving_field = None

    def _get_coords(self, x, y):
        """Returns the grid coordinates given absolute coordinates
        @param x: absolute x
        @param y: absoluyte y
        @returns: (gridx, gridy)
        """
        return (int(float(x) / (self._field_width + 1)),
                int(float(y) / (self._field_height + 1)))

    def _move_field(self, movement_type, delta):
        field = self._selected_field
        if not field:
            return True

        x = field.x
        y = field.y
        if movement_type == FIELD_MOVEMENT_VERTICAL:
            y += delta
        elif movement_type == FIELD_MOVEMENT_HORIZONTAL:
            x += delta
        else:
            raise AssertionError

        self._set_field_position(field, x, y)

    def _on_field__size_allocate(self, label, event, field):
        field.allocate(self._field_width + 1, self._field_height + 1)

    #
    # GtkWidget
    #

    def do_realize(self):
        gtk.Layout.do_realize(self)
        # Use the same gdk.window (from gtk.Layout) to capture these events.
        self.window.set_events(self.get_events() |
                               gdk.BUTTON_PRESS_MASK |
                               gdk.BUTTON_RELEASE_MASK |
                               gdk.KEY_PRESS_MASK |
                               gdk.KEY_RELEASE_MASK |
                               gdk.ENTER_NOTIFY_MASK |
                               gdk.LEAVE_NOTIFY_MASK |
                               gdk.POINTER_MOTION_MASK)

        self.modify_bg(gtk.STATE_NORMAL, gdk.color_parse('white'))
        gc = gdk.GC(self.window,
                    line_style=gdk.LINE_ON_OFF_DASH,
                    line_width=2)
        gc.set_rgb_fg_color(gdk.color_parse('blue'))
        self._selection_gc = gc

        gc = gdk.GC(self.window)
        gc.set_rgb_fg_color(gdk.color_parse('grey80'))
        self._grid_gc = gc

        gc = gdk.GC(self.window)
        gc.set_rgb_fg_color(gdk.color_parse('black'))
        self._border_gc = gc

        gc = gdk.GC(self.window)
        gc.set_rgb_fg_color(gdk.color_parse('grey40'))
        self._field_border_gc = gc

    def do_size_request(self, req):
        border_width = 1
        req.width = self.width * (self._field_width + border_width) + border_width
        req.height = self.height * (self._field_height + border_width) + border_width

    def do_expose_event(self, event):
        window = event.window

        if not (self.flags() & gtk.REALIZED):
            return

        for c in self._fields:
            self.propagate_expose(c.widget, event)

        fw = self._field_width + 1
        fh = self._field_height + 1

        width = (self.width * fw) - 1
        height = (self.height * fh) - 1
        window.draw_rectangle(self._border_gc, False, 0, 0,
                              width + 1, height + 1)

        if self._draw_grid:
            grid_gc = self._grid_gc

            for x in range(self.width):
                window.draw_line(grid_gc,
                                 x * fw, 0,
                                 x * fw, height)

            for y in range(self.height):
                window.draw_line(grid_gc,
                                 0, y * fh,
                                 width, y * fh)

        fields = self._fields[:]
        if self._selected_field:
            gc = self._selection_gc
            field = self._selected_field
            cx, cy, cw, ch = field.widget.allocation
            window.draw_rectangle(gc, False,
                                  cx + 1, cy + 1, cw - 2, ch - 2)

            fields.remove(field)

        gc = self._field_border_gc
        for field in fields:
            cx, cy, cw, ch = field.widget.allocation
            window.draw_rectangle(gc, False,
                                  cx + 1, cy + 1, cw - 2, ch - 3)

    def do_button_press_event(self, event):
        x, y = int(event.x), int(event.y)

        field = self._pick_field(x, y)
        self.select_field(field)

        self.grab_focus()

        if not field:
            return

        if not self._moving_field:
            if field.get_cursor(x, y):
                self._action_type = FIELD_RESIZE
            else:
                self._action_type = FIELD_MOVE

            self._begin_move_field(field, x, y, event.time)

        return False

    def do_button_release_event(self, event):
        self._update_move_field(int(event.x), int(event.y))
        self._end_move_field(event.time)

        return False

    def do_motion_notify_event(self, event):
        if self._moving_field != None:
            self._update_move_field(int(event.x), int(event.y))
        else:
            field = self._pick_field(event.x, event.y)
            cursor = None
            if field:
                cursor = field.get_cursor(event.x, event.y)
            self.window.set_cursor(cursor)

    def do_key_press_event(self, event):
        if self._moving_field:
            return

        if event.keyval == keysyms.Up:
            self._move_field(FIELD_MOVEMENT_VERTICAL, -1)
        elif event.keyval == keysyms.Down:
            self._move_field(FIELD_MOVEMENT_VERTICAL, 1)
        elif event.keyval == keysyms.Left:
            self._move_field(FIELD_MOVEMENT_HORIZONTAL, -1)
        elif event.keyval == keysyms.Right:
            self._move_field(FIELD_MOVEMENT_HORIZONTAL, 1)
        elif event.keyval == keysyms.Delete:
            self._remove_selected_field()

        if gtk.Layout.do_key_press_event(self, event):
            return True

        return True

    def do_drag_drop(self, context, x, y, time):
        return True

    def do_drag_data_received(self, context, x, y, data, info, time):
        if data.type == 'OBJECTLIST_ROW':
            row = pickle.loads(data.data)
            x, y = self._get_coords(x, y)
            if self.objectlist_dnd_handler(row, x, y):
                context.finish(True, False, time)
        elif data.type == '_NETSCAPE_URL':
            d = data.data.split('\n')[1]
            d = d.replace('&', '&amp;')
            x, y = self._get_coords(x, y)
            field = self.add_field(d, x, y)
            field.show()
            self.select_field(field)
            context.finish(True, False, time)

        context.finish(False, False, time)

    def do_focus(self, direction):
        self.set_can_focus(False)
        res = gtk.Layout.do_focus(self, direction)
        self.set_can_focus(True)

        return res

    #
    # Public API
    #

    def add_field(self, text, description, x, y, width=-1, height=1):
        """Adds a new field to the grid

        @param text: text of the field
        @param description: description of the field
        @param x: x position of the field
        @param y: y position of the field
        """
        return self._add_field(text, description, x, y, width, height)

    def select_field(self, field):
        """Selects a field
        @param field: the field to select, must be FieldInfo or None
        """
        if field == self._selected_field:
            return
        self._selected_field = field
        self.queue_resize()
        self.grab_focus()
        self.emit('selection-changed', field)

    def get_selected_field(self):
        """ Returns the currently selected field
        @returns: the currently selected field
        @rtype: FieldInfo
        """
        return self._selected_field

    def get_fields(self):
        """ Returns a list of fields in the grid
        @returns: a list of fields in the grid
        """
        return self._fields

    def objectlist_dnd_handler(self, item, x, y):
        """A subclass can implement this to support dnd from
        an ObjectList.
        @param item: the row dragged from the objectlist
        @param x: the x position it was dragged to
        @param y: the y position it was dragged to
        """
        return False

    def resize(self, width, height):
        """
        Resize the grid.
        @param width: the new width
        @param height: the new height
        """
        self.width = width
        self.height = height
        self.queue_resize()

gobject.type_register(FieldGrid)
