#################################################
# hw8.py:
# Version 3.1
#
# Your name:
# Your andrew id:
#################################################

import cs112_f22_week8_linter
from cmu_112_graphics import *

import decimal

#################################################
# Helper functions
#################################################

def almostEqual(d1, d2, epsilon=10**-7):
    # note: use math.isclose() outside 15-112 with Python version 3.5 or later
    return (abs(d2 - d1) < epsilon)

def roundHalfUp(d):
    # Round to nearest with ties going away from zero.
    rounding = decimal.ROUND_HALF_UP
    # See other rounding options here:
    # https://docs.python.org/3/library/decimal.html#rounding-modes
    return int(decimal.Decimal(d).to_integral_value(rounding=rounding))

#################################################
# Classes and functions for you to write
#################################################

def loadElementData(filename):
    # We'll start you off with the code to read a file
    # https://www.cs.cmu.edu/~112/notes/notes-strings.html#basicFileIO
    # (The UTF-8 encoding helps work on all operating systems)
    with open(filename, "r", encoding="utf-8") as f:
        fileString = f.read()

    # TODO: MODIFY THESE EXAMPLE LINES
    # Parse the file data and load into a dictionary (rather than just 
    # printing it)
    firstLine = True
    for line in fileString.splitlines():
        if firstLine:
            print('HEADER LINE: ', end='')
            firstLine = False
        print(line)
    
    return {} # Empty dictionary instead of 42

def loadReactionData(filename):
    # Start with reading the file data:
    # https://www.cs.cmu.edu/~112/notes/notes-strings.html#basicFileIO

    return {} # Empty dictionary instead of 42

def tryReaction(elem1, elem2, reactionsDict):
    return None # None instead of 42

def isTerminalElement(elem, reactionsDict):
    return True # True rather than 42

def allNextElements(elementSet, reactionsDict):
    return set() # Empty set rather than 42

def bestNextElement(elementSet, reactionsDict):
    return "42"

def getPairSum(L, target):
    return 42

def containsPythagoreanTriple(L):
    return 42

def movieAwards(oscarResults):
    return 42

class Actor(object):
    pass

class Movie(object):
    pass

#################################################
# Test Functions
#################################################

def testLoadElementData():
    print("Testing loadElementData()...")
    elemDict = loadElementData('./lilAl_elements_small.csv')
    assert(len(elemDict) == 17)
    assert(elemDict['fire'] == \
        "https://littlealchemy.com/cheats/img/base/2.png")
    assert(elemDict['volcano'] == \
        "https://littlealchemy.com/cheats/img/base/8.png")
    assert(("water" in elemDict) == False)
    assert(("stone" in elemDict) == True)
    assert(("metal" in elemDict) == True)
    assert(("Name" in elemDict) == False)
    assert(("axolotl" in elemDict) == False)
    assert(elemDict['metal'] == \
        "https://littlealchemy.com/cheats/img/base/36.png")

    # Big dataset
    elemDict = loadElementData('./lilAl_elements.csv')
    assert(len(elemDict) == 435)
    assert(elemDict['fire'] == \
        "https://littlealchemy.com/cheats/img/base/2.png")
    assert(elemDict['sloth'] == \
        "https://littlealchemy.com/cheats/img/base/408.png")
    assert(elemDict['saturn'] == \
        "https://littlealchemy.com/cheats/img/base/616.png")
    assert(("water" in elemDict) == True)
    assert(("cow" in elemDict) == True)
    assert(("axolotl" in elemDict) == False)

    print("Passed!")

