from tkinter import Tk, Canvas, Frame, Button, LEFT, RIGHT, SUNKEN
from time import sleep

# global variables used for placement of towers and disks
canvassHeight = 250
canvassWidth = 650

halfTowerWidth = 100
towerHeight = 120
towerBaseThickness = 5
towerSpindleHalfWidth = 2

diskHeight = 20
tower1x = 15 + halfTowerWidth
tower2x = tower1x + 10 + 2*halfTowerWidth
tower3x = tower2x + 10 + 2*halfTowerWidth
towersBottom = canvassHeight - 50

# a Stack-like class that maintains information about one tower and its disks
#
class Tower():
    def __init__(self, xposition, initialItems = []):
        self.items = initialItems
        self.xposition = xposition
    def isEmpty(self):
        return(len(self.items) == 0)
    def push(self, item):
        self.items.append(item)
    def pop(self):
        return(self.items.pop())
    def top(self):
        return(self.items[-1])
    def numItems(self):
        return(len(self.items))

# Movie the top disk on fromTower to the top of ToTower.
#   - fromTower and toTower must both be Tower objects.
#   - This function will:
#     1) update both Tower objects, and also
#     2) display the result on the canvas.
#
def move(fromTower, toTower):
    global canvas
    #print("moving from ", fromTower, " to ", toTower)
    if (fromTower.isEmpty()):
        return
    id1 = fromTower.pop()
    currCoords = canvas.coords(id1)
    xchange = toTower.xposition - fromTower.xposition
    ychange = - (toTower.numItems() - fromTower.numItems()) * diskHeight 
    canvas.coords(id1,
                  currCoords[0] + xchange,
                  currCoords[1] + ychange,
                  currCoords[2] + xchange,
                  currCoords[3] + ychange)
    # Normally, it wouldn't make sense to put this delay in here.
    # However, putting it here allows the Towers of Hanoi
    # solver be  independent of graphics and waiting.
    # Without the wait, the graphics updates might be too fast to see
    # when the automatic solve is used.
    # You can make the argument to "sleep" smaller or larger (the
    # unit is seconds).  
    sleep(0.5)
    # The 'update_idletasks()" is used to "flush" graphics updates - that
    # is, it makes them appears immediately after execution.  Without this
    # line, the GUI system will often wait until a bunch of updates have been
    # specified and show them all at once. But in this case, we want to see
    # every update during, for instance, the automatic solution.
    canvas.update()
    toTower.push(id1)

def solveToH(n, fromTower, toTower, tempTower):
    if n == 0:
        return
    else:
        solveToH(n-1, fromTower, tempTower, toTower)
        move(fromTower, toTower)
        solveToH(n-1, tempTower, toTower ,fromTower)
        
def initializeAndSolve():
    initializeToH()
    solveToH(numDisks, tower1, tower2, tower3)

# Draws the base and "pole"/spindle that the disks are placed upon to create
# the tower.
#
# You should not need to change this function.
#
def drawSpindle(tower):
    canvas.create_rectangle(tower.xposition - halfTowerWidth,
                            towersBottom,
                            tower.xposition + halfTowerWidth,
                            towersBottom + towerBaseThickness,
                            fill = "black")
    
    canvas.create_rectangle(tower.xposition - towerSpindleHalfWidth,
                            towersBottom - towerHeight,
                            tower.xposition + towerSpindleHalfWidth,
                            towersBottom,
                            fill = "black")

# Create the three Tower objects, initialize tower1 with the desired
# number of disks, and draw the initial setup.
#
# You should not need to change this function.
#
def initializeToH():
    global tower1, tower2, tower3
    global canvas
    canvas.delete("all")
    
    tower1 = Tower(tower1x, [])
    tower2 = Tower(tower2x, [])
    tower3 = Tower(tower3x, [])
    drawSpindle(tower1)
    drawSpindle(tower2)
    drawSpindle(tower3)
    for i in range(numDisks):
        disk = canvas.create_rectangle(tower1.xposition - (numDisks-i)*diskHeight,
                                       towersBottom-(diskHeight*(i+1)),
                                       tower1.xposition + (numDisks-i)*diskHeight,
                                       towersBottom-(diskHeight*i),
                                       fill="#9999ff")
        tower1.push(disk)

 
# widgets and layout for Towers of Hanoi application
def initializeGUI():
    global root
    global canvas
    root = Tk()
    canvas = Canvas(root, height=canvassHeight, width=canvassWidth, relief=SUNKEN, borderwidth=2)
    canvas.pack(side=LEFT)

    buttonframe = Frame(root)
    button1 = Button(buttonframe, text='1->2', command=lambda: move(tower1,tower2))
    button2 = Button(buttonframe, text='1->3', command=lambda: move(tower1,tower3))
    button3 = Button(buttonframe, text='2->1', command=lambda: move(tower2,tower1))
    button4 = Button(buttonframe, text='2->3', command=lambda: move(tower2,tower3))
    button5 = Button(buttonframe, text='3->1', command=lambda: move(tower3,tower1))
    button6 = Button(buttonframe, text='3->2', command=lambda: move(tower3,tower2))
    button7 = Button(buttonframe, text='Initialize', command=initializeToH)
    button8 = Button(buttonframe, text='Solve', command=initializeAndSolve)
    button1.pack()
    button2.pack()
    button3.pack()
    button4.pack()
    button5.pack()
    button6.pack()
    button7.pack()
    button8.pack()
    buttonframe.pack(side=RIGHT)


# Call 'runToH' with the number of desired disks as argument
#
def runToH(numberOfDisks):
    global numDisks
    global root
    
    if ((numberOfDisks < 1) or (numberOfDisks > 10)):
        print("Error: runToH can only handle 1 to 10 disks")
        return
    numDisks = numberOfDisks
    initializeGUI()
    initializeToH()
    root.mainloop()



