# L-30 MCS 260 Mon 28 Mar 2016 : sliding_puzzle.py
"""
To introduce GUIs we look at a simple sliding puzzle.
The GUI contains several elements:
(1) buttons to start, scramble, unscramble, and clear
(2) a canvas widget to display the sliding puzzle
(3) a list of lists stores the puzzle data
(4) the scramble/unscramble buttons are animations
(5) mouse events allow user to solve the puzzle
"""
from tkinter import Tk, Canvas, Label, Button, W, E
from tkinter import StringVar
from random import randint

class SlidingPuzzle(object):
    """
    The start button of the GUI displays a board of
    sorted rectangles.  The scramble button perturbs
    the sorted configuration.  Pushing unscramble
    shows how the computer solves the puzzle.
    The user can move rectangles by clicking on
    a rectangle that can slide to the free position.
    """
    def draw_bounding_box(self):
        """
        Draws a bounding box on canvas.
        """
        (xp0, yp0) = (self.mag, self.mag)
        xp1 = self.mag*self.rows+self.mag
        yp1 = self.mag*self.cols+self.mag
        self.cnv.create_line(yp0, xp0, yp0, xp1, width=1)
        self.cnv.create_line(yp0, xp0, yp1, xp0, width=1)
        self.cnv.create_line(yp1, xp0, yp1, xp1, width=1)
        self.cnv.create_line(yp0, xp1, yp1, xp1, width=1)

    def bind_mouse_events(self):
        """
        Binds the mouse events to the canvas.
        """
        self.cnv.bind("<Button-1>", self.button_pressed)
        self.cnv.bind("<ButtonRelease-1>", self.button_released)
        self.cnv.bind("<Enter>", self.entered_window)
        self.cnv.bind("<Leave>", self.exited_window)
        self.cnv.bind("<B1-Motion>", self.mouse_dragged)

    def __init__(self, wdw, r, c):
        """
        The mouse is bound to the canvas.
        There are r rows of blocks and c columns of blocks.
        A label displays the mouse position.
        """
        wdw.title("a sliding puzzle")
        self.mag = 100  # magnification factor
        self.rows = r   # number of rows on canvas
        self.cols = c   # number of columns on canvas
        self.moves = [] # list of scrambling moves
        self.cnv = Canvas(wdw,\
            width=self.mag*self.cols+2*self.mag, \
            height=self.mag*self.rows+2*self.mag, \
            bg='white')
        self.cnv.grid(row=1, column=0, columnspan=4)
        # to display mouse position :
        self.mouse_position = StringVar()
        self.mouse_position.set("put mouse inside box to move it")
        self.position_label = Label(wdw, \
            textvariable=self.mouse_position)
        self.position_label.grid(row=2, column=0, columnspan=3)
        # draw bounding box and bind mouse events
        self.draw_bounding_box()
        self.bind_mouse_events()
        self.filled = []
        for _ in range(r):
            self.filled.append([0 for _ in range(c)])
        # three buttons
        self.bt1 = Button(wdw, text='start', command=self.start_fill)
        self.bt1.grid(row=0, column=0, sticky=W+E)
        self.bt2 = Button(wdw, text='scramble', command=self.scramble)
        self.bt2.grid(row=0, column=1, sticky=W+E)
        self.bt3 = Button(wdw, text='unscramble', command=self.unscramble)
        self.bt3.grid(row=0, column=2, sticky=W+E)
        self.bt4 = Button(wdw, text='clear', command=self.clear)
        self.bt4.grid(row=0, column=3, sticky=W+E)

    def movable(self, i, j):
        """
        Returns a tuple (a,b,c) where a is a boolean,
        indicating whether the rectangle at (i,j) can be moved.
        If a is True, then (b,c) are the new coordinates
        for the rectangle.
        """
        if i < self.rows-1:
            if self.filled[i+1][j] == 0:
                return (True, i+1, j)
        if i > 0:
            if self.filled[i-1][j] == 0:
                return (True, i-1, j)
        if j < self.cols-1:
            if self.filled[i][j+1] == 0:
                return (True, i, j+1)
        if j > 0:
            if self.filled[i][j-1] == 0:
                return (True, i, j-1)
        return (False, i, j)

    def make_move(self, i, j):
        """
        Verifies first if the (i,j)-th rectangle can move
        and if so, then it moves the (i,j)-th rectangle.
        """
        (a, b, c) = self.movable(i, j)
        if not a:
            self.mouse_position.set("nowhere to move")
        else:
            self.mouse_position.set('move to ' + str(b) + ',' + str(c))
            nbr = self.filled[i][j]
            self.filled[i][j] = 0
            name = '('+str(i)+','+str(j)+')'
            self.cnv.delete(name)
            lbl = 't' + name
            self.cnv.delete(lbl)
            (yp0, xp0) = ((c+1)*self.mag, (b+1)*self.mag)
            (yp1, xp1) = (yp0 + self.mag, xp0 + self.mag)
            name = '('+str(b)+','+str(c)+')'
            self.cnv.create_rectangle(yp0, xp0, yp1, xp1, \
                fill="green", tags=name)
            self.filled[b][c] = nbr
            lbl = 't' + name
            (yp1, xp1) = (yp0 + self.mag//2, xp0 + self.mag//2)
            txt = str(nbr)
            self.cnv.create_text(yp1, xp1, text=txt, tags=lbl)

    def draw_rectangle(self, xpt, ypt):
        """
        Draws a green rectangle on canvas,
        with coordinates given at (xpt, ypt) by the mouse.
        """
        xp0 = ypt - ypt % self.mag
        yp0 = xpt - xpt % self.mag
        i = xp0//self.mag - 1
        j = yp0//self.mag - 1
        if self.filled[i][j] == 0:
            self.mouse_position.set("cannot move empty box")
        else:
            self.make_move(i, j)

    def movable_spot(self):
        """
        Returns (i, j) of a rectangle that can be moved
        and the (i, j) of the free spot.
        """
        for i in range(len(self.filled)):
            for j in range(len(self.filled[i])):
                if self.filled[i][j] == 0:
                    while True:
                        rnd = randint(0, 3)
                        if rnd == 0:
                            if i > 0:
                                return (i-1, j, i, j)
                        if rnd == 1:
                            if j > 0:
                                return (i, j-1, i, j)
                        if rnd == 2:
                            if i < self.rows-1:
                                return (i+1, j, i, j)
                        if rnd == 3:
                            if j < self.cols-1:
                                return (i, j+1, i, j)

    def scramble(self):
        """
        Makes random moves and stores the moves.
        """
        for i in range(100):
            (i, j, row, col) = self.movable_spot()
            if self.filled[i][j] != 0:
                self.make_move(i, j)
                self.cnv.after(60)
                self.cnv.update()
                self.moves.insert(0, (row, col))

    def unscramble(self):
        """
        Uses the stored moves to unscramble.
        """
        while self.moves != []:
            (i, j) = self.moves.pop(0)
            self.make_move(i, j)
            self.cnv.after(60)
            self.cnv.update()

    def start_fill(self):
        """
        Fills the canvas with boxes placed in order.
        """
        self.moves = []
        mvc = self.rows*self.cols - 1
        k = 0
        for row in range(len(self.filled)):
            for col in range(len(self.filled[row])):
                name = '('+str(row)+','+str(col)+')'
                lbl = 't'+ name
                if k < mvc:
                    k = k + 1
                    (xp0, yp0) = ((row+1)*self.mag, (col+1)*self.mag)
                    (xp1, yp1) = (xp0 + self.mag, yp0 + self.mag)
                    self.cnv.create_rectangle(yp0, xp0, yp1, xp1, \
                        fill="green", tags=name)
                    (xp1, yp1) = (xp0 + self.mag/2, yp0 + self.mag/2)
                    txt = str(k)
                    self.cnv.create_text(yp1, xp1, text=txt, tags=lbl)
                    self.filled[row][col] = k
                else:
                    self.cnv.delete(name)
                    self.cnv.delete(lbl)
                    self.filled[row][col] = 0

    def clear(self):
        """
        Clears the canvas, emptying all boxes.
        """
        self.moves = []
        for i in range(0, len(self.filled)):
            for j in range(0, len(self.filled[i])):
                name = '('+str(i)+','+str(j)+')'
                self.cnv.delete(name)
                lbl = 't' + name
                self.cnv.delete(lbl)
                self.filled[i][j] = 0

    def button_pressed(self, event):
        """
        Displays the coordinates of button pressed.
        """
        self.mouse_position.set("currently at [ " + \
            str(event.x) + ", " + str(event.y) + " ]" +\
            " release to play")

    def button_released(self, event):
        """
        Display the coordinates of button release.
        """
        self.mouse_position.set("released at [ " + \
            str(event.x) + ", " + str(event.y) + " ]" + \
            " redo to clear")
        self.draw_rectangle(event.x, event.y)

    def entered_window(self, event):
        """
        Display the message that mouse entered window.
        """
        self.mouse_position.set("press mouse to grab a number")

    def exited_window(self, event):
        """
        Display the message that mouse exited window.
        """
        self.mouse_position.set("put mouse inside box to play")

    def mouse_dragged(self, event):
        """
        Displays coordinates of a moving mouse.
        """
        self.mouse_position.set("dragging at [ " + \
            str(event.x) + ", " + str(event.y) + " ]" + \
            " release to draw")

def main():
    """
    Prompts the user for the dimensions
    and launches the event loop.
    """
    top = Tk()
    rows = int(input('Give #rows : '))
    cols = int(input('Give #columns : '))
    SlidingPuzzle(top, rows, cols)
    top.mainloop()

if __name__ == "__main__":
    main()