def testLoadReactionData():
    print("Testing loadReactionData()...")
    reactionsDict = loadReactionData('./lilAl_reactions_small.csv')
    assert(len(reactionsDict) == 17)
    assert(("earth" in reactionsDict) == False)
    assert((("earth", "fire") in reactionsDict) == True)
    assert((("fire", "earth") in reactionsDict) == False)
    assert((("earth", "earth") in reactionsDict) == True)
    assert((("fire", "stone") in reactionsDict) == True)
    assert((("Element1", "Element2") in reactionsDict) == False)
    assert((("air", "cloud") in reactionsDict) == False)
    assert(reactionsDict[("earth","fire")] == "lava")
    assert(reactionsDict[("air","stone")] == "sand")
    assert(reactionsDict[("fire","stone")] == "metal")

    # Big dataset
    reactionsDict = loadReactionData('./lilAl_reactions.csv')
    assert(len(reactionsDict) == 715)
    assert(("water" in reactionsDict) == False)
    assert((("earth", "fire") in reactionsDict) == True)
    assert((("fire", "earth") in reactionsDict) == False)
    assert((("water", "water") in reactionsDict) == True)
    assert((("air", "cloud") in reactionsDict) == True)
    assert((("stegosaurus", "axolotl") in reactionsDict) == False)
    assert(reactionsDict[("water","water")] == "sea")
    assert(reactionsDict[("milk","ice cream")] == "milk shake")
    assert(reactionsDict[("ring","planet")] == "saturn")

    print("Passed!")
    # app.reactionsDict = loadReactionData('./lilAl_reactions.csv')
    #app.reactionsDict = loadReactionData('./lilAl_reactions_small.csv')

def testTryReaction():
    print("Testing tryReaction()...")
    reactionsDict = {
        ("earth","fire"):"lava",
        ("air","air"):"pressure",
        ("air","fire"):"energy"
    }
    assert(tryReaction("earth", "fire", reactionsDict) == "lava")
    assert(tryReaction("air", "air", reactionsDict) == "pressure")
    assert(tryReaction("air", "fire", reactionsDict) == "energy")
    assert(tryReaction("fire", "air", reactionsDict) == "energy")
    assert(tryReaction("fire", "fire", reactionsDict) == None)
    assert(tryReaction("water", "fire", reactionsDict) == None)
    assert(tryReaction("water", "water", reactionsDict) == None)

    print("Passed!")

def testIsTerminalElement():
    print("Testing isTerminalElement()...")
    reactionsDict = {
        ("earth","fire"):"lava",
        ("air","air"):"pressure",
        ("air","fire"):"energy"
    }
    assert(isTerminalElement("earth", reactionsDict) == False)
    assert(isTerminalElement("air", reactionsDict) == False)
    assert(isTerminalElement("energy", reactionsDict) == True)

    # Small reactions dictionary
    reactionsDict = loadReactionData('./lilAl_reactions_small.csv')
    assert(isTerminalElement("air", reactionsDict) == False)
    assert(isTerminalElement("energy", reactionsDict) == False)
    assert(isTerminalElement("gunpowder", reactionsDict) == False)
    assert(isTerminalElement("lava", reactionsDict) == False)
    assert(isTerminalElement("explosion", reactionsDict) == True)
    assert(isTerminalElement("metal", reactionsDict) == True)

    print("Passed!")

def testAllNextElements():
    print("Testing allNextElements()...")
    reactionsDict = {
        ("earth","fire"):"lava",
        ("air","air"):"pressure",
        ("air","fire"):"energy"
    }
    assert(allNextElements({"fire", "earth", "air"}, reactionsDict) == \
        {"energy", "pressure", "lava"})
    assert(allNextElements({"fire", "earth", "air", "lava"}, reactionsDict) == \
        {"energy", "pressure"})
    assert(allNextElements({"fire", "air"}, reactionsDict) == \
        {"energy", "pressure"})
    assert(allNextElements({"fire", "earth"}, reactionsDict) == \
        {"lava"})
    assert(allNextElements({"air"}, reactionsDict) == \
        {"pressure"})
    assert(allNextElements({"earth"}, reactionsDict) == set())
    elemSet = {"fire", "earth", "air", "energy", "pressure", "lava"}
    assert(allNextElements(elemSet, reactionsDict) == set())

    # Small dataset
    reactionsDict = loadReactionData('./lilAl_reactions_small.csv')

    elemSet = {"fire", "earth", "air"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "energy", "pressure", "lava"})

    elemSet = {"fire", "earth", "air", "dust"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"energy", "pressure", "lava", "gunpowder"})

    elemSet = {"fire", "earth", "air", "energy"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "pressure", "lava", "wind", "earthquake"})

    elemSet = {"fire", "earth", "air", "pressure"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "energy", "lava", "wind"})

    elemSet = {"fire", "earth", "air", "lava"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "energy", "pressure", "stone", "volcano"})

    elemSet = {"fire", "earth", "air", "energy", "lava"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "pressure", "wind", "earthquake", "stone", "volcano"})

    # Big dataset
    reactionsDict = loadReactionData('./lilAl_reactions.csv')

    elemSet = {"water", "fire", "earth", "air"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "energy", "pressure", "lava", "mud", "rain", "sea", "steam"})

    elemSet = {"water", "fire", "earth", "air", "energy"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "pressure", "lava", "mud", "rain", "sea", "steam", 
        "wind", "earthquake"})

    elemSet = {"water", "fire", "earth", "air", "energy", "lava"}
    assert(allNextElements(elemSet, reactionsDict) == \
        {"dust", "pressure", "mud", "rain", "sea", "steam", 
        "wind", "earthquake", "volcano", "obsidian", "stone"})

    print("Passed!")

