# Copyright 2007 World Wide Workshop Foundation
#
# This program 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 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# If you find this activity useful or end up using parts of it in one of your
# own creations we would love to hear from you at info@WorldWideWorkshop.org !
#

import pygtk
pygtk.require('2.0')
import gtk, gobject, pango, cairo
import random
import os
import logging
import md5
from cStringIO import StringIO
from mmm_modules import BorderFrame, utils

MAGNET_POWER_PERCENT = 20
CUTTERS = {}

def create_pixmap (w, h):
    cm = gtk.gdk.colormap_get_system()
    pm = gtk.gdk.Pixmap(None, w, h, cm.get_visual().depth)
    gc = pm.new_gc()
    gc.set_colormap(gtk.gdk.colormap_get_system())
    color = cm.alloc_color('white')
    gc.set_foreground(color)
    pm.draw_rectangle(gc, True, 0, 0, w, h)
    return pm

class JigsawPiece (gtk.EventBox):
    __gsignals__ = {'picked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
                    'moved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, int)),
                    'dropped' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),}
    
    def __init__ (self):
        super(JigsawPiece, self).__init__()
        self.index = None
        self.press_coords = (0,0)
        self.root_coords = (0,0)
        self.last_coords = (0,0)
        self.shape = None
        self.image = gtk.Image()
        self.pb_wf = gtk.Image()
        self.placed = False
        self._prepare_ui()
        self._prepare_event_callbacks()

    def _prepare_ui (self):
        self._c = gtk.Fixed()
        self._c.show()
        self.add(self._c)
        self._c.put(self.image, 0, 0)
        self.image.show()
        self._c.put(self.pb_wf, 0, 0)

    def _prepare_event_callbacks (self):
        self.l_evids = []
        self.l_evids.append(self.connect('button-press-event', self._press_cb))
        self.l_evids.append(self.connect('button-release-event', self._release_cb))
        self.l_evids.append(self.connect('motion-notify-event', self._motion_cb))
        self.connect('expose-event', self._expose_cb)

    def set_index (self, index):
        self.index = index

    def get_index (self):
        return self.index
    
    def set_from_pixbuf(self, pb, pb_wf=None, mask=None):
        self.image.set_from_pixbuf(pb)
        self.width = pb.get_width()
        self.height = pb.get_height()
        self.shape = mask
        if pb_wf is not None:
            self.pb_wf.set_from_pixbuf(pb_wf)
            self.pb_wf.show()
            self.image.hide()
        self.set_size_request(self.width, self.height)

    def get_width (self):
        return self.width

    def get_height (self):
        return self.height

    def hide_wireframe (self):
        self.pb_wf.hide()
        self.image.show()

    def get_position (self):
        # The position relative to the puzzle playing area
        if self.parent and self.parent.window:
            bx,by = self.parent.window.get_origin()
            px,py = self.window.get_origin()
            self.last_coords = (px-bx,py-by)
        return self.last_coords

    def set_position (self, x, y):
        # The new position, relative to the piece parent
        self.parent.move(self, x, y)

    def bring_to_top (self):
        p = self.get_parent()
        self.unparent()
        self.set_parent(p)

    def _press_cb (self, w, e, *attrs):
        self.press_coords = e.get_coords()
        self.root_coords = w.window.get_origin()
        self.emit('picked')
        
    def _motion_cb (self, w, e, *args):
        nx, ny = w.root_coords
        rx, ry = e.get_root_coords()
        px, py = w.press_coords
        delta = (rx-nx-px, ry-ny-py)
        w.root_coords = (nx+delta[0], ny+delta[1])
        self.emit('moved', *delta)

    def _release_cb (self, w, e, *args):
        self.emit('dropped')
        # The actual position in the whole window is w.window.get_origin()

    def _expose_cb (self, *args):
        if self.shape is not None:
            self.window.shape_combine_mask(self.shape, 0, 0)


