import time, random, sets, math

import pygame
from pygame.locals import *

pygame.init()

# codes from the code tiles
CODE_GO = 1
CODE_LIT = 2
CODE_UNLIT = 3
CODE_GHOST = 4

# tiles to mod the map with
TILE_LIGHT_LIT = 16
TILE_LIGHT_UNLIT = 19

class Quit(Exception):
    pass

class Player:
    def __init__(self, pos):
        self.pos = pos
        self.move = [0, 0]
    def startMoveLeft(self):
        self.move[0] = -1
    def startMoveRight(self):
        self.move[0] = 1
    def startMoveUp(self):
        self.move[1] = -1
    def startMoveDown(self):
        self.move[1] = 1
    def stopMoveLeft(self):
        self.move[0] = 0
    stopMoveRight = stopMoveLeft
    def stopMoveUp(self):
        self.move[1] = 0
    stopMoveDown = stopMoveUp

    def animate(self, game):
        px, py = self.pos
        dx, dy = self.move
        if dx or dy:
            tx, ty = px + dx, py + dy
            game.movePlayerTo(tx, ty)

class Ghost:
    LOOK_LEFT = 0
    LOOK_UP = 1
    LOOK_RIGHT = 2
    LOOK_DOWN = 3

    MIN_LT_DIST = 8
    MIN_SHOOT_DIST = 10

    zapping = None
    recharging = 0

    def __init__(self, pos):
        self.pos = pos
        self.looking = self.LOOK_LEFT
    def getTile(self, game):
#        for pos, light in game.lights.items():
#            if light and calculateDistance(self.pos, pos) < self.MIN_LT_DIST:
#                # blue
#                return (2*16, 7*16, 16, 16)
        if self.recharging:
            return (2*16, 7*16, 16, 16)
        return (0, 7*16, 16, 16)
    def getEyesTile(self, player):
        x, y = self.pos
        px, py = player.pos
        ax = abs(x-px)
        ay = abs(y-py)
        if px <= x and ax >= ay: self.looking = self.LOOK_LEFT
        elif px >= x and ax >= ay: self.looking = self.LOOK_RIGHT
        elif py < y and ax < ay: self.looking = self.LOOK_UP
        elif py > y and ax < ay: self.looking = self.LOOK_DOWN
        return (self.looking*16, 6*16, 16, 16)
    def animate(self, game):
        self.zapping = None

        if self.recharging:
            self.recharging -= 1
            # can't do anything else
            return

        # ok, are we running away from any lights?
        ldist = {}
        for pos, light in game.lights.items():
            if not light: continue    # light not on
            dist = calculateDistance(self.pos, pos)
            if dist < self.MIN_LT_DIST:
                self.pos = findWorse(game.lightff, self.pos)
                return
            ldist[dist] = light

        # find something to zap
        l = ldist.items()
        l.sort()
        for dist, light in l:
            if light and dist < self.MIN_SHOOT_DIST:
                self.zapping = light.pos
                break
        else:
            self.pos = findBetter(game.lightff, self.pos)

class Light:
    def __init__(self, pos, damage=0):
        self.pos = pos
        self.damage = damage
        self.ok = damage == 0

    def __nonzero__(self):
        return self.ok

class ProgressDialog:
    light = None

    def __init__(self, game):
        text = game.font.render('Repairing...', 1, (100, 0, 0),
            (200, 200, 200))
        tx, ty = text.get_size()
        self.surf = pygame.Surface((tx+20, ty+50), HWSURFACE, game.screen)
        self.surf.fill((200, 200, 200))
        self.surf.blit(text, (10, 10))

        self.bwidth = tx
        self.btop = ty + 20

    def __nonzero__(self):
        return self.light is not None

    def fixLight(self, light, game):
        self.light = light
        x, y = light.pos
        sx, sy = x*16, y*16

    def stopFixingLight(self):
        self.light = None

    def animate(self, game):
        self.surf.fill((100, 100, 100), (10, self.btop, self.bwidth, 10))
        donewidth = int(self.bwidth * (self.light.damage / 50.0))
        self.surf.fill((255, 50, 50), (10, self.btop, donewidth, 10))
        self.light.damage -= 1
        if not self.light.damage:
            x, y = self.light.pos
            game.level[x][y][0] = TILE_LIGHT_LIT
            game.drawOneLevelElement(x, y)
            self.light.ok = True
            self.light = None
            game.genLightFlood()

