import psp2d, pspsnd
import random
import copy
import math
import level
import time

sndMenu = pspsnd.Sound("snd/menu.wav")
sndCancel = pspsnd.Sound("snd/cancel.wav")
sndDestroy_group = pspsnd.Sound("snd/destroy_group.wav")
sndLose = pspsnd.Sound("snd/lose.wav")
sndApplause = pspsnd.Sound("snd/applause.wav")
sndLaunch = pspsnd.Sound("snd/launch.wav")

## Image Loading.
font = psp2d.Font("gfx/font.png")
font_info_level = psp2d.Font("gfx/font_info_level.png")
intro = psp2d.Image("gfx/intro.png")
pressStartImg = psp2d.Image("gfx/press_start.png")
pause = psp2d.Image("gfx/pause.png")

chooseLevelSetImg = psp2d.Image("gfx/chooseLevelSet.png")
freccia_s = psp2d.Image("gfx/freccia_s.png")
freccia_d = psp2d.Image("gfx/freccia_d.png")

menuImg = [psp2d.Image("gfx/menu_1.png"),\
           psp2d.Image("gfx/menu_2.png"),\
           psp2d.Image("gfx/menu_3.png")]
menuImgSel = [psp2d.Image("gfx/menu_1_sel.png"),\
           psp2d.Image("gfx/menu_2_sel.png"),\
           psp2d.Image("gfx/menu_3_sel.png")]

bg = psp2d.Image("gfx/bg.png")
wall = psp2d.Image("gfx/wall.png")
wall1 = psp2d.Image("gfx/wall1.png")

mascotte = [psp2d.Image("gfx/mascotte/%d.png" %x) for x in range(1, 7)]

image = []
for i in enumerate(['blue','red','orange','pink','black','grey','green','yellow']):
    image.append(psp2d.Image("gfx/%s_ball.png" %i[1]))
balls_effect = [psp2d.Image("gfx/balls_effect/%d.png" %x) for x in range(8)]

throwerImg = []
for i in xrange(0, 180):
    throwerImg.append(psp2d.Image("gfx/thrower_%s.png"%str(i)))

loseImg = psp2d.Image("gfx/lose.png")
##################

screen = psp2d.Screen()

class Game:
        def __init__(self, levelSet, nLevel, maxLevel):
            self.curLevel = nLevel
            self.maxLevel = maxLevel
            self.levelSet = levelSet
            level.initialize(self.levelSet)
            self.board = Board([20/1.5+40, 60/1.5-15], self.curLevel)
            self.keys = [False, False, False]
            self.pad = None
            self.returnToMainMenu = False
            self.pause = False
            self.timerInfoLev = 0
                
        def run(self, pad, oldpad):
            self.pad = pad
            self.oldPad = oldpad

            self.board.render()
            
            if not self.board.lose and not self.pause:
                self.update()
                if self.pad.start:
                    time.sleep(0.2)
                    self.pause = True
            elif self.board.lose:
                screen.blit(loseImg, 0, 0, 480, 272, 0, 0, True)
                if self.pad.start:
                    self.keys[2] = False
                    self.board = Board([20/1.5+40, 60/1.5-15], self.curLevel)
                elif self.pad.select:
                    time.sleep(1)
                    self.returnToMainMenu = True
            elif self.pause:
                screen.blit(pause, 0, 0, 480, 272, 0, 0, True)
                if self.pad.start:
                    time.sleep(0.2)
                    self.pause = False
                
        def update(self):
            self.runActions()
            self.board.updateAnimation()
            if self.timerInfoLev < 50:
                if self.levelSet != 'random': font_info_level.drawText(screen, 90, 170, "LEVEL %d" %self.curLevel)
                else: font_info_level.drawText(screen, 90 -25, 170, "RANDOM LEVEL")
                self.timerInfoLev += 1
            if len(self.board.balls) == 0 and not self.board.inAnimation:
                if self.curLevel != self.maxLevel:
                    sndApplause.start()
                    self.curLevel += 1
                    self.board = Board([20/1.5+40, 60/1.5-15], self.curLevel)
                    self.timerInfoLev = 0
                    time.sleep(0.2)
                else:
                    self.returnToMainMenu = True
                    time.sleep(0.5)
                
        def runActions(self):
            if self.keys[0]:
                self.board.incAngle()
                self.keys[0] = False
            if self.keys[1]:
                self.board.decAngle()
                self.keys[1] = False
            if self.keys[2]:
                self.board.throwBall()
                self.keys[2] = False
                sndLaunch.start()
            if self.pad.left:
                        self.keys[0] = True
                        if not self.board.inAnimation: self.board.downTimer += 1
            elif self.pad.right:
                        self.keys[1] = True
                        if not self.board.inAnimation: self.board.downTimer += 1
            elif self.pad.cross and not self.oldPad.cross and not self.board.inAnimation:
                        self.keys[2] = True
            elif not self.board.inAnimation:
                    self.board.downTimer += 4