def testBestNextElement():
    print("Testing bestNextElement()...")
    reactionsDict = loadReactionData('./lilAl_reactions_small.csv')

    elemSet = {"fire", "earth", "air"}
    assert(bestNextElement(elemSet, reactionsDict) == "energy")

    elemSet = {"fire", "earth", "air", "energy"}
    assert(bestNextElement(elemSet, reactionsDict) == "lava")

    elemSet = {"fire", "earth", "air", "energy", "lava"}
    assert(bestNextElement(elemSet, reactionsDict) == "stone")

    reactionsDict = {
        ("earth","fire"):"lava",
        ("air","air"):"pressure",
        ("air","fire"):"energy"
    }
    assert(bestNextElement({"fire", "earth", "air"}, reactionsDict) == "energy")
    elemSet = {"fire", "earth", "air", "energy", "pressure", "lava"}
    assert(bestNextElement(elemSet, reactionsDict) == None)

    reactionsDict = {
        ("water","fire"):"steam",
        ("water","energy"):"steam",
        ("air","fire"):"energy",
        ("air","steam"):"cloud"
    }
    elemSet = {"water", "fire", "air"}
    assert(bestNextElement(elemSet, reactionsDict) == "steam")

    reactionsDict = loadReactionData('./lilAl_reactions.csv')

    elemSet = {"water", "fire", "earth", "air"}
    assert(bestNextElement(elemSet, reactionsDict) == "lava")

    elemSet = {"water", "fire", "earth", "air", "lava"}
    assert(bestNextElement(elemSet, reactionsDict) == "energy")

    print("Passed!")

def testGetPairSum():
    print("Testing getPairSum()...")
    assert(getPairSum([1], 1) == None)
    assert(getPairSum([5, 2], 7) in [ (5, 2), (2, 5) ])
    assert(getPairSum([10, -1, 1, -8, 3, 1], 2) in
                      [ (10, -8), (-8, 10),(-1, 3), (3, -1), (1, 1) ])
    assert(getPairSum([10, -1, 1, -8, 3, 1], 10) == None)
    assert(getPairSum([10, -1, 1, -8, 3, 1, 8, 19, 0, 5], 10) in
                      [ (10, 0), (0, 10)] )
    assert(getPairSum([10, -1, 1, -8, 3, 1, 8, 19, -9, 5], 10) in
                      [ (19, -9), (-9, 19)] )
    assert(getPairSum([1, 4, 3], 2) == None) # catches reusing values! 1+1...
    print("Passed!")