class CutterBasic (object):
    """ Cutters are used to create the connectors between pieces.
    This one uses no connector at all, pieces will be simple rectangles. """
    SIDE_LEFT = 1
    SIDE_RIGHT = 2
    SIDE_TOP = 3
    SIDE_BOTTOM = 4

    connector_percent = 10

    def draw_connector (self, cairo_ctx, size, side, point_out=True, type=0):
        """
        cairo_ctx is the Cairo context used to draw, already at the start position for the connector.
        size is the length of the connector along the piece side. It's assumed to be 'connector_percent' of the side length.
        side describes the side being drawn.
        point_out is true if the connector is to be drawn onto the outside of the piece.
        type is a placeholder for supporting multiple connector subtypes.
        returns the extrusion size.
        """
        if side in (self.SIDE_RIGHT, self.SIDE_TOP):
            op = lambda x: -x
        else:
            op = lambda x: x

        if side in (self.SIDE_LEFT, self.SIDE_RIGHT):
            cairo_ctx.rel_line_to(0,op(size))
        else:
            cairo_ctx.rel_line_to(op(size),0)
        return 0
CUTTERS['basic'] = CutterBasic

class CutterSimple (CutterBasic):
    connector_percent = 20

    def draw_connector (self, cairo_ctx, size, side, point_out=True, type=0):
        if (side in (self.SIDE_RIGHT, self.SIDE_BOTTOM)) ^ point_out:
            op1 = lambda x: -x
        else:
            op1 = lambda x: x
        if side in (self.SIDE_RIGHT, self.SIDE_TOP):
            op2 = lambda x: -x
        else:
            op2 = lambda x: x
            
        if side in (self.SIDE_LEFT, self.SIDE_RIGHT):
            cairo_ctx.rel_line_to(op1(size),0)
            cairo_ctx.rel_line_to(0,op2(size))
            cairo_ctx.rel_line_to(-op1(size),0)
        else:
            cairo_ctx.rel_line_to(0,op1(size))
            cairo_ctx.rel_line_to(op2(size),0)
            cairo_ctx.rel_line_to(0,-op1(size))
        return size
CUTTERS['simple'] = CutterSimple

class CutterClassic (CutterBasic):
    connector_percent = 20

    def draw_connector (self, cairo_ctx, size, side, point_out=True, type=0):
        if (side in (self.SIDE_RIGHT, self.SIDE_BOTTOM)) ^ point_out:
            op1 = lambda x: -x
        else:
            op1 = lambda x: x
        if side in (self.SIDE_RIGHT, self.SIDE_TOP):
            op2 = lambda x: -x
        else:
            op2 = lambda x: x
            
        if side in (self.SIDE_LEFT, self.SIDE_RIGHT):
            cairo_ctx.rel_curve_to(op1(size*2), -op2(size*1.5), op1(size*2), op2(size*2.5), 0, op2(size))
        else:
            cairo_ctx.rel_curve_to(-op2(size*1.5), op1(size*2), op2(size*2.5), op1(size*2), op2(size), 0)
        return size*2
CUTTERS['classic'] = CutterClassic