class Board(object):
    def __init__(self, pos, lvl):
        self.pos = pos
        self.size = [240/1.5, 350/1.5]
        self.nextBallPos = [[27/1.5, 324/1.5][0] + pos[0], [27/1.5, 324/1.5][1] + pos[1]]
        throwerPos = [[70/1.5, 264/1.5][0] + pos[0], [70/1.5, 264/1.5][1] + pos[1] ]
        self.thrower = Thrower(self.pos)
        self.balls = []
        self.throwAngle = 0
        self.thrownBall = None
        self.isTop8BallLine = True
        self.ballMatrix = [[None for i in range([12, 8][1]+2)] for i in range([12, 8][0])]
        self.animations = []
        self.inAnimation = False
        self.lose = False
        self.downTimer = 0
        self.mascotteTim = 0
        self.offsetY = 0
        self.ball_with_effect = False
        self.perScendere = False
 
        self.nLevel = lvl
        for i in level.ball_position[self.nLevel-1]:
            b = Ball(color = i[2])
            b.pos = [i[0], i[1]]
            self.addToBallMatrixWithoutCanc(b)
            self.balls.append(b)

        colors = []
        for ball in self.balls:
            if not ball.color in colors:
                colors.append(ball.color)
        self.ball = Ball(color = random.choice(colors))
        self.ball.setCenterPos(self.thrower.getCenterPos())
        self.nextBall = Ball(color = random.choice(colors))
        self.nextBall.setCenterPos(self.nextBallPos)
                
    def addToBallMatrixWithoutCanc(self, ball):
        ## To call when you wanna create a new level.
        pos = ball.getPosInBoard(self.pos, self.isTop8BallLine)
        self.ballMatrix[pos[0]][pos[1]] = ball
        looseBalls = self.getLooseBalls()
        self.removeFromBallMatrix(looseBalls)
        
    def addToBallMatrix(self, ball):
        pos = ball.getPosInBoard(self.pos, self.isTop8BallLine)
        self.ballMatrix[pos[0]][pos[1]] = ball
        sameColor = self.getBallsWithSameColor(ball.color, pos, [])
        if pos[0] == 10 and len(sameColor) < 3:
            sndLose.start()
            self.lose = True
            return
        if len(sameColor) >= 3:
            self.removeFromBallMatrix(sameColor)
        looseBalls = self.getLooseBalls()
        self.removeFromBallMatrix(looseBalls)
        if len(sameColor) >= 3:
            sndDestroy_group.start()
            sameColor.extend(looseBalls)
            self.animations.append(ballAnimation(sameColor))
            self.inAnimation = True

    def getLooseBalls(self):
        visitedBallsMatrix = [[False for i in range([11, 8][1]+2)] for i in range([11, 8][0])]
        for j in xrange([11, 8][1]):
            ball = self.ballMatrix[0][j]
            if ball:
                self.visitBallMatrix(visitedBallsMatrix, [0, j])

        looseBalls = []
        for ball in self.balls:
            boardPos = ball.getPosInBoard(self.pos, self.isTop8BallLine)
            if not visitedBallsMatrix[boardPos[0]][boardPos[1]]:
                looseBalls.append(ball)

        return looseBalls

    def visitBallMatrix(self, visitedBallMatrix, pos):
        for i in xrange(2):
            if pos[i] < 0 or pos[i] > [11, 8][i]:
                return

        pi = int(pos[0])
        pj = int(pos[1])
        ball = self.ballMatrix[pi][pj]
        if not ball or visitedBallMatrix[pi][pj]:
             return 

        visitedBallMatrix[pi][pj] = True
        if (self.isTop8BallLine and pi % 2 == 0) or (not self.isTop8BallLine and pi % 2 == 1):
            for i in xrange(-1, 2):
                for j in xrange(-1, 2):
                    if (i,j) != (0,0) and (i,j) != (-1,-1) and (i,j) != (+1, -1):
                        self.visitBallMatrix(visitedBallMatrix, [pi+i, pj+j])
        else:
            for i in xrange(-1, 2):
                for j in xrange(-1, 2):
                    if (i,j) != (0,0) and (i,j) != (-1,+1) and (i,j) != (+1, +1):
                        self.visitBallMatrix(visitedBallMatrix, [pi+i, pj+j])


    def removeFromBallMatrix(self, balls):
        for ball in balls:
            pos = ball.getPosInBoard(self.pos, self.isTop8BallLine)
            self.ballMatrix[pos[0]][pos[1]] = None
            try:
                self.balls.remove(ball)
            except:
                pass

    def getBallsWithSameColor(self, color, pos, ballsFound=[]):
        for i in xrange(2):
            if pos[i] < 0 or pos[i] > [11, 8][i]:
                return []

        pi = int(pos[0])
        pj = int(pos[1])
        ball = self.ballMatrix[pi][pj]
        if not ball or color != ball.color or ball in ballsFound:
            return []

        ballsFound.append(ball)
        if (self.isTop8BallLine and pi % 2 == 0) or (not self.isTop8BallLine and pi % 2 == 1):
            for i in xrange(-1, 2):
                for j in xrange(-1, 2):
                    if (i,j) != (0,0) and (i,j) != (-1,-1) and (i,j) != (+1, -1):
                        self.getBallsWithSameColor(color, [pi+i, pj+j], ballsFound)
        else:
            for i in xrange(-1, 2):
                for j in xrange(-1, 2):
                    if (i,j) != (0,0) and (i,j) != (-1,+1) and (i,j) != (+1, +1):
                        self.getBallsWithSameColor(color, [pi+i, pj+j], ballsFound)

        return ballsFound

    def updateAnimation(self):
        if self.downTimer > 2500 and not self.thrownBall and not self.perScendere: # discesa parete
            self.downTimer = 0
            self.initializeBallsEffect()
            self.perScendere = True
            self.ball_with_effect = True
        elif self.downTimer > 2800 and self.perScendere: # e' passato troppo tempo, la parete scende e "spara" la palla
            self.throwBall()
            self.downTimer = 0
            self.initializeBallsEffect()
            self.perScendere = True
            self.ball_with_effect = True
            
        if len(self.animations) != 0:
            for anim in self.animations:
                if anim.isFinished():
                    self.inAnimation = False
                    self.animations.remove(anim)
                else:
                    anim.update()
                               
        if self.thrownBall:
            ball = self.thrownBall
            if len(self.balls) < 40:
                ball.pos[0] = ball.pos[0] + math.cos(self.throwAngle)*5
                ball.pos[1] = ball.pos[1] - math.sin(self.throwAngle)*5
            else:
                ball.pos[0] = ball.pos[0] + math.cos(self.throwAngle)*7
                ball.pos[1] = ball.pos[1] - math.sin(self.throwAngle)*7

            if ball.pos[0]  <= self.pos[0]:
                ball.pos[0] = self.pos[0] + 1
                self.throwAngle  = math.pi - self.throwAngle
            if ball.pos[0] >= self.pos[0] -1  + self.size[0] - 2 * ball.radius:
                self.throwAngle  = math.pi - self.throwAngle

            for b in self.balls:
                if ball.collides(b):
                    ball.roundPosAroundBall(b)
                    self.thrownBall = None
                    self.balls.append(ball)
                    if ball.pos[1] > 195+21:
                        sndLose.start()
                        self.lose = True
                        self.throwAngle = 0
                        ball = None
                        time.sleep(0.5)
                    else:
                        self.addToBallMatrix(ball)
                        self.throwAngle = 0
                        ball = None
                    if self.perScendere:
                        self.perScendere = False
                        self.pos[1] += 21
                        self.offsetY += 1
                        for i in range(len(self.balls)): 
                            if self.balls[i].pos[1] > 195-21:
                                sndLose.start()
                                self.lose = True 
                            self.balls[i].pos[1] += 21
                    break

            if ball and ball.pos[1] <= self.pos[1]:
                            ball.roundPosToBoardTop(self.pos, self.isTop8BallLine)
                            self.thrownBall = None
                            self.balls.append(ball)
                            self.addToBallMatrix(ball)
                            self.throwAngle = 0

    def drawMascotte(self):
        self.mascotteTim += 1
        TIMER = 140
        OFFSET = 7
        if self.lose:
            screen.blit(mascotte[5], 0, 0, 31, 32, 94-(47-31), 226-(48-32), True)
        if self.mascotteTim < TIMER:
            screen.blit(mascotte[0], 0, 0, 31, 32, 94, 226, True)
        elif self.mascotteTim >= TIMER and self.mascotteTim < TIMER+OFFSET:
                screen.blit(mascotte[1], 0, 0, 31, 32, 94, 226, True)
        elif self.mascotteTim >= TIMER+OFFSET and self.mascotteTim < TIMER+(OFFSET*2):
                screen.blit(mascotte[2], 0, 0, 31, 32, 94, 226, True)
        elif self.mascotteTim >= TIMER+(OFFSET*2) and self.mascotteTim < TIMER+(OFFSET*3):
                screen.blit(mascotte[3], 0, 0, 31, 32, 94, 226, True)
        elif self.mascotteTim >= TIMER+(OFFSET*3) and self.mascotteTim < TIMER+(OFFSET*4):
                screen.blit(mascotte[4], 0, 0, 31, 32, 94, 226, True)
        else:
            screen.blit(mascotte[0], 0, 0, 31, 32, 94, 226, True)
            self.mascotteTim = 0

    def initializeBallsEffect(self):
        for ball in self.balls:
            ball.nEffect = 0
        
    def render(self):
        self.thrower.render()
        self.drawMascotte()
        self.nextBall.render()
        self.ball.render()
        if self.thrownBall:
            self.thrownBall.render()
        for ball in self.balls:
            if self.perScendere:
                ball.render(self.ball_with_effect)
            else:
                ball.render()
        for anim in self.animations:
	    anim.render()
	if self.offsetY != 0:
            for i in range(self.offsetY):
                if i != self.offsetY - 1:
                    screen.blit(wall1, 0, 0, 168, 21, 53, 24+(i*21), True)
                else:
                    screen.blit(wall, 0, 0, 168, 21, 53, 24+(i*21), True)
	if self.nLevel != 'random': font.drawText(screen, 330, 122, 'Level: %s' %str(self.nLevel))
	else: font.drawText(screen, 330 -20, 122, 'Level: %s' %str(self.nLevel))

    def incAngle(self):
        self.thrower.incAngle()

    def decAngle(self):
        self.thrower.decAngle()

    def throwBall(self):
        if self.thrownBall:
            return
        self.throwAngle = self.thrower.angle
        self.thrownBall = self.ball
        self.ball = self.nextBall
        self.ball.setCenterPos(self.thrower.getCenterPos())
        colors = []
        for ball in self.balls:
            if not ball.color in colors:
                colors.append(ball.color)
        self.nextBall = Ball(color = random.choice(colors))
        self.nextBall.setCenterPos(self.nextBallPos)
        