def testContainsPythagoreanTriple():
    print("Testing containsPythagoreanTriple()...")
    assert(containsPythagoreanTriple([1,3,6,2,5,1,4]) == True)
    assert(containsPythagoreanTriple([1,3,6,2,8,1,4]) == False)
    assert(containsPythagoreanTriple([1,730,3,6,54,2,8,1,728,4])
                                      == True) # 54, 728, 730
    assert(containsPythagoreanTriple([1,730,3,6,54,2,8,1,729,4]) == False)
    assert(containsPythagoreanTriple([1,731,3,6,54,2,8,1,728,4]) == False)
    assert(containsPythagoreanTriple([1,731,3,6,54,2,8,1,728,4,
                                6253, 7800, 9997]) == True) # 6253, 7800, 9997
    assert(containsPythagoreanTriple([1,731,3,6,54,2,8,1,728,4,
                                      6253, 7800, 9998]) == False)
    assert(containsPythagoreanTriple([1,731,3,6,54,2,8,1,728,4,
                                      6253, 7800, 9996]) == False)
    assert(containsPythagoreanTriple([1, 2, 3, 67, 65, 35,83, 72, 
                                      97, 25, 98, 12]) == True) # 65, 72, 97
    assert(containsPythagoreanTriple([1, 1, 1]) == False)
    assert(containsPythagoreanTriple([1, 1, 2]) == False)
    assert(containsPythagoreanTriple([3, 5, 5]) == False)
    assert(containsPythagoreanTriple([1,2,3,4,5,6,7,8,9,10]) == True)
    assert(containsPythagoreanTriple([10,11,20,21,30,31,40,41,50,
                                    51,60,61]) == True)
    assert(containsPythagoreanTriple([300,161,42,100,240,64,72,112,
                                    1,2,3,289,5,15]) == True)
    assert(containsPythagoreanTriple([300,161,42,100,250,225,288,290,
                                    240,64,72,112,1,2,3,289,5,15,
                                    200,190]) == True)
    assert(containsPythagoreanTriple([100,110,120,140,133,140,150,156,
                                    160,170,180,190,200]) == True)
    print("Passed!")

def testMovieAwards():
    print('Testing movieAwards()...')
    tests = [
      (({ ("Best Picture", "The Shape of Water"), 
          ("Best Actor", "Darkest Hour"),
          ("Best Actress", "Three Billboards Outside Ebbing, Missouri"),
          ("Best Director", "The Shape of Water") },),
        { "Darkest Hour" : 1,
          "Three Billboards Outside Ebbing, Missouri" : 1,
          "The Shape of Water" : 2 }),
      (({ ("Best Picture", "Moonlight"),
          ("Best Director", "La La Land"),
          ("Best Actor", "Manchester by the Sea"),
          ("Best Actress", "La La Land") },),
        { "Moonlight" : 1,
          "La La Land" : 2,
          "Manchester by the Sea" : 1 }),
      (({ ("Best Picture", "12 Years a Slave"),
          ("Best Director", "Gravity"),
          ("Best Actor", "Dallas Buyers Club"),
          ("Best Actress", "Blue Jasmine") },),
        { "12 Years a Slave" : 1,
          "Gravity" : 1,
          "Dallas Buyers Club" : 1,
          "Blue Jasmine" : 1 }),
      (({ ("Best Picture", "The King's Speech"),
          ("Best Director", "The King's Speech"),
          ("Best Actor", "The King's Speech") },),
        { "The King's Speech" : 3}),
      (({ ("Best Picture", "Spotlight"), ("Best Director", "The Revenant"),
          ("Best Actor", "The Revenant"), ("Best Actress", "Room"),
          ("Best Supporting Actor", "Bridge of Spies"),
          ("Best Supporting Actress", "The Danish Girl"),
          ("Best Original Screenplay", "Spotlight"),
          ("Best Adapted Screenplay", "The Big Short"),
          ("Best Production Design", "Mad Max: Fury Road"),
          ("Best Cinematography", "The Revenant") },),
        { "Spotlight" : 2,
          "The Revenant" : 3,
          "Room" : 1,
          "Bridge of Spies" : 1,
          "The Danish Girl" : 1,
          "The Big Short" : 1,
          "Mad Max: Fury Road" : 1 }),
       ((set(),), { }),
            ]
    for args,result in tests:
        if (movieAwards(*args) != result):
            print('movieAwards failed:')
            print(args)
            print(result)
            assert(False)
    print('Passed!')

