# L-36 MCS 507 Mon 13 Nov 2023 : rubik_magic_cube.py

"""
Tkinter model of Rubik's cube, based on GAP example:
http://www.gap-system.org/Doc/Examples/rubik.html.
"""

# DIM = (40, 12, 8)   # for tiny screens
# DIM = (50, 15, 9)   # for smallest screens
DIM = (60, 18, 12)  # for smaller screens
# DIM = (100, 20, 14)   # for large screens
MAGNIFICATION = DIM[0]         # number of pixels = 11 x 14 x MAGNIFICATION
CANVASFONT = ("Times", DIM[1]) # size of letters and numbers on canvas
ENTRYFONT = ("Times", DIM[2])  # size of numbers in the entry widget

from tkinter import Tk, Canvas, Button, Entry, INSERT, END, CENTER, W, E

class Cube(object):
    """
    The GUI exports a canvas to show all six sides
    of a cube as a box that is cut open.
    The start button displays the initial state.
    With the shuffle button an animation starts,
    which applies a random sequence of moves.
    The random shuffle is undone with the solve.
    To clear the canvas, press the clear button.
    With the six buttons under the canvas,
    the user can apply any of the six rotations.
    The state of the cube is displayed in the
    entry widget at the bottom row.
    """
    def draw_sides(self):
        """
        Draws the six sides of the cube on canvas.
        """
        # coordinates of all corners
        m = self.mag
        (x0, y0) = (m, m)
        (x1, y1) = (x0 + 3*m, y0 + 3*m)
        (x2, y2) = (x0 + 6*m, y0 + 6*m)
        (x3, y3) = (x0 + 9*m, y0 + 9*m)
        y4 = y0 + 12*m
        # horizontal lines
        self.c.create_line(y1, x0, y2, x0, width=1)
        self.c.create_line(y1, x3, y2, x3, width=1)
        self.c.create_line(y0, x1, y4, x1, width=1)
        self.c.create_line(y0, x2, y4, x2, width=1)
        # vertical lines
        self.c.create_line(y0, x1, y0, x2, width=1)
        self.c.create_line(y1, x0, y1, x3, width=1)
        self.c.create_line(y2, x0, y2, x3, width=1)
        self.c.create_line(y2, x0, y2, x3, width=1)
        self.c.create_line(y3, x1, y3, x2, width=1)
        self.c.create_line(y4, x1, y4, x2, width=1)

    def __init__(self, wdw, magfac, sleeptime):
        """
        Defines the layout of the six sides of the cube,
        with the given magnification factor in magfac,
        and the time it sleeps between the moves in the
        shuffle and the solve.
        """
        wdw.title("Rubik's magic cube")
        self.mag = magfac  # magnification factor
        self.topcolor = 'spring green'
        self.leftcolor = 'sky blue'
        self.frontcolor = 'tomato'
        self.rightcolor = 'yellow'
        self.rearcolor = 'pink'
        self.bottomcolor = 'orange'
        self.c = Canvas(wdw, width=14*self.mag, \
            height = 11*self.mag, bg='white')
        self.c.grid(row=1, column=0, columnspan=6)
        self.draw_sides()
        # buttons
        self.b1 = Button(wdw, text='start', command=self.start_fill)
        self.b1.grid(row=0, column=0, sticky=W+E)
        self.b2 = Button(wdw, text='shuffle', command=self.shuffle_tiles)
        self.b2.grid(row=0, column=1, columnspan=2, sticky=W+E)
        self.b3 = Button(wdw, text='solve', command=self.solve_puzzle)
        self.b3.grid(row=0, column=3, columnspan=2, sticky=W+E)
        self.b4 = Button(wdw, text='clear', command=self.clear)
        self.b4.grid(row=0, column=5, sticky=W+E)
        self.a1 = Button(wdw, text='turn top', \
            command=self.apply_first_action)
        self.a1.grid(row=2, column=0, sticky=W+E)
        self.a2 = Button(wdw, text='turn left', \
            command=self.apply_second_action)
        self.a2.grid(row=2, column=1, sticky=W+E)
        self.a3 = Button(wdw, text='turn front', \
            command=self.apply_third_action)
        self.a3.grid(row=2, column=2, sticky=W+E)
        self.a4 = Button(wdw, text='turn right', \
            command=self.apply_fourth_action)
        self.a4.grid(row=2, column=3, sticky=W+E)
        self.a5 = Button(wdw, text='turn rear', \
            command=self.apply_fifth_action)
        self.a5.grid(row=2, column=4, sticky=W+E)
        self.a6 = Button(wdw, text='turn bottom', \
            command=self.apply_sixth_action)
        self.a6.grid(row=2, column=5, sticky=W+E)
        # the permutation of the tiles
        self.numbers = list(range(1, 49))
        self.moves = []
        self.sleep = sleeptime
        # entry widget to display the state
        self.state = Entry(wdw, justify=CENTER, font=ENTRYFONT)
        self.state.grid(row=3, column=0, columnspan=6, sticky=W+E)
        self.state.insert(INSERT, str(tuple(self.numbers)))

    def show_state(self):
        """
        Updates the entry widget with the current state.
        """
        self.state.delete(0, END)
        self.state.insert(INSERT, str(tuple(self.numbers)))

    def tile_color(self, ind):
        """
        Define the color of a tile
        depending of the value of ind.
        """
        if(ind < 9):
            return self.topcolor
        elif(ind < 17):
            return self.leftcolor
        elif(ind < 25):
            return self.frontcolor
        elif(ind < 33):
            return self.rightcolor
        elif(ind < 41):
            return self.rearcolor
        else:
            return self.bottomcolor

    def fill_side(self, row, col, ind, color, side):
        """
        Fills a side, starting at (row, col), with count at ind.
        The color name is in color and the name of the side in side.
        """
        k = ind
        for i in range(row, row+3):
            for j in range(col, col+3):
                if((i == row+1) and (j == col+1)):
                    name = side
                    (x0, y0) = ((i+1)*self.mag, (j+1)*self.mag)
                    (x1, y1) = (x0 + self.mag, y0 + self.mag)
                    self.c.create_rectangle(y0, x0, y1, x1, \
                        fill=color, tags=name)
                    lbl = 't' + name
                    (x1, y1) = (x0 + self.mag/2, y0 + self.mag/2)
                    self.c.create_text(y1, x1, text=name, tags=lbl, \
                        font=CANVASFONT)
                else:
                    k = k + 1
                    name = str(self.numbers[k])
                    tag = 'r' + name
                    tilcol = self.tile_color(self.numbers[k])
                    (x0, y0) = ((i+1)*self.mag, (j+1)*self.mag)
                    (x1, y1) = (x0 + self.mag, y0 + self.mag)
                    self.c.create_rectangle(y0, x0, y1, x1, \
                         fill=tilcol, tags=tag)
                    lbl = 't' + name
                    (x1, y1) = (x0 + self.mag/2, y0 + self.mag/2)
                    self.c.create_text(y1, x1, text=name, tags=lbl, \
                        font=CANVASFONT)

    def start_fill(self):
        """
        Fills the canvas with boxes placed in order.
        """
        self.fill_side(0, 3, -1, self.topcolor, 'top')
        self.fill_side(3, 0, 7, self.leftcolor, 'left')
        self.fill_side(3, 3, 15, self.frontcolor, 'front')
        self.fill_side(3, 6, 23, self.rightcolor, 'right')
        self.fill_side(3, 9, 31, self.rearcolor, 'rear')
        self.fill_side(6, 3, 39, self.bottomcolor, 'bottom')

    def clear(self):
        """
        Clears the canvas, emptying all boxes,
        and resets the numbers.
        """
        self.numbers = list(range(1, 49))
        self.moves = []
        self.clear_tiles()

    def clear_tiles(self):
        """
        Clears the canvas, emptying all boxes.
        """
        self.c.delete('top', 'ttop')
        self.c.delete('left', 'tleft')
        self.c.delete('front', 'tfront')
        self.c.delete('right', 'tright')
        self.c.delete('rear', 'trear')
        self.c.delete('bottom', 'tbottom')
        for k in self.numbers:
            name = 't' + str(k)
            self.c.delete(name)
            tag = 'r' + str(k)
            self.c.delete(tag)

    def first_action(self):
        """
        Applies on the numbers the action, turning the top face,
        (1 3 8 6)(2 5 7 4)(9 33 25 17)(10 34 26 18)(11 35 27 19).
        """
        p = self.numbers
        (p[2], p[7], p[5], p[0]) = (p[0], p[2], p[7], p[5])
        (p[4], p[6], p[3], p[1]) = (p[1], p[4], p[6], p[3])
        (p[32], p[24], p[16], p[8]) = (p[8], p[32], p[24], p[16])
        (p[33], p[25], p[17], p[9]) = (p[9], p[33], p[25], p[17])
        (p[34], p[26], p[18], p[10]) = (p[10], p[34], p[26], p[18])

    def second_action(self):
        """
        Applies on the numbers the action, turning the left face,
        (9 11 16 14)(10 13 15 12)(1 17 41 40)(4 20 44 37)(6 22 46 35).
        """
        p = self.numbers
        (p[13], p[8], p[10], p[15]) = (p[8], p[10], p[15], p[13])
        (p[11], p[9], p[12], p[14]) = (p[9], p[12], p[14], p[11])
        (p[39], p[0], p[16], p[40]) = (p[0], p[16], p[40], p[39])
        (p[36], p[3], p[19], p[43]) = (p[3], p[19], p[43], p[36])
        (p[34], p[5], p[21], p[45]) = (p[5], p[21], p[45], p[34])

    def third_action(self):
        """
        Applies on the numbers the action, turning the front face,
        (17 19 24 22)(18 21 23 20)(6 25 43 16)(7 28 42 13)(8 30 41 11).
        """
        p = self.numbers
        (p[21], p[16], p[18], p[23]) = (p[16], p[18], p[23], p[21])
        (p[19], p[17], p[20], p[22]) = (p[17], p[20], p[22], p[19])
        (p[15], p[5], p[24], p[42]) = (p[5], p[24], p[42], p[15])
        (p[12], p[6], p[27], p[41]) = (p[6], p[27], p[41], p[12])
        (p[10], p[7], p[29], p[40]) = (p[7], p[29], p[40], p[10])

    def fourth_action(self):
        """
        Applies on the numbers the action, turning the right face,
        (25 27 32 30)(26 29 31 28)(3 38 43 19)(5 36 45 21)(8 33 48 24)
        """
        p = self.numbers
        (p[29], p[24], p[26], p[31]) = (p[24], p[26], p[31], p[29])
        (p[27], p[25], p[28], p[30]) = (p[25], p[28], p[30], p[27])
        (p[18], p[2], p[37], p[42]) = (p[2], p[37], p[42], p[18])
        (p[20], p[4], p[35], p[44]) = (p[4], p[35], p[44], p[20])
        (p[23], p[7], p[32], p[47]) = (p[7], p[32], p[47], p[23])

    def fifth_action(self):
        """
        Applies on the numbers the action, turning the rear face,
        (33 35 40 38)(34 37 39 36)(3 9 46 32)( 2 12 47 29)( 1 14 48 27)
        """
        p = self.numbers
        (p[37], p[32], p[34], p[39]) = (p[32], p[34], p[39], p[37])
        (p[35], p[33], p[36], p[38]) = (p[33], p[36], p[38], p[35])
        (p[31], p[2], p[8], p[45]) = (p[2], p[8], p[45], p[31])
        (p[28], p[1], p[11], p[46]) = (p[1], p[11], p[46], p[28])
        (p[26], p[0], p[13], p[47]) = (p[0], p[13], p[47], p[26])

    def sixth_action(self):
        """
        Applies on the numbers the action, turning the bottom face,
        (41 43 48 46)(42 45 47 44)(14 22 30 38)(15 23 31 39)(16 24 32 40).
        """
        p = self.numbers
        (p[45], p[40], p[42], p[47]) = (p[40], p[42], p[47], p[45])
        (p[43], p[41], p[44], p[46]) = (p[41], p[44], p[46], p[43])
        (p[37], p[13], p[21], p[29]) = (p[13], p[21], p[29], p[37])
        (p[38], p[14], p[22], p[30]) = (p[14], p[22], p[30], p[38])
        (p[39], p[15], p[23], p[31]) = (p[15], p[23], p[31], p[39])

    def apply_action(self, act):
        """
        Applies the action corresponding to the number in act.
        """
        self.clear_tiles()
        if(act == 1):
            self.first_action()
        elif(act == 2):
            self.second_action()
        elif(act == 3):
            self.third_action()
        elif(act == 4):
            self.fourth_action()
        elif(act == 5):
            self.fifth_action()
        else:
            self.sixth_action()
        self.start_fill()
        self.show_state()
        self.moves.append(act)

    def apply_first_action(self):
        """
        Applies the first group action.
        """
        self.apply_action(1)

    def apply_second_action(self):
        """
        Applies the second group action.
        """
        self.apply_action(2)

    def apply_third_action(self):
        """
        Applies the third group action.
        """
        self.apply_action(3)

    def apply_fourth_action(self):
        """
        Applies the fourth group action.
        """
        self.apply_action(4)

    def apply_fifth_action(self):
        """
        Applies the fourth group action.
        """
        self.apply_action(5)

    def apply_sixth_action(self):
        """
        Applies the sixth group action.
        """
        self.apply_action(6)

    def shuffle_tiles(self):
        """
        Clears the canvas, shuffles the numbers
        and renders the configuration with the shuffled numbers.
        """
        # from random import shuffle
        from random import randint
        self.clear()
        # shuffle(self.numbers)
        for i in range(15):
            r = randint(1, 6)
            self.c.after(self.sleep)
            if(r == 1):
                self.apply_first_action()
            elif(r == 2):
                self.apply_second_action()
            elif(r == 3):
                self.apply_third_action()
            elif(r == 4):
                self.apply_fourth_action()
            elif(r == 5):
                self.apply_fifth_action()
            elif(r == 6):
                self.apply_sixth_action()
            self.c.update()

    def solve_puzzle(self):
        """
        Undoes the moves.
        """
        last = len(self.moves)
        while(last > 0):
            r = self.moves.pop(last-1)
            self.c.after(self.sleep)
            if(r == 1):
                self.apply_first_action()
                self.apply_first_action()
                self.apply_first_action()
            elif(r == 2):
                self.apply_second_action()
                self.apply_second_action()
                self.apply_second_action()
            elif(r == 3):
                self.apply_third_action()
                self.apply_third_action()
                self.apply_third_action()
            elif(r == 4):
                self.apply_fourth_action()
                self.apply_fourth_action()
                self.apply_fourth_action()
            elif(r == 5):
                self.apply_fifth_action()
                self.apply_fifth_action()
                self.apply_fifth_action()
            elif(r == 6):
                self.apply_sixth_action()
                self.apply_sixth_action()
                self.apply_sixth_action()
            self.c.update()
            last = len(self.moves)
            for k in range(3):
                r = self.moves.pop(last-1)
                last = last - 1

def main():
    """
    Sets up a canvas with magnification factor.
    """
    top = Tk()
    Cube(top, MAGNIFICATION, 500)
    top.mainloop()

if __name__ == "__main__":
    main()
