class Game(object):

    modes = ['Waiting', 'In Play']

    def __init__(self, fps=60, fullscreen=False):
        """ (int, int, int, bool) -> Game
        Instantiate Game object with expected properties of a tower defense game. """
        pygame.init()
        
        # Parameter-based properties
        self.fps = fps
        self.fullscreen = fullscreen

        # Determine width and height
        self.screenW = pygame.display.list_modes()[0][0]
        self.screenH = pygame.display.list_modes()[0][1]
        if not self.fullscreen:
            self.screenW = floor(0.8 * self.screenW)
            self.screenH = floor(0.8 * self.screenH)

        # Display and framerate properties
        self.caption = GAME_NAME + ' - Left Mouse Button to select/place turret, drag by moving the mouse'
        self.displaySurf = None
        if self.fullscreen:
            self.flags = FULLSCREEN | DOUBLEBUF | HWACCEL
        else:
            self.flags = DOUBLEBUF | HWACCEL
        self.fpsClock = pygame.time.Clock()
        self.initializeDisplay()

        # Define and reset gameplay properties and objects
        self.money, self.wave, self.turrets, self.enemies, self.intersections, \
                    self.measuredFPS, self.tower, self.mode = [None] * 8
        self.reset()

        # HUD object 
        hudSize = 300
        hudRect = pygame.Rect(self.screenW - hudSize, 0, hudSize, self.screenH)
        hudColour = GREY
        self.hud = HUD(hudRect, hudColour, Turret(), FourShotTurret(), GlueTurret(),
                       FireTurret(), LongRange(), EightShotTurret())

        # Collision-related properties
        self.pathRects = []

        # Level appearance and elements
        self.intersectionSurface = pygame.Surface(self.displaySurf.get_size(), SRCALPHA | RLEACCEL, 32).convert_alpha()
        self.pathColour = ORANGE
        self.grassColour = LGREEN
        self.pathWidth = 50

        # Mouse and events
        self.mouseX, self.mouseY = 0, 0
        self.clicking = False
        self.dragging = False
        self.events = []

        # Health increment
        self.healthIncrement = 1

        # Menu object
        self.menu = Menu()

        # Background
        self.background = pygame.image.load(getDataFilepath(IMG_PATH_GRASS)).convert()
        self.pathImage = pygame.image.load(getDataFilepath(IMG_PATH_DIRT)).convert()

        # Sounds
        self.backgroundMusic = Sound(getDataFilepath('retro.wav'), True)
        self.hitSound = Sound(SOUND_PATH_SHOT, False)

        self.inPlay = True

    def reset(self, money=200, wave=1):
        """ ([int], [int]) -> None
        Reset money, wave number, and other similar game world properties. """
        self.money = money
        self.wave = wave
        self.turrets = []
        self.intersections = []
        self.enemies = []
        self.tower = Tower(IMG_PATH_TOWER, self.screenW / 2, self.screenH / 2)
        self.mode = self.modes[0]

    def incrementEnemyHealth(self, increment):
        for enemy in self.enemies:
            enemy.health *= increment

    def generateEnemies(self, x=1, separation=70):
        """ ([int], [int]) -> None
        Generate "x" number of enemies with the given separation for the tower defense game. """

        # Return immediately if there are no intersections loaded.
        if not self.intersections:
            print('WARNING: Enemies not loaded! Intersections must be loaded first.')
            return

        # Clear the list of enemies to start with.
        self.enemies = []

        # Gather information and create temporary variables.
        firstTurnX = self.intersections[0][0]
        firstTurnY = self.intersections[0][1]
        secondTurnX = self.intersections[1][0]
        secondTurnY = self.intersections[1][1]
        gap = x * separation
        xlist = []
        ylist = []
        direction = NODIR

        # Determine the starting direction and co-ordinate lists for the enemies.
        if firstTurnX == secondTurnX and firstTurnY > secondTurnY:
            xlist = [firstTurnX]
            ylist = xrange(firstTurnY, firstTurnY + gap, separation)
            direction = UP
        elif firstTurnX == secondTurnX:
            xlist = [firstTurnX]
            ylist = xrange(firstTurnY - gap, firstTurnY, separation)
            direction = DOWN
        elif firstTurnY == secondTurnY and firstTurnX > secondTurnX:
            xlist = xrange(firstTurnX, firstTurnX + gap, separation)
            ylist = [firstTurnY]
            direction = LEFT
        elif firstTurnY == secondTurnY:
            xlist = xrange(firstTurnX - gap, firstTurnX, separation)
            ylist = [firstTurnY]
            direction = RIGHT

        # Create enemies with the information determined above.
        w = Enemy(IMG_PATH_ENEMY1, 0, 0).w
        h = Enemy(IMG_PATH_ENEMY1, 0, 0).h
        assigned = False
        for x in xlist:
            for y in ylist:
                enemyType = random.randint(1, 5)
                if enemyType == 2 and not assigned:
                    self.enemies.append(Enemy(IMG_PATH_ENEMY2, x - w // 2, y - h // 2, direction, 3, 200))
                    assigned = True
                elif enemyType == 3 and not assigned and self.wave >= 2:
                    self.enemies.append(Enemy(IMG_PATH_ENEMY3, x - w // 2, y - h // 2, direction, 2, 300))
                    assigned = True
                elif enemyType == 4 and not assigned and self.wave >= 2:
                    self.enemies.append(Plane(IMG_PATH_ENEMY4, x - w // 2, y - h // 2, direction, 6, 100))
                    assigned = True
                elif enemyType == 5 and not assigned and self.wave >= 10:
                    self.enemies.append(HugeTank(IMG_PATH_ENEMY5, x - w // 2, y - h // 2, direction, 2, 500))
                    assigned = True
                else:
                    self.enemies.append(Enemy(IMG_PATH_ENEMY1, x - w // 2, y - h // 2, direction, health=100))
                    assigned = True
                self.enemies[-1].setNextIntersection(self.intersections[0])
                self.enemies[-1].initialDistanceFromWorld = distance((self.enemies[-1].x, self.enemies[-1].y),
                                                                     (firstTurnX, firstTurnY))
                assigned = False

        if self.wave % 5 == 0:
            self.healthIncrement += 1
        self.incrementEnemyHealth(self.healthIncrement)
        # Sort the list of enemies in terms of ascending distance away from the initial intersection.
        self.enemies.sort(key=lambda x: x.initialDistanceFromWorld)

    def makeStrSubstitutions(self, string):
        """ (str) -> str
        Return the input string but with human-readable keywords
        exchanged for co-ordinates and directions. """
        substitutions = {'RIGHT': RIGHT, 'LEFT': LEFT, 'UP': UP, 'DOWN': DOWN, 'WIDTH': self.hud.left,
                         'HEIGHT': self.screenH}
        result = string[:]
        for word in string.split():
            if word in substitutions:
                result = result.replace(word, str(substitutions[word]))
        return result

    def stretchIntersections(self):
        """ (None) -> None
        Stretch or compress intersection co-ordinates as necessary to fit them to screen. """
        # Return immediately if there are no intersections
        if not self.intersections:
            return
        # Gather info about the needed scaling for horizontal and vertical co-ordinates of each intersection
        temp = self.intersections[:]
        temp.sort(key=lambda x: x[0])
        horizontalStretch = (self.screenW - self.hud.width) / float(temp[-1][0] + self.pathWidth // 2)
        temp = self.intersections[:]
        temp.sort(key=lambda x: x[1])
        verticalStretch = self.screenH / float(temp[-1][1] + self.pathWidth)
        # Make it happen and leave the intersection direction intact
        for i in xrange(len(self.intersections)):
            self.intersections[i] = ceil(self.intersections[i][0] * horizontalStretch), \
                                    ceil(self.intersections[i][1] * verticalStretch), self.intersections[i][2]
        self.tower.x *= horizontalStretch
        self.tower.y *= verticalStretch

    def loadIntersections(self, filename):
        """ (None) -> tuple
        Load the saved intersections from file based on the current wave.
        Return the loaded intersection 3-tuples. """
        self.intersections = []
        data = open(getDataFilepath(filename), 'r')
        for line in data:
            intersection = self.makeStrSubstitutions(line).split()
            intersection = int(intersection[0]), int(intersection[1]), int(intersection[2])
            self.intersections.append(intersection)
        self.stretchIntersections()
        return self.intersections

    def loadTowerLoc(self, filename):
        """ (None) -> None
        Load the co-ordinates of the tower to defend. """
        data = open(getDataFilepath(filename), 'r').read().split()
        x = int(self.makeStrSubstitutions(data[-3]))
        y = int(self.makeStrSubstitutions(data[-2]))
        self.tower.x, self.tower.y = x - self.tower.w // 2, y - self.tower.h // 2
        newRect = self.tower.getRect().clamp(pygame.Rect(0, 0, self.screenW - self.hud.width, self.screenH))
        self.tower.x = newRect.x
        self.tower.y = newRect.y

    def incrementWave(self):
        """ (None) -> None
        Set up the level for the next wave. """
        self.wave += 1
        self.generateEnemies(4 * self.wave)
        for turret in self.turrets:
            turret.bullets = []
            turret.angle = 0

    def drawText(self, text, x=0, y=0):
        """ (str, [int], [int]) -> None
        Draw the given string such that the text matches up with the given top-left co-ordinates.
        Acts as a wrapper for the HUD drawText(). """
        self.hud.drawText(self.displaySurf, text=text, left=x, top=y)

    def handleAI(self):
        """ (None) -> None
        Force the enemies to turn at each intersection. """
        if not self.enemies or not self.intersections:
            return
        for enemy in self.enemies:
            nextTurn = enemy.getNextIntersection()            
            if not enemy.getReducedRect().collidepoint(nextTurn[0:2]):
                continue
            if nextTurn[-1] == LEFT: enemy.startMovingLeft()   
            elif nextTurn[-1] == RIGHT: enemy.startMovingRight()
            elif nextTurn[-1] == UP: enemy.startMovingUp()
            elif nextTurn[-1] == DOWN: enemy.startMovingDown()
            else: enemy.stop()
            intersectionIndex = self.intersections.index(nextTurn)  
            if intersectionIndex + 1 < len(self.intersections):  
                enemy.setNextIntersection(self.intersections[intersectionIndex + 1])
                
    def drawIntersections(self, surface):
        """ (Surface) -> None
        Draw a sequence of paths joining all of the intersections onto the given surface.
        Update the list of path Rect objects for collision detection. """
        if not self.intersections:
            return
        points, intersectionRects, joinRects, result = [], [], [], []
        half = floor(self.pathWidth / 2.0)
        for intersection in self.intersections:
            points.append((intersection[0], intersection[1]))
        for point in points:
            intersectionRects.append(pygame.Rect(point[0] - half, point[1] - half, 2 * half, 2 * half))
        for i in xrange(len(points) - 1):
            result.append(intersectionRects[i].union(intersectionRects[i + 1]))
            surface.blit(self.pathImage, (result[-1].x, result[-1].y), result[-1])
        self.pathRects = result

    def onPath(self, other):
        """ (int, int) -> bool
        Return True if the x and y co-ordinates represent a spot on the paths, False otherwise. """
        result = False
        if not self.pathRects:
            return result
        for rect in self.pathRects:
            if (isinstance(other, tuple) and rect.collidepoint(other)) or rect.colliderect(other):
                result = True
        return result

    def onGrass(self, other):
        """ (int, int) -> bool
        Return True if the x and y co-ordinates represent a spot on the grass, False otherwise. """
        return not self.onPath(other)

    def hoveringSidebar(self):
        """ (None) -> bool
        Return True if the mouse is hovering over the HUD sidebar, False otherwise. """
        return self.mouseX >= self.hud.left
        
    def initializeDisplay(self):
        """ (None) -> Surface
        Initialize the display Surface and update the caption and display settings. Only call once in __init__. """
        self.displaySurf = pygame.display.set_mode((self.screenW, self.screenH), self.flags)
        pygame.display.set_caption(self.caption)
        return self.displaySurf

    def redrawAndProceedTick(self):
        """ (None) -> None
        Redraw the screen, and delay to enforce the FPS. Call on every update. """
        pygame.display.flip()
        self.fpsClock.tick_busy_loop(self.fps)
        self.measuredFPS = self.fpsClock.get_fps()

    def terminate(self):
        """ (None) -> None
        Set the game to exit as soon as possible. """
        print('Game closing...')
        self.inPlay = False

    def handleQuitEvents(self, events):
        """ (list-of-Events) -> None
        Exit the game if Escape is pressed or if the close button is used. """
        for event in events:
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
                self.terminate()

    def updateState(self):
        """ (None) -> None
        Update the state of the game based on the user selection on the sidebar. """
        if self.hud.shouldPause():
            self.backgroundMusic.pause()
            if self.menu.drawPauseMenu(self.displaySurf):
                self.__init__(self.fps, self.fullscreen)
                self.execute()
            self.menu.drawPauseMenu(self.displaySurf)
            self.backgroundMusic.play(-1)
        if self.hud.shouldStart():
            self.mode = self.modes[1]
        self.canRun()

    def updateEnemies(self):
        """ (None) -> None
        Update and draw all enemies and the central tower. """
        levelComplete = True
        for enemy in self.enemies:
            if self.mode == self.modes[0]:
                enemy.pause()
            elif self.mode == self.modes[1]:
                enemy.unpause()
            if enemy.alive():
                levelComplete = False
            enemy.update()

            self.tower.update(self.displaySurf, enemy)
            enemy.draw(self.displaySurf, enemy.x, enemy.y)
        if levelComplete and self.mode == self.modes[1]:
            self.mode = self.modes[0]
            self.incrementWave()

    def enemyIndex(self, enemy):
        try:
            return self.enemies.index(enemy)
        except IndexError:
            print('WARNING: Tried to access nonexistent enemy.')
            return 0

    @staticmethod
    def inRange(enemy, turret):
        """ (Enemy, Turret) -> None
        Return True if the enemy is in range of the given turret, False
        otherwise. """
        return distance(enemy.getRect().center, turret.getRect().center) < turret.range and enemy.active

    def setTarget(self, enemy, turret):
        """ (Enemy, Turret) -> None
        Lock onto a new enemy with the given turret. """
        if not isinstance(turret, FourShotTurret) and turret.canShoot:
            turret.angle = getAngle(deltaX(enemy.x, turret.x), deltaY(enemy.y, turret.y))
        turret.lockOn = True

    def provideReward(self, turret):
        """ (None, Turret) -> None
        Provide the player with a reward for each kill. """
        self.money += turret.reward

    def updateTurrets(self):
        """ (None) -> None
        Update and draw all turrets and bullets. """
        for turret in self.turrets:
            # Check if the turret is highlighted
            turret.highlighted = False
            if turret.getRect().collidepoint(self.mouseX, self.mouseY):
                turret.highlighted = True
                
            # Check for lock-on with enemies
            foundTarget = False
            for enemy in self.enemies:
                if self.inRange(enemy, turret):
                    self.setTarget(enemy, turret)
                    foundTarget = True
                    break
            if not foundTarget:
                turret.lockOn = False
                turret.bullets = []
                
            # Update and draw the turret
            turret.update(self.displaySurf)
            
            # Check for bullet collision with enemies
            for bullet in turret.bullets:
                for enemy in self.enemies:
                    bulletEnemyCollision = bullet.getRect().colliderect(enemy.getRect())
                    if bulletEnemyCollision and not isinstance(turret, FourShotTurret):
                        self.hitSound.play()
                        bullet.dispose()
                        turret.test = True
                        if not isinstance(turret, GlueTurret) or \
                                (isinstance(enemy, Plane) and not isinstance(turret, FireTurret)):
                            enemy.health -= bullet.damagePotential
                        else:
                            enemy.topSpeed *= bullet.slowFactor
                        enemy.dispose()
                        if enemy.health <= 0:
                            self.provideReward(turret)             
                    elif bulletEnemyCollision:
                        self.hitSound.play()
                        enemy.health -= bullet.damagePotential
                        enemy.dispose()
                        bullet.dispose()
                        if enemy.health <= 0:
                            self.provideReward(turret)

    def updateHud(self):
        """ (None) -> None
        Update and draw the HUD sidebar. """
        self.hud.update(self.tower.health, self.money, self.wave)
        self.hud.draw(self.displaySurf)

    def updateInputs(self):
        """ (None) -> None
        Get keyboard and mouse status and check for quit events. """
        self.mouseX, self.mouseY = pygame.mouse.get_pos()
        self.clicking = pygame.mouse.get_pressed()[0]
        self.events = pygame.event.get()
        self.handleQuitEvents(self.events)

    def addTurret(self, index):
        """ (int) -> None
        Add a turret with the given index to the list of turrets. """
        newTurret = copy.copy(self.hud.turrets[index])
        newTurret.x = DEFAULT_VALUE
        newTurret.y = DEFAULT_VALUE
        self.turrets.append(newTurret)

    def handleDragging(self):
        """ (None) -> None
        Facilitate dragging of turrets from the HUD sidebar to the game field. """
        overlapping = False
        index = self.hud.highlighted()
        clicked = False
        rects = [turret.getRect() for turret in self.turrets[0:-1]]
        if len(self.turrets) > 1:
            for rect in rects:
                if self.turrets[-1].getRect().colliderect(rect):
                    overlapping = True
        for event in self.events:
            if event.type == MOUSEBUTTONDOWN:
                clicked = True
        if self.dragging and clicked and self.onGrass(self.turrets[-1].getRect()) and not overlapping:
            self.dragging = False
            self.turrets[-1].canShoot = True
            self.money -= self.turrets[-1].price
        if index >= 0 and clicked and not self.dragging and self.money >= self.hud.turrets[index].price:
            self.dragging = True
            self.addTurret(index)
        if self.dragging and not clicked:
            self.turrets[-1].x = self.mouseX - self.turrets[-1].width // 2
            self.turrets[-1].y = self.mouseY - self.turrets[-1].height // 2
            self.turrets[-1].canShoot = False

    def update(self):
        """ (None) -> None
        Update the entire game state and draws all objects on the screen. """
        self.displaySurf.blit(self.background, ORIGIN)
        self.updateInputs()
        self.handleAI()
        self.updateEnemies()
        self.updateTurrets()
        self.updateHud()
        self.handleDragging()
        self.redrawAndProceedTick()
        self.updateState()

    def execute(self):
        """ (None) -> None
        Execute the Tower Defense game. """

        # Play background music and enter the title screen
        self.backgroundMusic.play(-1)
        self.menu.drawTitleMenu(self.displaySurf)
        filename = self.menu.drawSelectLevelScreen(self.displaySurf)

        # Load the first level properties
        self.loadTowerLoc(filename)
        self.loadIntersections(filename)
        self.generateEnemies(5)

        # Blit the tower and paths onto the background Surface
        self.drawIntersections(self.intersectionSurface)
        self.background.blit(self.intersectionSurface, ORIGIN)
        self.tower.draw(self.background)

        # Play!
        while self.inPlay:
            self.update()
        self.menu.drawLosePanel (self.displaySurf)
        self.menu.drawCredits(self.displaySurf)
        pygame.quit()

    def getRect(self):
        """ (None) -> Rect
        Return a pygame Rect object defining the display surface boundaries. """
        return self.displaySurf.get_rect()

    def canRun (self):
        if self.tower.health <= 0:
            self.terminate ()