def testActorMovieClasses():
    print('Testing Actor and Movie Classes...')

    # Actor class
    tom = Actor("Tom Hanks")
    # Note that tom != "Tom Hanks": one is an object, and the other is a string.
    assert(isinstance(tom, Actor))
    assert(tom.getName() == "Tom Hanks")

    # Movie class
    mrRogers = Movie("A Beatiful Day in the Neighborhood", 2019)
    assert(isinstance(mrRogers, Movie))
    assert(mrRogers.getTitle() == "A Beatiful Day in the Neighborhood")
    assert(mrRogers.getYear() == 2019)

    # Note: actor.getMovies() returns a list of Movie objects that
    #       that this actor is in, listed in the order that
    #       they were added. This will start off empty.
    # Note: actor.getMovieTitles() returns a list of strings, the
    #       titles of the movies this actor was in.  This list is sorted!
    assert(tom.getMovies() == [])
    assert(tom.getMovieTitles() == [])
    # Similarly, getCast returns a list of Actor objects, while
    # getCastNames returns a sorted list of Actor names
    assert(mrRogers.getCast() == [])
    assert(mrRogers.getCastNames() == [])

    mrRogers.addActor(tom)
    assert(mrRogers.getCast() == [tom])
    assert(mrRogers.getCastNames() == ["Tom Hanks"])
    # Adding an actor also updates the actor
    assert(tom.getMovies() == [mrRogers])
    assert(tom.getMovieTitles() == ["A Beatiful Day in the Neighborhood"])

    diego = Actor("Diego Luna")
    terminal = Movie("The Terminal", 2004)
    terminal.addCast([tom, diego])
    assert(terminal.getCast() == [tom, diego])
    assert(terminal.getCastNames() == ["Diego Luna", "Tom Hanks"])
    terminal.addActor(tom)
    assert(terminal.getCast() == [tom, diego]) # Don't add twice

    felicity = Actor("Felicity Jones")
    rogue = Movie("Rogue One", 2016)
    rogue.addCast([felicity, diego])

    inferno = Movie("Inferno", 2016)
    felicity.addMovie(inferno)
    tom.addMovie(inferno)
    assert(felicity.getMovies() == [rogue, inferno])
    assert(tom.getMovieTitles() == ["A Beatiful Day in the Neighborhood",
        "Inferno", "The Terminal"])

    print('Passed!')

def testAll():
    testLoadElementData()
    testLoadReactionData()
    testTryReaction()
    testIsTerminalElement()
    testAllNextElements()
    testBestNextElement()

    testGetPairSum()
    testContainsPythagoreanTriple()
    testMovieAwards()
    testActorMovieClasses()

#################################################
# Little Alchemy app
# Based on https://littlealchemy.com/
#################################################

def appStarted(app):
    app.bgColor = 'snow1'
    app.bgDark = 'indigo'
    app.darkMode = False

    app.elemSize = 40
    app.toolboxWidth = app.elemSize*4.5
    app.toolboxMargin = app.elemSize//4
    app.toolboxCellHeight = app.elemSize + app.toolboxMargin*2
    app.toolboxFont = f'Arial {int(app.elemSize/3)}'
    app.toolboxFontTerminal = f'Arial {int(app.elemSize/3)} underline'
    app.toolboxFontColor = 'gray'
    app.toolboxScroll = 0
    app.scrollbarWidth = app.elemSize//2
    app.toolboxExtent = 0
    
    app.workspaceElements = []
    app.toolboxElements = []
    
    app.mousePressedLoc = None
    app.pressedElem = None
    app.pressedScroll = None
    app.selectedIndex = None

    app.showAllNext = False
    app.showBestNext = False

    app.elementIconFilenameDict = loadElementData('./lilAl_elements.csv')
    #app.elementIconFilenameDict = loadElementData('./lilAl_elements_small.csv')

    # We load icons on demand, so start with this empty
    app.elementIconDict = {} 

    app.reactionsDict = loadReactionData('./lilAl_reactions.csv')
    #app.reactionsDict = loadReactionData('./lilAl_reactions_small.csv')

    initElementSet(app)