class CutBoard (object):
    def __init__ (self, *args, **kwargs):
        if len(args) or len(kwargs):
            self._prepare(*args, **kwargs)
        self.cutter = CutterClassic()
        self.pb = None

    def _prepare (self, cols, rows, cutter=None, hch=None, vch=None):
        if self.pb is None:
            logging.error("You must fist set CutBoard.pb with a pixbuf to be used!")
            return
        if cutter is not None:
            self.cutter = CUTTERS.get(cutter, CutterClassic)()
        self.rows, self.cols = rows, cols
        if hch is not None:
            self.h_connector_hints = hch
        else:
            self.h_connector_hints = [random.random()*2-1 for x in range((self.rows+1)*(self.cols+1))]
        if vch is not None:
            self.v_connector_hints = vch
        else:
            self.v_connector_hints = [random.random()*2-1 for x in range((self.rows+1)*(self.cols+1))]
        self.width, self.height = self.pb.get_width(), self.pb.get_height()
        self.pm = create_pixmap(self.width, self.height)
        self.cr = self.pm.cairo_create()
        self.pieces = []
        self.prepare_hint()
        for c in range(self.cols):
            self.pieces.append([])
            for r in range(self.rows):
                self.pieces[c].append(self.cut(c,r))

    def get_cutter (self):
        for k,v in CUTTERS.items():
            if isinstance(self.cutter, v):
                return k
        return None

    def set_cutter (self, cutter):
        if cutter is not None:
            self.cutter = CUTTERS.get(cutter, CutterClassic)()

    def prepare_hint (self):
        self.hint_pm = create_pixmap(self.width, self.height)
        self.hint_cr = self.hint_pm.cairo_create()
        self.hint_cr.set_source_rgb (0,0,0)
        self.hint_cr.set_line_width(0.5)

    def refresh (self):
        self.cr.set_source_pixbuf(self.pb, 0, 0)
        self.cr.paint()
        self.cr.set_line_width(1.0)
        self.cr.set_source_rgb(0,0,0)

    def get_hint (self):
        pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.width, self.height)
        pb.get_from_drawable(self.hint_pm, self.pm.get_colormap(), 0, 0, 0, 0, self.width, self.height)
        return pb

    def draw_vertical_path (self, cairo_ctx, piece_nr, col, height, ptype=0):
        """ returns the number of overlapping pixels on the drawn side """
        if col > piece_nr:
            # right_side
            op = lambda x: -x
        else:
            # left_side
            op = lambda x: x
        if col > 0 and col < self.cols:
            point_out = col > piece_nr
            t1 = height*self.cutter.connector_percent/100.0
            t2 = t1/2.0
            if ptype < 0:
                point_out = not point_out
            cairo_ctx.rel_line_to(0, op((height/2.0) - t2))
            t = self.cutter.draw_connector(cairo_ctx, t1, col>piece_nr and self.cutter.SIDE_RIGHT or self.cutter.SIDE_LEFT, point_out)
            cairo_ctx.rel_line_to(0, op((height/2.0) - t2))
            if point_out:
                overlap = t
            else:
                overlap = 0
        else:
            cairo_ctx.rel_line_to(0, op(height))
            overlap = 0
        return overlap

    def draw_horizontal_path (self, cairo_ctx, piece_nr, row, width, ptype=0):
        """ returns the number of overlapping pixels on the drawn side """
        # positive or negative connector? We must randomize this.
        if row > piece_nr:
            # bottom_side
            op = lambda x: x
        else:
            # top_side
            op = lambda x: -x
        if row > 0 and row < self.rows:
            point_out = row > piece_nr
            t1 = width*self.cutter.connector_percent/100.0
            t2 = t1/2.0
            if ptype < 0:
                point_out = not point_out
            cairo_ctx.rel_line_to(op((width/2.0) - t2), 0)
            t = self.cutter.draw_connector(cairo_ctx, t1, row>piece_nr and self.cutter.SIDE_BOTTOM or self.cutter.SIDE_TOP, point_out)
            cairo_ctx.rel_line_to(op((width/2.0) - t2), 0)
            if point_out:
                overlap = t
            else:
                overlap = 0
        else:
            cairo_ctx.rel_line_to(op(width),0)
            overlap = 0
        return overlap

    def path_for_piece (self, cairo_context, x, y, width, height):
        hpos = (y*self.cols)+x
        vpos = (x*self.rows)+y
        l_offset = self.draw_vertical_path(cairo_context, x, x, height, self.h_connector_hints[hpos])
        b_offset = self.draw_horizontal_path(cairo_context, y, y+1, width, self.v_connector_hints[vpos+1])
        r_offset = self.draw_vertical_path(cairo_context, x, x+1, height, self.h_connector_hints[hpos+1])
        t_offset = self.draw_horizontal_path(cairo_context, y, y, width, self.v_connector_hints[vpos])
        return {'left':l_offset, 'right': r_offset, 'top': t_offset, 'bottom': b_offset}

    def cut (self, x, y):
        width = self.width / self.cols
        height = self.height / self.rows
        px = width*x
        py = height*y
        if x == self.cols - 1:
            width = self.width - px
        if y == self.rows - 1:
            height = self.height - py

        # the cut mask is done on each piece at the right and bottom sides,
        # except for the right on the last column and bottom on the last row.

        # Draw outline on the board hint image
        self.hint_cr.move_to(px,py)
        offsets = self.path_for_piece(self.hint_cr, x, y, width, height)
        self.hint_cr.stroke()
        width_offset = int(offsets['left'] + offsets['right'])
        height_offset = int(offsets['top'] + offsets['bottom'])
        width = int(width)
        height = int(height)
        # Prepare the piece mask
        mask = gtk.gdk.Pixmap(None, width+width_offset, height+height_offset, 1)
        mask_cr = mask.cairo_create()
        mask_cr.save()
        mask_cr.set_operator(cairo.OPERATOR_SOURCE)
        mask_cr.set_source_rgba(0,0,0,0)
        mask_cr.paint()
        mask_cr.restore()
        mask_cr.set_line_width(1.0)
        mask_cr.set_source_rgba(1,1,1,1)
        mask_cr.move_to(offsets['left'], offsets['top'])
        self.path_for_piece(mask_cr, x, y, width, height)
        mask_cr.stroke_preserve()
        mask_cr.fill()

        width += width_offset
        height += height_offset
        px -= int(offsets['left'])
        py -= int(offsets['top'])

        # The piece image
        self.refresh()
        pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
        pb.get_from_drawable(self.pm, self.pm.get_colormap(), px, py, 0, 0, width, height)

        # The outlined image
        self.cr.move_to(px+offsets['left'], py+offsets['top'])
        self.path_for_piece(self.cr, x, y, width-width_offset, height-height_offset)
        self.cr.stroke()
        pb_wf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
        pb_wf.get_from_drawable(self.pm, self.pm.get_colormap(), px, py, 0, 0, width, height)

        return (pb, pb_wf, mask, px, py, width-width_offset, height-height_offset)

    def get_image_as_png (self, cb=None):
        rv = None
        if cb is None:
            rv = StringIO()
            cb = rv.write
        self.pb.save_to_callback(cb, "png")
        if rv is not None:
            return rv.getvalue()
        else:
            return True

    def _freeze (self, img_cksum_only=False):
        if self.pb is not None:
            if img_cksum_only:
                cksum = md5.new()
                self.get_image_as_png(cksum.update)
                return {'geom': (self.cols, self.rows),
                        'hints': (self.h_connector_hints, self.v_connector_hints),
                        'pb-cksum': cksum.hexdigest(),
                        'cutter': self.get_cutter(),
                        }
            else:
                return {'geom': (self.cols, self.rows),
                        'hints': (self.h_connector_hints, self.v_connector_hints),
                        'pb': self.get_image_as_png(),
                        'cutter': self.get_cutter(),
                        }
        return None

    def _thaw (self, data):
        if data is None:
            return
        if data.has_key('pb') and data['pb'] is not None:
            fn = os.tempnam() 
            f = file(fn, 'w+b')
            f.write(data['pb'])
            f.close()
            i = gtk.Image()
            i.set_from_file(fn)
            os.remove(fn)
            self.pb = i.get_pixbuf()
            del data['pb']
        logging.debug("cutboard._thaw(%s)" % str(data))
        cols, rows = data['geom']
        hch, vch = data['hints']
        cutter = data['cutter']
        self._prepare(cols, rows, cutter, hch, vch)