class Ball(object):
    def __init__(self, pos = [0,0], color = None):
        if color == None:
                self.color = random.randint(0, len(image)-1)
        else:
                self.color = color
        self.ballImage = image[self.color]
        self.radius = 7*1.5
        self.pos = copy.copy(pos)
        self.boardPos = [-1, -1]
        self.nEffect = 0
        
    def setCenterPos(self, pos):
        for a in xrange(2):
            self.pos[a] = pos[a] - self.radius

    def centerDistance(self, ball):
        a = self.pos[0] - ball.pos[0]
        b = self.pos[1] - ball.pos[1]
        return math.sqrt(a**2 + b**2)

    def collides(self, ball):
        return self.centerDistance(ball) <  self.radius + self.radius - 4

    def render(self, with_effect = False):
        if not with_effect: 
            screen.blit(self.ballImage, 0, 0, self.ballImage.width, self.ballImage.height, int(self.pos[0]), int(self.pos[1]), True)
        else:
            screen.blit(self.ballImage, 0, 0, self.ballImage.width, self.ballImage.height, int(self.pos[0]), int(self.pos[1]), True)
            if self.nEffect >= 0:
                screen.blit(balls_effect[self.nEffect], 0, 0, self.ballImage.width, self.ballImage.height, int(self.pos[0]), int(self.pos[1]), True)
            if self.nEffect < 7:
                self.nEffect += 1
            else:
                self.nEffect = -20

    def roundPosAroundBall(self, ball):
        a = float(self.pos[0] - ball.pos[0])
        b = float(self.pos[1] - ball.pos[1])
        beta = math.acos(a / math.sqrt(a**2 + b**2))
        piThirds = math.pi / 3.0
        beta = piThirds * math.floor(0.5 + beta/piThirds)
        if self.pos[1] < ball.pos[1]:
                beta = - beta
        self.pos[0] = 2 * self.radius * math.cos(beta) + ball.pos[0]
        self.pos[1] = 2 * self.radius * math.sin(beta) + ball.pos[1]


    def roundPosToBoardTop(self, offsetPos = (0,0) , isTop8BallLine = True):
        [i, j] = self.getPosInBoard(offsetPos, isTop8BallLine)

        if isTop8BallLine:
            compensation = 0
        else:
            compensation = self.radius

        self.pos[0] = int(j * self.radius * 2.0 + compensation)
        self.pos[1] = int(i * math.sin(math.pi/3.0) * self.radius * 2.0)

        self.pos[0] = offsetPos[0] + self.pos[0]
        self.pos[1] = offsetPos[1] + self.pos[1]        

    def getPosInBoard(self, offsetPos = (0, 0), isTop8BallLine = True):
        x = self.pos[0] - offsetPos[0]
        y = self.pos[1] - offsetPos[1]
        
        i = math.floor(0.5 + y / (math.sin(math.pi/3.0) * self.radius * 2.0))
        if (isTop8BallLine and i % 2 == 0) or (not isTop8BallLine and i % 2 == 1):
            compensation = 0
        else:
            compensation = - self.radius
        j = math.floor(0.5 + (x - compensation)  / (self.radius * 2.0))

        return [int(i),int(j)]