class Game:
    pause = False
    show_fps = True
    step = 0
    expose = False

    def main(self):
        self.screen = pygame.display.set_mode((800, 600), DOUBLEBUF)
        #self.screen = pygame.display.set_mode((800, 600), DOUBLEBUF|FULLSCREEN)

        level = pygame.image.load('data/level.tga')
        self.level = pygame.surfarray.pixels3d(level)
        self.tiles = pygame.image.load('data/tiles.tga')
        self.palette = pygame.image.load('data/palette.tga')
        self.font = pygame.font.Font('data/MODENINE.TTF', 12)
        self.font_small = pygame.font.Font('data/MODENINE.TTF', 8)
        # cache rendered numbers
        self.text_nums = {}

        self.timer = pygame.time.Clock()

        self.renderLevel()

        # determine game starting state
        self.ghosts = []
        self.lights = {}
        for x, row in enumerate(self.level):
            for y, cell in enumerate(row):
                if cell[2] == CODE_GO:
                    self.player = Player((x, y))
                elif cell[2] == CODE_GHOST:
                    self.ghosts.append(Ghost((x,y)))
                elif cell[0] == TILE_LIGHT_UNLIT:
                    # initialise with some damage
                    self.lights[(x,y)] = Light((x,y), random.randint(20, 50))
                elif cell[0] == TILE_LIGHT_LIT:
                    self.lights[(x,y)] = Light((x,y))

        self.genLightFlood()

        self.progress = ProgressDialog(self)

        self.render()
        pygame.display.flip()

        while 1:
            val = self.timer.tick(15)
            if self.events():
                raise Quit

            # stepping
            if self.step > 1:
                continue
            if self.step:
                self.step = 2

            if self.animate():
                return False

            # check player victory
            for light in self.lights.values():
                if not light: break
            else: return True

            # check ghost victory
            for light in self.lights.values():
                if light: break
            else: return False

            self.render()
            self.renderProgress()
            if self.show_fps:
                self.renderFPS()
            pygame.display.flip()

    def genLightFlood(self):
        n = len(self.level[0])-1
        self.lightff = [[0]*n for row in self.level]
        for x, row in enumerate(self.level):
            for y, cell in enumerate(row):
                if cell[0] == TILE_LIGHT_LIT:
                    self.lightff[x][y] = 1
        floodFill(self.lightff)

    def animate(self):
        self.progress.stopFixingLight()
        for ghost in self.ghosts:
            ghost.animate(self)
        self.player.animate(self)

        regen_ff = False
        for ghost in self.ghosts:
            # check for player demise
            if ghost.pos == self.player.pos:
                return True

            # zapping ghosts?
            if ghost.zapping:
                x, y = ghost.zapping
                light = self.lights[ghost.zapping]
                light.damage += random.randint(1,5)
                ghost.recharging = 10
                if light.damage > 25 and random.random() > .5:
                    light.ok = False
                    self.level[x][y][0] = TILE_LIGHT_UNLIT
                    self.drawOneLevelElement(x, y)
                    regen_ff = True
        if regen_ff:
            self.genLightFlood()
                    

        # how many players does it take to fix a light?
        if self.progress:
            self.progress.animate(self)
        return False

    def render(self):
        self.screen.blit(self.drawn_level, (0,0))
        self.renderActors()
        self.renderLights()
        self.renderGhostSpecials()

    def renderProgress(self):
        if not self.progress:
            return
        px, py = self.progress.surf.get_size()
        sx, sy = self.screen.get_size()
        mx, my = self.progress.light.pos

        px2 = px/2
        py2 = py/2
        mx = (mx * 16) + 8 - px2
        my = (my * 16) + 8 - py2

        if mx < 0: mx = 0
        elif mx > (sx - px): mx = px
        if my < 0: my = 0
        elif my > (sy - py): my = py

        self.screen.blit(self.progress.surf, (mx, my))

    def renderLevel(self):
        self.drawn_level = pygame.Surface((800, 600), HWSURFACE, self.screen)
        self.drawn_level.fill((100,100,100))
        for x, row in enumerate(self.level):
            for y, cell in enumerate(row):
                self.drawOneLevelElement(x, y)

    def drawOneLevelElement(self, x, y):
        tile, back, code = self.level[x, y]
        tiley = (back/16) * 16
        tilex = (back%16) * 16
        self.drawn_level.blit(self.tiles, (x*16, y*16), (tilex, tiley, 16, 16))
        tiley = (tile/16) * 16
        tilex = (tile%16) * 16
        self.drawn_level.blit(self.tiles, (x*16, y*16), (tilex, tiley, 16, 16))

    def renderActors(self):
        for ghost in self.ghosts:
            x, y = ghost.pos
            self.screen.blit(self.tiles, (x*16, y*16), ghost.getTile(self))
        px, py = self.player.pos
        self.screen.blit(self.tiles, (px*16, py*16), (4*16, 0, 16, 16))

    def renderLights(self):
        lightmap = pygame.Surface((800, 576), HWSURFACE, self.screen)
        #lightmap = pygame.Surface((800, 600), SWSURFACE, 1)
        #print lightmap.get_palette()
        #lightmap.set_palette(((0,0,0), (255,255,255)))
        #lightmap.fill(0)
        #white = 1
        black = (0,0,0)
        white = (255,255,255)
        lightmap.fill(black)
        if self.expose:
            width = 1
        else:
            width = 0
        for x, row in enumerate(self.level):
            for y, cell in enumerate(row):
                if cell[0] == TILE_LIGHT_LIT:
                    pygame.draw.circle(lightmap, white, (x*16+8, y*16+8),
                        7*16, width)
                if cell[0] == TILE_LIGHT_UNLIT:
                    lightmap.fill(white, (x*16, y*16, 16, 16))
        px, py = self.player.pos
        pygame.draw.circle(lightmap, white, (px*16+8, py*16+8), 2*16, width)
        if self.expose:
            lightmap.set_colorkey(black, RLEACCEL)
        else:
            lightmap.set_colorkey(white, RLEACCEL)
        self.screen.blit(lightmap, (0,0))

    def renderGhostSpecials(self):
        for ghost in self.ghosts:
            x, y = ghost.pos
            x *= 16
            y *= 16
            self.screen.blit(self.tiles, (x, y),
                ghost.getEyesTile(self.player))
            if ghost.zapping:
                tx, ty = ghost.zapping
                pygame.draw.line(self.screen, (100,100,255), (x+8, y+8),
                    (tx*16+8, ty*16+8), 2)


    def renderFPS(self):
        fps = int(self.timer.get_fps())
        if fps not in self.text_nums:
            text = self.font.render(str(fps), 1, (100, 0, 0), (200, 200, 200))
            self.text_nums[fps] = text
        else:
            text = self.text_nums[fps]
        tx, ty = text.get_size()
        sx, sy = self.screen.get_size()
        self.screen.fill((0, 0, 0), (sx-tx, sy-ty, tx, ty))
        self.screen.blit(text, (sx-tx, sy-ty))

    def renderFlood(self):
        for i, row in enumerate(self.lightff):
            for j, num in enumerate(row):
                if num not in self.text_nums:
                    text = self.font.render(str(num), 1, (255, 0, 0))
                    self.text_nums[num] = text
                else:
                    text = self.text_nums[num]
                self.screen.blit(text, (i*16, j*16))

    def movePlayerTo(self, tx, ty):
        cell = self.level[tx][ty]
        if cell[2]:
            self.handleCode(tx, ty, cell[2])
        if not cell[0]:
            self.player.pos = (tx, ty)
        else:
            self.handleCell(tx, ty, cell[0])
        self.render()

    def handleCell(self, tx, ty, tile):
        if tile == TILE_LIGHT_UNLIT:
            self.progress.fixLight(self.lights[(tx, ty)], self)

    def handleCode(self, tx, ty, code):
        if code in (CODE_GO, CODE_GHOST):
            return
        else:
            raise ValueError, 'code %s not handled'%code

    def events(self):
        event = pygame.event.poll()
        while event.type != NOEVENT:
            if event.type == NOEVENT:
                pass
            elif event.type == QUIT:
                return True
            elif event.type == KEYDOWN:
                if self.keyDown(event.key, event.mod):
                    return True
            elif event.type == KEYUP:
                if self.keyUp(event.key):
                    return True
            event = pygame.event.poll()
        return False

    def keyUp(self, key):
        if key == K_LEFT:
            self.player.stopMoveLeft()
        elif key == K_RIGHT:
            self.player.stopMoveRight()
        elif key == K_UP:
            self.player.stopMoveUp()
        elif key == K_DOWN:
            self.player.stopMoveDown()

    def keyDown(self, key, mod):
        if key == K_p:
            self.pause = not self.pause
        elif key == K_f:
            self.show_fps = not self.show_fps
            self.render()
        elif key == K_s and mod & KMOD_SHIFT:
            if self.step: self.step = 0
            else: self.step = 1
        elif key == K_s:
            self.step = 1
        elif key == K_l:
            self.render()
            self.renderFlood()
            pygame.display.flip()
        elif key == K_e:
            self.expose = not self.expose
            self.render()
            pygame.display.flip()
        elif key == K_ESCAPE:
            return True
        elif key == K_LEFT:
            self.player.startMoveLeft()
        elif key == K_RIGHT:
            self.player.startMoveRight()
        elif key == K_UP:
            self.player.startMoveUp()
        elif key == K_DOWN:
            self.player.startMoveDown()
        return False

