import random, sys, copy, os, pygame
from pygame.locals import *

FPS = 30
WINWIDTH = 800
WINHEIGHT = 600
HALF_WINWIDTH = int(WINWIDTH / 2)
HALF_WINHEIGHT = int(WINHEIGHT / 2)

TILEWIDTH = 50
TILEHEIGHT = 85
TILEFLOORHEIGHT = 45

CAM_MOVE_SPEED = 5

OUTSIDE_DECORATION_PCT = 20

BRIGHTBLUE = ( 0, 170, 255)
WHITE      = (255, 255, 255)
BGCOLOR = BRIGHTBLUE
TEXTCOLOR = WHITE

UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'


def main():
    global FPSCLOCK, DISPLAYSURF, IMAGESDICT, TILEMAPPING, OUTSIDECOMAPPING, BASICFONT, PLAYERIMAGES, currentImage

    pygame.init()
    FPSCLOCK = pygame.time.Clock()

    DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))

    pygame.display.set_caption('Noah Nickel Adventure!')
    BASICFONT = pygame.font.Font('freesansbold.ttf', 18)

    IMAGESDICT = {'uncovered goal': pygame.image.load('RedSelector.png'),
                  'covered goal': pygame.image.load('Selector.png'),
                  'star': pygame.image.load('Star.png'),
                  'corner': pygame.image.load('Wall Block Tall.png'),
                  'wall': pygame.image.load('Wood Block Tall.png'),
                  'inside floor': pygame.image.load('Plain Block.png'),
                  'outside floor': pygame.image.load('Grass Block.png'),
                  'title': pygame.image.load('star_title.png'),
                  'solved': pygame.image.load('star_solved.png'),
                  'princess': pygame.image.load('princess.png'),
                  'boy': pygame.image.load('boy.png'),
                  'catgirl': pygame.image.load('catgirl.png'),
                  'horngirl': pygame.image.load('horngirl.png'),
                  'pinkgirl': pygame.image.load('pinkgirl.png'),
                  'rock': pygame.image.load('Rock.png'),
                  'short tree': pygame.image.load('Tree_Short.png'),
                  'tall tree': pygame.image.load('Tree_Tall.png'),
                  'ugly tree': pygame.image.load('Tree_Ugly.png')}

    TILEMAPPING = {'X': IMAGESDICT['corner'],
                   '#': IMAGESDICT['wall'],
                   'o': IMAGESDICT['inside floor'],
                   ' ': IMAGESDICT['outside floor']}

    OUTSIDECOMAPPING = {'1': IMAGESDICT['rock'],
                        '2': IMAGESDICT['short tree'],
                        '3': IMAGESDICT['tall tree'],
                        '4': IMAGESDICT['ugly tree']}
    #Super important to remember:
    #DEAR GOD REMEMBER THIS IS AN INDEX OF PLAYER IMAGES.                  
    currentImage = 0
    PLAYERIMAGES = [IMAGESDICT['princess'],
                    IMAGESDICT['boy'],
                    IMAGESDICT['catgirl'],
                    IMAGESDICT['horngirl'],
                    IMAGESDICT['pinkgirl']]
    startScreen()

    levels = readLevelsFile('levels.txt')
    currentLevelIndex = 0

    while True:
        result = runLevel(levels, currentLevelIndex)

        if result in ('solved', 'next'):
            currentLevelIndex += 1
            if currentLevelIndex >= len(levels):
                currentLevelIndex = 0

        elif result == 'back':
            currentLevelIndex -= 1
            if currentLevelIndex <0:
                currentLevelIndex = len(levels)-1

        elif result == 'reset':
            pass