class Thrower(object):
    def __init__(self, offsetPos):
        self.size = [100/1.5, 100/1.5]
        self.pos = [offsetPos[0] + [70/1.5, 264/1.5][0] + 7, offsetPos[1] + [70/1.5, 264/1.5][1] ]
        self.angle = math.pi / 2
        self.angleInc = math.pi / 180

    def render(self):
        screen.blit(throwerImg[int(self.angle/math.pi * 180)], 0, 0, 50, 50, int(self.pos[0]+7), int(self.pos[1]+7), True)

    def incAngle(self):
        self.angle = self.angle + self.angleInc
        if self.angle >  math.pi - 3*self.angleInc:
            self.angle = math.pi - 3*self.angleInc

    def decAngle(self):
        self.angle = self.angle - self.angleInc
        if self.angle <  3*self.angleInc:
            self.angle = 3*self.angleInc

    def getCenterPos(self):
        return [self.pos[0] + self.size[0]/2, self.pos[1] + self.size[1] / 2]

class ballAnimation(object):
	def __init__(self, balls):
		self.balls = copy.copy(balls)
		self.maxHeight = 272
		self.speed = 0
		self.acceleration = 0.2

	def render(self):
		for ball in self.balls:
			ball.render()

	def update(self):
		self.speed += self.acceleration
		for ball in self.balls:
			ball.pos[1] = ball.pos[1]+int(self.speed)

	def isFinished(self):
		for ball in self.balls:
			if ball.pos[1] < self.maxHeight:
				return False
		return True