def floodFill(ff):
    left = True
    change = True
    while left and change:
        left = False
        change = False
        lff = len(ff)-1
        lrow = len(ff[0])-1
        js = sets.Set()
        for i, row in enumerate(ff):
            for j, elem in enumerate(row):
                if elem:
                    continue
                num = 9999
                if i > 0 and ff[i-1][j] and (i-1, j) not in js:
                    change = True
                    num = ff[i-1][j]+1
                if i < lff and ff[i+1][j] and (i+1, j) not in js:
                    change = True
                    num = min(num, ff[i+1][j]+1)
                if j > 0 and ff[i][j-1] and (i, j-1) not in js:
                    change = True
                    num = min(num, ff[i][j-1]+1)
                if j < lrow and ff[i][j+1] and (i, j+1) not in js:
                    change = True
                    num = min(num, ff[i][j+1]+1)
                if num == 9999:
                    left = True
                else:
                    row[j] = num
                    js.add((i, j))
    return ff

def findBetter(ff, pos):
    x, y = pos
    xp = x+1
    xm = x-1
    yp = y+1
    ym = y-1
    current = ff[x][y]
    choices = []
    if x > 0:
        if y > 0 and ff[xm][ym] <= current:
            choices.append((xm, ym))
        if ff[xm][y] <= current:
            choices.append((xm, y))
        if y < len(ff[0])-1 and ff[xm][yp] <= current:
            choices.append((xm, yp))
    if x < len(ff)-1:
        if y > 0 and ff[xp][ym] <= current:
            choices.append((xp, ym))
        if ff[xp][y] <= current:
            choices.append((xp, y))
        if y < len(ff[0])-1 and ff[xp][yp] <= current:
            choices.append((xp, yp))
    if y > 0 and ff[x][ym] <= current:
        choices.append((x, ym))
    if y < len(ff[0])-1 and ff[x][yp] <= current:
        choices.append((x, yp))
    if not choices: return pos # XXX stuck ghost
    return random.choice(choices)