class JigsawBoard (BorderFrame):
    """ Drop area for jigsaw pieces to be tested against.
    Maybe use this to do the piece cutting / hint ? """
    __gsignals__ = {'solved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
                    }
    def __init__ (self):
        super(JigsawBoard, self).__init__(border_color="#0000FF")
        #self.image = None
        self.board = gtk.Fixed()
        self.board.show()
        self.add(self.board)
        self.board_distribution = None
        self.target_pieces_per_line = 3
        self.hint_board_image = gtk.Image()
        self.cutboard = CutBoard()

    def get_cutter (self):
        return self.cutboard.get_cutter()

    def set_cutter (self, cutter):
        self.cutboard.set_cutter(cutter)
        
    def update_hint (self):
        self.hint_board_image.set_from_pixmap(self.hint_board, None)

    def set_image (self, pixbuf):
        self.board.foreach(self.board.remove)
        self.board_distribution = None
        self.board.put(self.hint_board_image, 0,0)
        self.img_width = pixbuf.get_width()
        self.img_height = pixbuf.get_height()
        self.set_size_request(self.img_width, self.img_height)
        self.queue_resize()
        self.cutboard.pb = pixbuf

    #def reshuffle (self):
    #    self.cutboard._prepare(self.target_pieces_per_line,self.target_pieces_per_line)#, self.cutter)

    def get_pieces (self, reshuffle=True):
        if self.cutboard.pb is None:
            return
        self.board_distribution = []
        pcw = self.target_pieces_per_line
        pch = self.target_pieces_per_line
        # Find the best cut for our difficulty level
        changed = True
        while changed:
            pw = self.img_width / pcw
            ph = self.img_height / pch
            changed = False
            if pcw == 1 or pch == 1:
                break
            if abs((self.img_width / (pcw-1))-ph) < abs(pw-ph):
                pcw -= 1
                changed = True
                continue
            if abs((self.img_height / (pch-1))-pw) < abs(ph-pw):
                pch -= 1
                changed = True

        logging.debug("Board matrix %s %s" % (pcw, pch))
        if reshuffle:
            self.cutboard._prepare(pcw, pch)
        # Prepare the pieces
        self.hint_board_image.set_from_pixbuf(self.cutboard.get_hint())

        pos_x = 0
        for col in range(pcw):
            pos_y = 0
            for row in range(pch):
                piece = JigsawPiece()
                pb, pb_wf, mask, px, py, pw, ph = self.cutboard.pieces[col][row]
                piece.set_from_pixbuf(pb, pb_wf, mask)
                piece.show()
                piece.set_index(len(self.board_distribution))
                self.board_distribution.append((px, py, pw*MAGNET_POWER_PERCENT/100.0, ph*MAGNET_POWER_PERCENT/100.0))
                yield piece

    def get_placed_pieces (self):
        return [x for x in self.board.get_children() if isinstance(x, JigsawPiece)]

    def place_piece (self, piece):
        piece.placed = True
        index = piece.get_index()
        piece.reparent(self.board)
        #piece.hide_wireframe()
        bx, by, mx, my = self.board_distribution[index]
        self.board.move(piece, bx, by)
        self.board_distribution[index] = None
        if len(filter(None, self.board_distribution))==0:
            for p in self.board.get_children():
                if isinstance(p, JigsawPiece):
                    p.hide_wireframe()
            self.emit('solved')

    def drop_piece (self, piece, x, y):
        index = piece.get_index()
        bx, by, mx, my = self.board_distribution[index]
        x -= self.padding[0]
        y -= self.padding[1]
        logging.debug("Board drop for piece #%i (%i,%i) : (%i,%i)" % (index, x,y,bx,by))
        if abs(bx-x) < mx and abs(by-y) < my:
            # We have a positive positioning
            self.place_piece(piece)

    def _freeze (self, img_cksum_only=False):
        return {'target_pieces_per_line': self.target_pieces_per_line,
                'cutboard': self.cutboard and self.cutboard._freeze(img_cksum_only) or None,
                }

    def _thaw (self, data):
        for k in ('target_pieces_per_line', ):
            if data.has_key(k):
                setattr(self, k, data[k])
        self.cutboard._thaw(data['cutboard'])
            