def initElementSet(app):
    app.elementSet = set()

    startingElements = ['water','fire','earth','air']
    for elementName in startingElements:
        app.elementSet.add(elementName)
        loadIcon(app, elementName)

    updateToolbox(app)
    
def loadIcon(app, elementName):
    if elementName not in app.elementIconFilenameDict:
        print(f'Element "{elementName} not in icon filename dictionary')
        return

    if elementName not in app.elementIconDict:
        # Load the icon
        iconFilename = app.elementIconFilenameDict[elementName]
        print(f'Loading icon {iconFilename}')
        im = app.loadImage(iconFilename)

        # Resize image
        imWidthOrig, imHeightOrig = im.size
        imSizeOrig = max(imWidthOrig, imHeightOrig)
        im = app.scaleImage(im, app.elemSize/imSizeOrig)

        im = ImageTk.PhotoImage(im)

        app.elementIconDict[elementName] = im

def pointInElement(app, elemX, elemY, mouseX, mouseY):
    if mouseX < elemX:
        return False
    elif mouseX >= elemX + app.elemSize:
        return False
    elif mouseY < elemY:
        return False
    elif mouseY >= elemY + app.elemSize:
        return False
    else:
        return True

def pointInToolbox(app, x, y):
    if x < app.width-app.toolboxWidth or x >= app.width:
        return False
    if y < 0 or y >= app.height:
        return False

    return True       

def pointInWorkspace(app, x, y):
    workspaceWidth = app.width-app.toolboxWidth

    if x < 0 or x >= workspaceWidth:
        return False
    if y < 0 or y >= app.height:
        return False

    return True       

def viewToElementIndex(app, x, y, skipIndex=None):
    for i in reversed(range(len(app.workspaceElements))):
        if i == skipIndex:
            continue

        element = app.workspaceElements[i]
        if pointInElement(app, element[0], element[1], x, y):
            return i

    return None
    
def viewToToolboxIndex(app, x, y):
    for i in range(len(app.toolboxElements)):
        elem = app.toolboxElements[i]
        if pointInElement(app, elem[0], elem[1] + app.toolboxScroll, x, y):
            return i

    return None

def selectElement(app, x, y):
    app.selectedIndex = None
    app.pressedElem = None
    elem = None

    elemIndex = viewToElementIndex(app, x, y)
    if elemIndex is not None:
        # Move app to end of list so that it will be displayed on top
        elem = app.workspaceElements.pop(elemIndex)
    else:
        elemIndex = viewToToolboxIndex(app, x,y)
        if elemIndex is not None:
            elemX, elemY, elemColor = app.toolboxElements[elemIndex]
            elemY += app.toolboxScroll
            elem = (elemX, elemY, elemColor)
        
    if elem is not None:
        app.workspaceElements.append(elem)

        app.selectedIndex = len(app.workspaceElements)-1
        app.pressedElem = app.workspaceElements[app.selectedIndex]

def updateToolbox(app):
    workspaceWidth = app.width - app.toolboxWidth
    elemX = workspaceWidth + app.toolboxMargin
    
    app.toolboxElements = []
    i = 0
    for elem in sorted(app.elementSet):
        elemY = i*app.toolboxCellHeight + app.toolboxMargin
        
        app.toolboxElements.append((elemX, elemY, elem))
        
        i += 1
    
    app.toolboxExtent = len(app.elementSet)*app.toolboxCellHeight + \
        app.toolboxMargin*2

def combineWorkspaceElements(app, elemIndex1, elemIndex2):
    elem1 = app.workspaceElements[elemIndex1][2]
    elem2 = app.workspaceElements[elemIndex2][2]
    newElem = tryReaction(elem1, elem2, app.reactionsDict)

    if newElem is None:
        return

    app.elementSet.add(newElem)
    loadIcon(app, newElem)

    updateToolbox(app)

    newX, newY = app.workspaceElements[app.selectedIndex][:2]

    app.workspaceElements.append((newX, newY, newElem))

    # Remove the bigger index first
    # (Shouldn't be needed because elemIndex1 should be the selectedIndex,
    # which should be the last index)
    if elemIndex1 > elemIndex2:
        app.workspaceElements.pop(elemIndex1)
        app.workspaceElements.pop(elemIndex2)
    else:
        app.workspaceElements.pop(elemIndex2)
        app.workspaceElements.pop(elemIndex1)