def findWorse(ff, pos):
    x, y = pos
    xp = x+1
    xm = x-1
    yp = y+1
    ym = y-1
    current = ff[x][y]
    choices = []
    if x > 0:
        if y > 0 and ff[xm][ym] >= current:
            choices.append((xm, ym))
        if ff[xm][y] >= current:
            choices.append((xm, y))
        if y < len(ff[0])-1 and ff[xm][yp] >= current:
            choices.append((xm, yp))
    if x < len(ff)-1:
        if y > 0 and ff[xp][ym] >= current:
            choices.append((xp, ym))
        if ff[xp][y] >= current:
            choices.append((xp, y))
        if y < len(ff[0])-1 and ff[xp][yp] >= current:
            choices.append((xp, yp))
    if y > 0 and ff[x][ym] >= current:
        choices.append((x, ym))
    if y < len(ff[0])-1 and ff[x][yp] >= current:
        choices.append((x, yp))
    if not choices: return pos # XXX stuck ghost
    return random.choice(choices)

def calculateDistance(a, b):
    ax, ay = a; bx, by = b
    return math.sqrt((bx-ax)**2 + (by-ay)**2)

if __name__ == '__main__':
    try:
        win = Game().main()
    except Quit:
        pass

    if win: print 'you win!'
    else: print 'you lose!'