class JigsawPuzzleWidget (gtk.EventBox):
    __gsignals__ = {
        'picked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (JigsawPiece,)),
        'dropped' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (JigsawPiece,bool)),
        'solved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'cutter-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str, int)),
        }
    def __init__ (self):
        super(JigsawPuzzleWidget, self).__init__()
        self._container = gtk.Fixed()
        self.add(self._container)
        self.board = JigsawBoard()
        self.board.connect('solved', self._solved_cb)
        self.board.show()
        self._container.put(self.board, 10, 10)
        self._container.show_all()
        self.running = False
        self.forced_location = False

    def bring_to_top (self, piece):
        wx,wy = self._container.child_get(piece, 'x', 'y')
        self._container.remove(piece)
        self._container.put(piece, wx, wy)

    def show_hint (self, show):
        if show:
            self.board.hint_board_image.show()
        else:
            self.board.hint_board_image.hide()

    def get_floating_pieces (self):
        return [x for x in self._container.get_children() if isinstance(x, JigsawPiece)]

    def set_cutter (self, cutter):
        if cutter is None:
            cutter = 'classic'
            logging.debug('set_cutter(None) setting default to "classic"')
        self.board.set_cutter(cutter)
        self.emit('cutter-changed', self.get_cutter(), self.board.target_pieces_per_line)

    def get_cutter (self):
        return self.board.get_cutter()

    def set_target_pieces_per_line (self, tppl):
        if tppl is None:
            tppl = 3
        self.board.target_pieces_per_line = tppl
        self.emit('cutter-changed', self.get_cutter(), self.board.target_pieces_per_line)

    def get_target_pieces_per_line (self):
        return self.board.target_pieces_per_line

    def prepare_image (self, pixbuf=None, reshuffle=True):
        x,y,w,h = self.get_allocation()
        if pixbuf is not None:
            factor = min((float(w)*0.6)/pixbuf.get_width(), (float(h)*0.6)/pixbuf.get_height())
            pixbuf = pixbuf.scale_simple(int(pixbuf.get_width() * factor),
                                         int(pixbuf.get_height()*factor),
                                         gtk.gdk.INTERP_BILINEAR)
        if pixbuf is None:
            pixbuf = self.board.cutboard.pb
        if pixbuf is None:
            return False
        self.board.set_image(pixbuf)

        for child in self._container.get_children():
            if child is not self.board:
                self._container.remove(child)
        bx, by = self._container.child_get(self.board, 'x', 'y')
        bw, bh = self.board.inner.get_size_request()
        br = gtk.gdk.Rectangle(bx,by,bw,bh)
        for n, piece in enumerate(self.board.get_pieces(reshuffle)):
            if self.forced_location and len(self.forced_location)>n:
                if self.forced_location[n] is None:
                    # Will be placed in the correct place later
                    self._container.put(piece, 0, 0)
                else:
                    self._container.put(piece, *self.forced_location[n])
            else:
                pw,ph = piece.get_size_request()
                r = gtk.gdk.Rectangle(bx,by,pw,ph)
                r.x = int(random.random()*(w-pw))
                r.y = int(random.random()*(h-ph))
                if br.intersect(r).width > 0:
                    r.x = int(random.random()*(w-pw))
                    r.y = int(random.random()*(h-ph))
                self._container.put(piece, r.x, r.y)
            piece.connect('picked', self._pick_cb)
            piece.connect('moved', self._move_cb)
            piece.connect('dropped', self._drop_cb)
            if self.forced_location and len(self.forced_location)>n and self.forced_location[n] is None:
                self.board.place_piece(piece)
            while gtk.events_pending():
                gtk.main_iteration(False)
            piece.get_position()
        self.forced_location = None
        self.running = True
        return True

    def is_running (self):
        return self.running

    def solve (self):
        for p in [x for x in self._container.get_children() if isinstance(x, JigsawPiece)]:
            self.board.place_piece(p)

    def _solved_cb (self, *args):
        self.emit('solved')

    def _pick_cb (self, w):
        self.emit('picked', w)

    def _move_cb (self, w, x, y, absolute=False):
        if w.get_parent() != self._container:
            return
        if absolute:
            wx,wy = 0,0
        else:
            wx,wy = self._container.child_get(w, 'x', 'y')

        wa = w.get_allocation()
        ca = self._container.get_allocation()

        if wx+x > 0 and wy+y > 0 and wx+wa[2]+x <= ca[2] \
                and wy+wa[3]+y <= ca[3]:
            #logging.debug("moving %i,%i : %i:%i : %i:%i" % (wx,wy, x, y,wx+x, wy+y))
            self._container.move(w, max(0,wx+x), max(0,wy+y))

    def _drop_cb (self, w, from_mesh=False):
        if w.get_parent() != self._container:
            return
        self.bring_to_top(w)
        x,y,a,b = w.get_allocation()
        c,d = w.get_size_request()
        logging.debug("Dropped Widget allocation: %s %s %s %s %s %s" % (x,y,a,b,c,d))
        if w.intersect(self.board.get_allocation()):
            wx,wy,ww,wh = w.get_allocation()
            bx,by,bw,bh = self.board.get_allocation()
            self.board.drop_piece(w, wx-bx, wy-by)
        self.emit('dropped', w, from_mesh)
        
    def _debug_cb (self, w, e, *args):
        logging.debug("%s %s %s" % (w, e, args))

    def _freeze (self, img_cksum_only=False):
        pieces = [(x.get_index(), None) for x in self.board.get_placed_pieces()]
        
        pieces.extend([(x.get_index(), x.get_position()) for x in self.get_floating_pieces()])
        pieces.sort(key=lambda x: x[0])
        return {'board': self.board._freeze(img_cksum_only),
                'cutter': self.get_cutter(),
                'target_pieces_per_line': self.get_target_pieces_per_line(),
                'piece_pos': [x[1] for x in pieces]}

    def _thaw (self, data):
        if data.has_key('board'):
            self.board._thaw(data['board'])
        self.set_cutter(data.get('cutter', None))
        self.set_target_pieces_per_line(data.get('target_pieces_per_line', None))
        self.forced_location = data.get('piece_pos', None)
        
if __name__ == '__main__':
    w = gtk.Window()
    j = JigsawPuzzleWidget()
    img = utils.load_image('test_image.gif')
    
    j.prepare_image(img)
    
    w.add(j)
    w.show_all()
    gtk.main()