def keyPressed(app, event):
    if event.key == 'Escape':
        app.showAllNext = False
        app.showBestNext = False
    elif event.key == 'a':
        app.showAllNext = not app.showAllNext
    elif event.key == 'b':
        app.showBestNext = not app.showBestNext
    elif event.key == 'c':
        app.workspaceElements = []
    elif event.key == 'd':
        app.darkMode = not app.darkMode

def mousePressed(app, event):
    if app.showAllNext or app.showBestNext:
        return

    app.mousePressedLoc = (event.x, event.y)

    selectElement(app, event.x, event.y)

    if app.selectedIndex is None:
        # Check for toolbox background selection
        if pointInToolbox(app, event.x, event.y):
            app.pressedScroll = app.toolboxScroll
    
def mouseDragged(app, event):
    if app.showAllNext or app.showBestNext:
        return

    dx = event.x - app.mousePressedLoc[0]
    dy = event.y - app.mousePressedLoc[1]

    if app.selectedIndex is not None:
        startElemX, startElemY, elem = app.pressedElem

        app.workspaceElements[app.selectedIndex] = \
            (startElemX+dx, startElemY+dy, elem)

    elif app.pressedScroll is not None:
        app.toolboxScroll = min(0, app.pressedScroll + dy)

def mouseReleased(app, event):
    if app.showAllNext or app.showBestNext:
        return

    if app.selectedIndex is not None:
        if pointInWorkspace(app, event.x, event.y):
            # React as needed
            secondElemIndex = viewToElementIndex(app, event.x, event.y, 
            skipIndex=app.selectedIndex)
            if secondElemIndex is not None:
                combineWorkspaceElements(app, 
                    app.selectedIndex, secondElemIndex)

        else:
            app.workspaceElements.pop(app.selectedIndex)

        app.selectedIndex = None

    app.mousePressedLoc = None
    app.pressedElem = None
    app.pressedScroll = None

###
### View
###

def drawAllNext(app, canvas):
    if app.showAllNext:
        workspaceWidth = app.width - app.toolboxWidth
        canvas.create_rectangle(app.elemSize, app.elemSize,
            workspaceWidth - app.elemSize, app.height-app.elemSize,
            fill='dark magenta', outline='black', width=5)

        nextElementsSet = allNextElements(app.elementSet, app.reactionsDict)
        if len(nextElementsSet) > 0:
            nextElementsString = '\n'.join(sorted(nextElementsSet))
        else:
            nextElementsString = "No more elements to find!"

        canvas.create_text(app.elemSize*2, app.elemSize*2,
            text="Try creating one of the following:\n\n"+nextElementsString, 
            fill='white', anchor='nw', font=app.toolboxFont)

def drawBestNext(app, canvas):
    if app.showBestNext:
        bestNext = bestNextElement(app.elementSet, app.reactionsDict)
        if bestNext is None:
            bestNext = "No more elements to find!"

        workspaceWidth = app.width - app.toolboxWidth
        canvas.create_rectangle(app.elemSize, app.elemSize,
            workspaceWidth - app.elemSize, app.height-app.elemSize,
            fill='dark magenta', outline='black', width=5)

        canvas.create_text(app.elemSize*2, app.elemSize*2,
            text="Try creating :\n\n"+bestNext, 
            fill='white', anchor='nw', font=app.toolboxFont)

def drawHints(app, canvas):
    hintStr = "Press 'A' for all next elements\n" + \
        "Press 'B' for best next element\n" + \
        "Press 'C' to clear workspace\n" + \
        "Press 'D' to toggle dark mode\n" + \
        "\nhttps://littlealchemy.com/" 
    canvas.create_text(app.elemSize/2, app.height-app.elemSize/2,
        text=hintStr, anchor='sw', font=app.toolboxFont,
        fill=app.toolboxFontColor)

    drawAllNext(app, canvas)
    drawBestNext(app, canvas)