game = None
mode = 'intro'
sel = 1
currSelLev = 0
introTimer = 0

oldpad = psp2d.Controller()
while True:
    pad = psp2d.Controller()
        
    if mode == 'intro':
        screen.blit(intro, 0, 0, 480, 272, 0, 0, True)
        
        if introTimer > 50:
            screen.blit(pressStartImg, 0, 0, pressStartImg.width, pressStartImg.height, 351, 231, True)
        if introTimer > 100:
            introTimer = 0
        if pad.start:
            introTimer = 0
            time.sleep(0.5)
            mode = 'menu'
        introTimer += 1

    elif mode == 'menu':
        screen.blit(intro, 0, 0, 480, 272, 0, 0, True)
        
        screen.blit(menuImg[0], 0, 0, menuImg[0].width, menuImg[0].height, 28, 22, True)
        screen.blit(menuImg[1], 0, 0, menuImg[0].width, menuImg[0].height, 165, 116, True)
        screen.blit(menuImg[2], 0, 0, menuImg[0].width, menuImg[0].height, 302, 209, True)
        
        if sel == 1:
            screen.blit(menuImgSel[0], 0, 0, menuImg[0].width, menuImg[0].height, 28, 22, True)
            if pad.cross and not oldpad.cross:
                sel = 0
                mode = 'chooseLevelSet'
                time.sleep(0.5)
        elif sel == 2:
            screen.blit(menuImgSel[1], 0, 0, menuImg[0].width, menuImg[0].height, 165, 116, True)
            if pad.cross and not oldpad.cross:
                game = Game('random', 1, 1)
                game.board.nLevel = 'random'
                mode = 'game'
        elif sel == 3:
            screen.blit(menuImgSel[2], 0, 0, menuImg[0].width, menuImg[0].height, 302, 209, True)
            if pad.cross and not oldpad.cross: break
        if pad.down and not oldpad.down and sel != 3:
            sndMenu.start()
            sel += 1
        elif pad.up and not oldpad.up and sel != 1:
            sndMenu.start()
            sel -= 1

    elif mode == 'chooseLevelSet':
        screen.blit(intro, 0, 0, 480, 272, 0, 0, True)
        screen.blit(menuImgSel[0], 0, 0, menuImg[0].width, menuImg[0].height, 28, 22, True)
        screen.blit(menuImg[1], 0, 0, menuImg[0].width, menuImg[0].height, 165, 116, True)
        screen.blit(menuImg[2], 0, 0, menuImg[0].width, menuImg[0].height, 302, 209, True)
        
        screen.blit(chooseLevelSetImg, 0, 0, chooseLevelSetImg.width, chooseLevelSetImg.height, (480/2)-(chooseLevelSetImg.width/2),\
                    (272/2)-(chooseLevelSetImg.height/2), True)
        if sel != 0:
            screen.blit(freccia_s, 0, 0, freccia_s.width, freccia_s.height, 141, 187, True)
        if len(level.lista_level_set) != 1 and sel != len(level.lista_level_set) - 1:
            screen.blit(freccia_d, 0, 0, freccia_s.width, freccia_s.height, 327, 187, True)
        font.drawText(screen, 227, 90, level.lista_level_set[sel])
        font.drawText(screen, 227, 121, level.authors[sel])
        font.drawText(screen, 227, 150, str(currSelLev+1)+'/'+str(level.n_levels[sel])+' (L/R)')
        if pad.right and not oldpad.right and sel != len(level.lista_level_set) - 1:
            sel += 1
            currSelLev = 0
        elif pad.left and not oldpad.left and sel != 0:
            sel -= 1
            currSelLev = 0
        if pad.l and not oldpad.l and currSelLev != 0: currSelLev -= 1
        elif pad.r and not oldpad.r and currSelLev + 1 != level.n_levels[sel]: currSelLev += 1
        if pad.cross and not oldpad.cross:
            game = Game(level.lista_level_set[sel]+".txt", currSelLev + 1, level.n_levels[sel])
            mode = 'game'
        if pad.triangle and not oldpad.triangle:
            sndCancel.start()
            mode = 'menu'
            sel = 1
            currSelLev = 0
        
    elif mode == 'game':
        screen.blit(bg)
        if not game.returnToMainMenu:
            game.run(pad, oldpad)
        else:
            mode = 'menu'

    oldpad = pad
    screen.swap()