def runLevel(levels, levelNum):
    global currentImage
    level0bj = levels[levelnum]
    map0bj = decorateMap(level0bj['map0bj'], level0bj['startState']['player'])
    gameState0bj = copy.deepcopy(level0bj['startState'])
    mapNeedsRedraw = True
    levelSurf = BASICFONT.render('Level %s of %s' % (level0bj['levelNum'] +1, totalNumOfLevels), 1, TEXTCOLOR)
    levelRect = levelSurf.get_rect()
    levelRect.bottomleft = (20, WINHEIGHT - 35)
    mapWidth = len(map0bj) * TITLEWIDTH
    mapHeight = (len(map0bj[0]) - 1) * (TITLEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT
    MAX_CAM_X_PAN = abs(HALF_WINHEIGHT - int(mapHeight / 2)) + TILEWIDTH
    MAX_CAM_Y_PAN = abs(HALF_WINWIDTH - int(mapWidth / 2)) + TILEHEIGHT

    levelIsComplete = False
    camera0ffsetX = 0
    camera0ffsety = 0
    cameraUp = False
    cameraDown = False
    cameraLeft = False
    cameraRight = False

    while True:
        playerMoveTo = None
        kepPressed = False

        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()

            elif event.type == KEYDOWN:
                keyPressed == True
                if event.key == K_LEFT:
                    playerMoveTo = LEFT
                elif event.key == K_RIGHT:
                    playerMoveTo = RIGHT
                elif event.key == K_UP:
                    playerMoveTo = UP
                elif event.key == K_DOWN:
                    playerMoveTo = DOWN
                elif event.key == K_a:
                    cameraLeft = True
                elif event.key == K_d:
                    cameraRight = True
                elif event.key == K_w:
                    cameraUp = True
                elif event.key == K_s:
                    cameraDown = True

                elif event.key == K_n:
                    return 'next'
                elif event.key == K_b:
                    return 'back'

                elif event.key == K_ESCAPE:
                    terminate() #Perhaps add a print box confirmation?
                elif event.key == K_BACKSPACE:
                    return 'reset' #Same confirmation for level reset.
                elif event.key == K_p:
                    currentImage += 1
                    if currentImage >= len(PLAYERIMAGES):
                        currentImage = 0
                        mapNeedsRedraw = True
                elif event.type == KEYUP:
                    if event.key == K_a:
                        cameraLeft = False
                    elif event.key == K_d:
                        cameraRight = False
                    elif event.key == K_w:
                        cameraUp = False
                    elif event.key == K_s:
                        cameraDown = False

                if playerMoveTo != None and not levelIsComplete:
                    moved = makeMove(map0bj, gameState0bj, playerMoveTo)

                    if moved:
                        gameState0bj['stepCounter'] += 1
                        mapNeedsRedraw = True

                    if isLevelFinished(level0bj, gameState0bj):
                        levelIsComplete = True
                        keyPressed = False

                DISPLAYSURF.fill(BGCOLOR)

                if mapNeedsRedraw:
                    mapSurf = drawMap(map0bj, gameState0bj, level0bj['goals'])
                    mapNeedsRedraw = False

                if cameraUp and camera0ffsetY < MAX_CAM_X_PAN:
                    camera0ffsetY += CAM_MOVE_SPEED
                elif cameraDown and camera0ffsetY > -MAX_CAM_X_PAN:
                    camera0ffsetY -= CAM_MOVE_SPEED
                if cameraLeft and camera0ffsetX < MAX_CAM_Y_PAN:
                    camera0ffsetX += CAM_MOVE_SPEED
                elif cameraRight and camera0ffsetX > -MAX_CAM_Y_PAN:
                    camera0ffsetX -= CAM_MOVE_SPEED

                mapSurfRect = mapSurf.get_rect()
                mapSurfRect.center = (HALF_WINWIDTH + camera0ffsetX, HALF_WINHEIGHT + camera0ffsetY)

                DISPLAYSURF.blit(mapSurf, levelRect)
                stepSurf = BASICFONT.render('Steps: %s' %(gameState0bj['stepCounter']), 1, TEXTCOLOR)
                stepRect = stepSurf.get_rect()
                stepRect.bottomleft = (20, WINHEIGHT -10)
                DISPLAYSURF.blit(stepSurf, stepRect)

                if levelIsComplete:
                    solvedRect = IMAGESDICT['solved'].get_rect()
                    dolvedRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
                    DISPLAYSURF.blit(IMAGESDICT['solved'], solvedRect)

                if keyPressed:
                    return 'solved'

                pygame.display.update()
                FPSCLOCK.tick()


def decorateMap(map0bj, startxy):

    startx, starty = startxy

    map0bjCopy = copy.deepcopy(map0bj)

    for x in range(len(map0bjCopy)):
        for y in range(len(map0bjCopy[0])):
            if map0bjCopy[x][y] in ('$', '.', '@', '+', '*'):
                map0bjCopy[x][y] = ' '

    floodFill(map0bjCopy, startx, starty, ' ', 'o')

    for x in range(len(map0bjCopy)):
        for y in range(len(map0bjCopy[0])): 

            if (isWall(mapObjCopy, x, y-1) and isWall(mapObjCopy, x+1, y)) or \
               (isWall(mapObjCopy, x+1, y) and isWall(mapObjCopy, x, y+1)) or \
               (isWall(mapObjCopy, x, y+1) and isWall(mapObjCopy, x-1, y)) or \
               (isWall(mapObjCopy, x-1, y) and isWall(mapObjCopy, x, y-1)):
                map0bjCopy[x][y] = 'x'

            elif map0bjCopy[x][y] == ' ' and random.randint(0, 99) < OUTSIDE_DECORATION_PCT:
                 map0bjCopy[x][y] = random.choice(list(OUTSIDECOMAPPING.keys()))

    return map0bjCopy


def isBlocked(map0bj, gameState0bj, x, y):

    if isWall(map0bj, x, y):
        return True

    elif x < 0 or x >= len(map0bj) or y < 0 or y >= len(map0bj[x]):
        return True

    return False

def makeMove(map0bj, gameState0bj, playerMoveTo):
    playerx, playery = gameState0bj['player']

    stars = gameState0bj['stars']

    if playerMoveTo == UP:
        x0ffset = 0
        y0ffset = -1
    elif playerMoveTo == RIGHT:
        x0ffset = 1
        y0ffset = 0
    elif playerMoveTo == DOWN:
        x0ffset = 0
        y0ffset = 1
    elif playerMoveTo == LEFT:
        x0ffset = -1
        y0ffset = 0

    if isWall(map0bj, playerx, x0ffset, playery + y0ffset):
        return False
    else: 
        if (playerx + x0ffset, playery + y0ffset) in stars:
            if not isBlocked(map0bj, gameState0bj, playerx + (x0ffset*2), playery + (y0ffset*2)):
                ind = stars.index((playerx + x0ffset, playery + y0ffset))
                stars[ind] = (stars[ind][0] + x0ffset, stars[ind][1] + y0ffset)
            else:
                return False

        gameState0bj['player'] = (playerx + x0ffset, playery + y0ffset)
        return True


def startScreen():

    titleRect = IMAGESDICT['title'].get_rect()
    topCoord = 50
    titleRect.top = topCoord
    titleRect.centerx = HALF_WINWIDTH
    topCoord += titleRect.height

    instructionText = ['Push the stars over the marks.',
                       'Arrow keys move the player, WASD controls the camera, and P changes the characer.',
                       'Backspaced resets the level, Escape quits the game.',
                       'N for next level, B to go back a level.']

    DISPLAYSURF.fill(BGCOLOR)

    DISPLAYSURF.blit(IMAGESDICT['title'], titleRect)

    for i in range(len(instructionText)):
        instSurf = BASICFONT.render(instructionText[i], 1, TEXTCOLOR)
        instRect = instSurf.get_rect()
        topCoord += 10
        instRect.top = topCoord
        instRect.centerx = HALF_WINWIDTH
        topCoord += instRect.height
        DISPLAYSURF.blit(instSurf, instRect)

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if event.key ==K_ESCAPE:
                    terminate()
                return

        pygame.display.update()
        FPSCLOCK.tick()


def readLevelsFile(filename):
    assert os.path.exists(filename), 'Cannot find the level file: %s' % (filename)
    mapFile = open(filename, 'r')
    content = mapFile.readlines() + ['\r\n']
    mapFile.close()

    levels = []
    levelNum = 0
    mapTextLines = []
    map0bj = []
    for lineNum in range (len(content)):
        line = content[lineNum].rstrip('\r\n')

        if ';' in line:
            line = line[:line.find(';')]
        if line != '':
            mapTextLines.append(line)
        elif line == '' and len(mapTextLines) > 0:

            maxWidth = -1
            for i in range(len(mapTextLines)):
                if len(mapTextLines[i]) > maxWidth:
                    maxWidth = len(mapTextLines[i])

            for i in range(len(mapTextLines)):
                mapTextLines[i] += ' ' * (maxWidth - len(mapTextLines[i]))

            for x in range(len(mapTextLines[0])):
                map0bj.append([])
            for y in range(len(mapTextLines)):
                for x in range(maxWidth):
                    map0bj[x].append(mapTextLines[y][x])

            startx = None
            starty = None
            goals = []
            stars = []
            for x in range(maxWidth):
                for y in range (len(map0bj[x])):
                    if map0bj[x][y] in ('@', '+'):
                        startx = x
                        starty = y
                    if map0bj [x][y] in ('.', '+', '*'):
                        goals.append((x, y))    
                assert startx != None  and starty!= None, 'Level %s (around line %s) in %s is missing a "@" or "+" to mark the start point.' % (levelNum+1, lineNum, filename)
                assert len(goals) > 0, 'Level %s (around line %s) in %s must have at least one goal.' % (levelNum+1, lineNum, filename)
                assert len(stars) >= len(goals), 'Level %s (around line %s) in %s is impossible to solve. It has %s goals but only %s stars.' % (levelNum+1, lineNum, filename, len(goals), len(stars))

                gameState0bj = {'player': (startx, starty),
                                'stepCounter': 0,
                                'stars': stars}
                level0bj = {'width': maxWidth,
                            'height': len(map0bj),
                            'map0bj': map0bj,
                            'goals': goals,
                            'startState': gameState0bj}

                levels.append(level0bj)

                mapTextLines = []
                map0bj = []
                gameState0bj = {}
                levelNum += 1
    return levels


def floodFill(map0bj, x, y, oldCharacter, newCharacter):
    if map0bj[x][y] == oldCharacter:
        map0bj[x][y] = newCharacter

    if x < len(map0bj) - 1 and map0bj[x+1][y] == oldCharacter:
        floodFill(map0bj, x+1, y, oldCharacter, newCharacter)
    if x > 0 and map0bj[x-1][y] == oldCharacter:
        floodFill(mapObj, x-1, y, oldCharacter, newCharacter)
    if y < len(mapObj[x]) - 1 and mapObj[x][y+1] == oldCharacter:
        floodFill(mapObj, x, y+1, oldCharacter, newCharacter)
    if y > 0 and mapObj[x][y-1] == oldCharacter:
        floodFill(mapObj, x, y-1, oldCharacter, newCharacter)


def drawMap(map0bj, gamestate0bj, goals):
    mapSurfWidth = len(map0bj) * TILEWIDTH
    mapSurfHeight = len(len(map0bj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT
    mapSurf = pygame.Surface((mapSurfWidth, mapSurfHeight))
    mapSurf.fill(BGCOLOR)

    for x in range(len(map0bj)):
        for y in range(len(map0bj)):
            spaceRect = pygame.Rect((x * TILEWIDTH, y * (TILEHEIGHT - TILEFLOORHEIGHT), TILEWIDTH, TILEHEIGHT))
            if map0bj[x][y] in TILEMAPPING:
                baseTile = TILEMAPPING[map0bj[x][y]]
            elif map0bj[x][y] in OUTSIDECOMAPPING:
                baseTile = TILEMAPPING[' ']

            mapSurf.blit(baseTile, spaceRect)

            if map0bj[x][y] in OUTSIDECOMAPPING:
                mapSurf.blit(OUTSIDEDECOMAPPING[map0bj[x][y]], spaceRect)
            elif (x, y) in gameState0bj['stars']:
                if (x, y) in goals:
                    mapSurf.blit(IMAGESDICT['covered goal'], spaceRect)
                mapSurf.blit(IMAGESDICT['star'], spaceRect)
            elif (x, y) in goals:
                mapSurf.blit(IMAGESDICT['uncovered goal'], spaceRect)

            if (x, y) == gameState0bj['player']:
                mapSurf.blit(PLAYERIMAGES[currentImage], spaceRect)

    return mapSurf


def isLevelFinished(level0bj, gameState0bj):
    for goal in level0bj['goals']:
        if goal not in gameState0bj['stars']:
            return False
    return True


def terminate():
    pygame.quit()
    sys.exit()


if __name__ == '__main__':
    main()              