def drawElement(app, canvas, x, y, elem, isSelected=False, drawName=False):
    if isSelected:
        outline = 'black'
    else:
        outline = ''

    width=2
    # width=5

    if elem in app.elementIconDict:
        im = app.elementIconDict[elem]
        canvas.create_image(x, y, image=im, anchor='nw')
    else:
        label = elem[0].upper() + elem[1]
        font = f'Arial {int(app.elemSize/2)}'
        canvas.create_text(x+app.elemSize//2, y+app.elemSize//2, text=label,
            font=font)

    if drawName:
        if isTerminalElement(elem, app.reactionsDict):
            font = app.toolboxFontTerminal
        else:
            font = app.toolboxFont

        textX = x + app.elemSize + app.toolboxMargin
        textY = y + app.elemSize//2
        canvas.create_text(textX, textY, text=elem, font=font,
            anchor='w', fill=app.toolboxFontColor)

    if isSelected:
        canvas.create_rectangle(x, y, x+app.elemSize, y+app.elemSize,
            fill='', outline=outline, width=width)

def drawSelectedElement(app, canvas):
    if app.selectedIndex is not None:
        x, y, elem = app.workspaceElements[app.selectedIndex]
    
        isSelected = True
        drawElement(app, canvas, x, y, elem, isSelected)

def drawWorkspaceElements(app, canvas):
    for i in range(len(app.workspaceElements)):
        x, y, elem = app.workspaceElements[i]
        isSelected = (i == app.selectedIndex)
        if isSelected:
            continue

        drawElement(app, canvas, x, y, elem, isSelected)

def drawScrollbar(app, canvas):
    if app.toolboxExtent <= app.height:
        return
    
    scrollbarHeight = app.height*(app.height / app.toolboxExtent)

    maxScroll = app.toolboxExtent - app.height
    maxScrollbarMove = app.height - scrollbarHeight
    scrollBarY = maxScrollbarMove*(-app.toolboxScroll/maxScroll)

    scrollBarY0 = min(scrollBarY, maxScrollbarMove)

    canvas.create_rectangle(
        app.width-app.scrollbarWidth, scrollBarY0,
        app.width, scrollBarY0+scrollbarHeight,
        fill='light gray', outline='')

def drawToolbox(app, canvas):
    if app.darkMode:
        bgColor = app.bgDark
    else:
        bgColor = app.bgColor

    canvas.create_rectangle(app.width-app.toolboxWidth, 0, 
        app.width, app.height,
        fill=bgColor, outline='')

    for x, y, elem in app.toolboxElements:
        isSelected = False
        drawName = True
        drawElement(app, canvas, x, y + app.toolboxScroll, elem, 
            isSelected, drawName)        
        
def drawWorkspace(app, canvas):
    if app.darkMode:
        bgColor = app.bgDark
    else:
        bgColor = app.bgColor

    workspaceWidth = app.width-app.toolboxWidth

    canvas.create_rectangle(0, 0, workspaceWidth, app.height,
        fill=bgColor, outline='')

    lineWidth = 5
    canvas.create_line(workspaceWidth, 0, workspaceWidth, app.height,
        fill='gray', width=lineWidth)

def redrawAll(app, canvas):
    drawWorkspace(app, canvas)
    drawWorkspaceElements(app, canvas)
    drawToolbox(app, canvas)
    drawScrollbar(app, canvas)
    drawSelectedElement(app, canvas)
    drawHints(app, canvas)

def playLittleAlchemy():
    runApp(width=600, height=800)

#################################################
# main
#################################################

def main():
    cs112_f22_week8_linter.lint()

    # For fun
    # Comment out to skip
    # This app will naturally work better once you've implemented the
    # associated functions
    playLittleAlchemy()

    testAll()

if __name__ == '__main__':
    main()
