'''
Created on 23 juil. 2009

@author: Samuel Benveniste
'''
from math import floor, ceil
import pygame
import sys
import colorsys
import constants
from gradients import gradients
from logging.PickleableEvent import PickleableEvent
from logging.EventLog import EventLog


class SongFamiliarizer:
    '''
    The screen on which the game is played
    
        wiimotes: 
                The wiimotes used in this session
        window:
            The main display window
        screen:
            The main display surface
        clock:
            The clock used to animate the screen
        savedScreen:
            The background that is painted every time
        playerScreen:
            The buffer for painting everything before bliting
        width:
            The width of the window in pixels
        height:
            The height of the window in pixels
        extendScale :
            True if the scale is G to C instead of C to C
        cascade:
            True if crossing from note to note with a button pressed triggers a new note
        scaleSize:
            The size of the scale used
        cursorPositions:
            The positions of the cursors on the screen, in pixels
    '''
    
    
    
    def __init__(self, wiimotes, window, screen, clock, joys, portOffset, song,  activeWiimotes, cascade=False, extendedScale=False, easyMode = False, replay = False, eventLog = None, defaultInstrumentChannel = 16, defaultNote = 60):
        '''
        Constructor
        ''' 
        self.firstClickTime = None
        self.firstClickInTime = None
        self.duration = None
        self.clicks = 0
        self.clicksIn = 0
        
        pygame.font.init()
        self.font = pygame.font.Font(None,60)
        self.congratulations = ["Bien !","Tres Bien !","Bravo !","Excellent !","Felicitations !"]
        self.renderedCongratulations = [self.font.render(congratulation,False,(0,0,0)) for congratulation in self.congratulations]
        self.congratulationCount = None
        self.isCongratulating = False
        self.congratulationTimer = 0
        self.congratulationLength = 2000
        self.congratulationPos = None
               
        self.blinkLength = 200
        self.minimalVelocity = 90
        self.shortScaleSize = 8
        self.longScaleSize = 11
        if not extendedScale:
            self.offset = self.longScaleSize - self.shortScaleSize
        else:
            self.offset = 0
        self.borderSize = 5
        self.highlightedNote = 0
        self.highlightedNoteNumber = 0
        self.syllabus = None
        self.savedHighlightedNote = 0
        self.scaleFactor = 1
        self.level = 3
        
        self.wiimotes = wiimotes
        self.activeWiimotes = activeWiimotes
        self.window = window
        self.screen = screen
        self.width = int(floor(screen.get_width()*self.scaleFactor))
        self.height = int(floor(screen.get_height()*self.scaleFactor))
        self.blitOrigin = ((self.screen.get_width()-self.width)/2,(self.screen.get_height()-self.height)/2)        
        self.joys = joys
        self.clock = clock
        self.cursorPositions = []
        self.savedScreen = pygame.Surface(self.screen.get_size())
        self.savedScreen.fill((255,255,255))
        self.playerScreen = pygame.Surface(self.savedScreen.get_size())
        self.playerScreen.blit(self.savedScreen, (0, 0))
        self.extendedScale = extendedScale
        self.cascade = cascade
        self.portOffset =portOffset
        self.eventLog = eventLog
        self.song = song
        self.songIterator = self.song.getSongIterator()
        self.midiNoteNumbers = self.song.scale
        self.replay = replay
        self.quarterNoteLength = 800
        self.cascadeLockLengthMultiplier = 1
        self.cascadeLockLength = self.quarterNoteLength * self.cascadeLockLengthMultiplier
        
        self.defaultInstrumentChannel = defaultInstrumentChannel
        self.defaultNote = defaultNote
        
        self.done = False
        self.backToInstrumentChoice = False
        self.easyMode = easyMode
        
        if eventLog == None:
            self.eventLog = EventLog()
            self.replay = False
        else:
            self.eventLog = eventLog
            self.replay = replay
        
        #Initializes the highlightedNote and highlightedNoteNumber etc...
        self.moveToNextNote()
        
        self.blinkOn = False
        self.savedBlinkOn = False
        ##Will prevent the song to move on if two consecutive notes are identical and the buttons have not been released in between the two
        ##i.e. it guarantees that there will be an attack between two identical consecutive notes
        self.highlightIsFree = True
        
        self.noteRects = []
        self.boundingRect = None
        self.notes = []
        
        self.buttonDown = []
        self.velocityLock = []
        
        self._blinkOffset = 0
        self._cascadeLockTimer = 0
        self.cascadeIsFree = True
        
        self.font = pygame.font.Font(None,50)
        self.renderedNoteNames = [self.font.render(constants.noteNumberToName(note),False,(0,0,0)) for note in self.midiNoteNumbers]
        
        self.drawBackground()
        self.initializeWiimotes()
        
        events = pygame.event.get()
        
        #The main loop
        while not self.done :
            
            #Clear the cursors from the screen
            if self.hasChanged():
                self.drawBackground()
            self.playerScreen.blit(self.savedScreen, (0, 0))
            
            # Limit frame speed to 50 FPS
            #
            timePassed = self.clock.tick(10000)
            
            self._blinkOffset += timePassed
            if self.buttonDown and not self.cascadeIsFree :
                self._cascadeLockTimer += timePassed
                if self._cascadeLockTimer > self.cascadeLockLength :
                    self.cascadeIsFree = True
            
            
            if self._blinkOffset > self.blinkLength:
                self._blinkOffset -= self.blinkLength
                self.blinkOn = not self.blinkOn
                
            if self.replay:
                self.eventLog.update(timePassed)
                pickledEventsToPost = self.eventLog.getPickledEvents() 
                for pickledEvent in pickledEventsToPost:
                    pygame.event.post(pickledEvent.event)
            
            events = pygame.event.get()
            
            if not self.replay:
                pickledEvents = [PickleableEvent(event.type,event.dict) for event in events]
                if pickledEvents != [] :
                    self.eventLog.appendEventGroup(pickledEvents)
            
            for event in events:
                self.input(event)
            
            if self.isCongratulating :
                self.congratulationTimer += timePassed
                if self.congratulationTimer < self.congratulationLength :
                    self.blitCongratulation()
                else :
                    self.isCongratulating = False
                            
            for i in range(len(self.wiimotes)):
                if self.activeWiimotes[i]:
                    self.wiimotes[i].cursor.update(timePassed, self.cursorPositions[i])
                    if self.buttonDown[i] :
                        self.wiimotes[i].cursor.flash()
                    self.wiimotes[i].cursor.blit(self.playerScreen)
            
            self.screen.blit(self.playerScreen, (0,0))
            
            pygame.display.flip()
        
        for i in range(len(self.wiimotes)):
            if self.activeWiimotes[i]:
                self.wiimotes[i].stopNoteByNoteNumber(self.midiNoteNumbers[self.notes[i]])            
        if self.replay :
            self.duration = self.eventLog.getCurrentTime()
        
    def drawBackground(self):
        self.savedScreen.fill((255,255,255))
        
        if self.extendedScale :
            self.scaleSize = self.longScaleSize
        else:
            self.scaleSize = self.shortScaleSize
        
        self.noteRects = [pygame.Rect(i * self.width / self.scaleSize+self.blitOrigin[0], self.blitOrigin[1], self.width / self.scaleSize + 1, self.height+1) for i in range(self.scaleSize)]
        #inflate last noteRect to cover the far right pixels
        self.noteRects[-1].width = self.noteRects[-1].width + 1
        
        self.noteRects[self.highlightedNote-self.offset].inflate_ip(self.noteRects[self.highlightedNote-self.offset].width*2,0)
        
        #create bounding rect
        self.boundingRect = self.noteRects[0].unionall(self.noteRects)
        
        self.renderedNoteNames = [self.font.render(constants.noteNumberToName(note),False,(0,0,0)) for note in self.midiNoteNumbers]
        
        #fill the rectangles with a color gradient
        #We start with blue
        startingHue = 0.66666666666666663
        
#        for rectNumber in range(self.scaleSize):
#            colorRatio = float(rectNumber) / (self.scaleSize - 1)
#            #hue will go from 0.6666... (blue) to 0 (red) as colorRation goes up
#            hue = startingHue * (1 - colorRatio)
#            if rectNumber + self.offset != self.highlightedNote:
#                #The color of the bottom of the rectangle in hls coordinates
#                bottomColorHls = (hue, 0.1, 1)
#                #The color of the top of the rectangle in hls coordinates
#                topColorHls = (hue, 0.1, 1)
#            
#                #convert to rgb ranging from 0 to 255
#                bottomColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*bottomColorHls)]
#                topColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*topColorHls)]
#                #add transparency
#                bottomColorRgb.append(255)
#                topColorRgb.append(255)
#                #convert to tuple
#                bottomColorRgb = tuple(bottomColorRgb)
#                topColorRgb = tuple(topColorRgb)            
#                
#                self.savedScreen.blit(gradients.vertical(self.noteRects[rectNumber].size, topColorRgb, bottomColorRgb), self.noteRects[rectNumber])
#                
#                noteNameBlitPoint = (self.noteRects[rectNumber].left+(self.noteRects[rectNumber].width-self.renderedNoteNames[rectNumber].get_width())/2,
#                                 self.noteRects[rectNumber].bottom-self.renderedNoteNames[rectNumber].get_height())
#                
#                self.savedScreen.blit(self.renderedNoteNames[rectNumber], noteNameBlitPoint)
#                
#                pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 255), self.noteRects[rectNumber], 2)
        
        colorRatio = float(self.highlightedNote-self.offset) / (self.scaleSize - 1)
        #hue will go from 0.6666... (blue) to 0 (red) as colorRation goes up
        hue = startingHue * (1 - colorRatio)
        #The color of the bottom of the rectangle in hls coordinates
        bottomColorHls = (hue, 0.6, 1)
        #The color of the top of the rectangle in hls coordinates
        topColorHls = (hue, 0.9, 1)
        
        #convert to rgb ranging from 0 to 255
        bottomColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*bottomColorHls)]
        topColorRgb = [floor(255 * i) for i in colorsys.hls_to_rgb(*topColorHls)]
        #add transparency
        bottomColorRgb.append(255)
        topColorRgb.append(255)
        #convert to tuple
        bottomColorRgb = tuple(bottomColorRgb)
        topColorRgb = tuple(topColorRgb)            
        
        self.savedScreen.blit(gradients.vertical(self.noteRects[self.highlightedNote-self.offset].size, topColorRgb, bottomColorRgb), self.noteRects[self.highlightedNote-self.offset])
        
#        noteNameBlitPoint = (self.noteRects[self.highlightedNote-self.offset].left+(self.noteRects[self.highlightedNote-self.offset].width-self.renderedNoteNames[self.highlightedNote-self.offset].get_width())/2,
#                         self.noteRects[self.highlightedNote-self.offset].bottom-self.renderedNoteNames[self.highlightedNote-self.offset].get_height())
#        
#        self.savedScreen.blit(self.renderedNoteNames[self.highlightedNote-self.offset], noteNameBlitPoint)
#        
#        if self.syllabus :
#            renderedSyllabus = self.font.render(self.syllabus,False,(0,0,0))
#        
#            syllabusBlitPoint = (self.noteRects[self.highlightedNote-self.offset].left+(self.noteRects[self.highlightedNote-self.offset].width-renderedSyllabus.get_width())/2,
#                             self.noteRects[self.highlightedNote-self.offset].centery-renderedSyllabus.get_height()/2)
#            
#            self.savedScreen.blit(renderedSyllabus, syllabusBlitPoint)
        
        pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 255), self.noteRects[self.highlightedNote-self.offset], 2)    
            
#        if self.song != None and self.blinkOn:
#            borderSize = self.borderSize
#            pygame.draw.rect(self.savedScreen, pygame.Color(0, 0, 0, 0), self.noteRects[self.highlightedNote-self.offset].inflate(borderSize/2,borderSize/2), borderSize)
        
    def initializeWiimotes(self):
        for loop in self.wiimotes:
            if loop.port == None :
                loop.port = pygame.midi.Output(loop.portNumber)
            self.notes.append(0)
            self.cursorPositions.append(loop.cursor.centerPosition)
            self.buttonDown.append(False)
            self.velocityLock.append(False)
    
    def updateCursorPositionFromJoy(self, joyEvent):
        joyName = pygame.joystick.Joystick(joyEvent.joy).get_name()
        correctedJoyId = constants.joyNames.index(joyName)
        if correctedJoyId < len(self.cursorPositions):
            if joyEvent.axis == 0 :
                self.cursorPositions[correctedJoyId] = (int((joyEvent.value + 1) / 2 * self.screen.get_width()), self.cursorPositions[correctedJoyId][1])
            if joyEvent.axis == 1 :
                self.cursorPositions[correctedJoyId] = (self.cursorPositions[correctedJoyId][0], int((joyEvent.value + 1) / 2 * self.screen.get_height()))
    
    def heightToVelocity(self, pos, controllerNumber):
        if self.song != None:
            if self.boundingRect.collidepoint(pos) and (self.highlightedNote == self.notes[controllerNumber] or self.velocityLock[controllerNumber]):
                velocity = int(floor((1 - (float(pos[1])-self.blitOrigin[1]) / self.height) * (127-self.minimalVelocity))+self.minimalVelocity)
            else :
                if self.easyMode:
                    velocity = None
                else:
                    velocity = 60
        else:
            if self.boundingRect.collidepoint(pos):
                velocity = int(floor((1 - (float(pos[1])-self.blitOrigin[1]) / self.height) * (127-self.minimalVelocity))+self.minimalVelocity)
            else :
                velocity = self.minimalVelocity
        return(velocity)
    
    def widthToNote(self, pos):
        nn = 0
        try :
            if self.noteRects[self.highlightedNote-self.offset].collidepoint(pos) :
                return self.highlightedNote
            else :
                while self.noteRects[nn].collidepoint(pos) == False:
                    nn = nn + 1
                return(nn + self.offset)
        except(IndexError):
            return(None)
    
    def congratulate(self,targetRect,posy):
        if self.congratulationCount != None :
            if self.congratulationCount < len(self.congratulations)-1:
                self.congratulationCount += 1
        else :
            self.congratulationCount = 0
        self.congratulationTimer = 0
        self.congratulationPos = (targetRect.left+(targetRect.width-self.renderedCongratulations[self.congratulationCount].get_width())/2,posy)
        self.isCongratulating = True
        
    def resetCongratulation(self):
        self.congratulationCount = None
        self.congratulationPos = None
        self.isCongratulating = False
        
    def blitCongratulation(self):
        self.playerScreen.blit(self.renderedCongratulations[self.congratulationCount],self.congratulationPos)
    
    def input(self, event): 
        
        if event.type == pygame.QUIT:
            for loop in self.wiimotes:
                del loop.port
            pygame.midi.quit()
            sys.exit(0) 
        
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_q:
                self.nextLevel = None
                self.done = True
                
            if event.key == pygame.K_w:
                self.nextLevel = 0
                self.done = True
                
            if event.key == pygame.K_e:
                self.nextLevel = 1
                self.done = True
                
            if event.key == pygame.K_r:
                self.nextLevel = 2
                self.done = True
                
            if event.key == pygame.K_t:
                self.nextLevel = 3
                self.done = True
        
        if event.type == pygame.JOYAXISMOTION:
            
            joyName = pygame.joystick.Joystick(event.joy).get_name()
            correctedJoyId = constants.joyNames.index(joyName)
            if self.activeWiimotes[correctedJoyId]:
                self.updateCursorPositionFromJoy(event)
                wiimote = self.wiimotes[correctedJoyId]
                pos = self.cursorPositions[correctedJoyId]
    
                if self.buttonDown[correctedJoyId]:
                    if self.notes[correctedJoyId] != None:
                        velocity = self.heightToVelocity(pos, correctedJoyId)
                        if velocity != None :
                            CCHexCode = wiimote.getCCHexCode()
                            wiimote.port.write_short(CCHexCode, 07, velocity)
                    if self.cascade and self.cascadeIsFree :
                        n = self.widthToNote(pos)
                        if self.highlightedNote == n:
                            wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
                            self.notes[correctedJoyId] = n
                            velocity = self.heightToVelocity(pos, correctedJoyId)
                            self.velocityLock[correctedJoyId] = True
                            wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
                            self.moveToNextNote()
                            self._cascadeLockTimer = 0
                            self.cascadeIsFree = False
        
        if event.type == pygame.JOYBUTTONDOWN :
            
            joyName = pygame.joystick.Joystick(event.joy).get_name()
            correctedJoyId = constants.joyNames.index(joyName)
            if self.activeWiimotes[correctedJoyId]:
                wiimote = self.wiimotes[correctedJoyId]
                pos = self.cursorPositions[correctedJoyId]
                self.wiimotes[correctedJoyId].cursor.flash()
                if self.replay:
                    self.clicks += 1
                    if self.firstClickTime == None :
                        self.firstClickTime = self.eventLog.getCurrentTime()
                        
                if not self.buttonDown[correctedJoyId]:
                    n = self.widthToNote(pos)
                    if self.highlightedNote == n:
                        self._cascadeLockTimer = 0
                        self.cascadeIsFree = False
                        if self.easyMode:
                            wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
                        self.notes[correctedJoyId] = n
                        velocity = self.heightToVelocity(pos, correctedJoyId)
                        self.velocityLock[correctedJoyId] = True
                        wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
                        self.congratulate(self.noteRects[self.notes[correctedJoyId]],pos[1])
                        if self.replay:
                            self.clicksIn += 1
                            if self.firstClickInTime == None :
                                self.firstClickInTime = self.eventLog.getCurrentTime()
                 
                        self.moveToNextNote()
                    else :
                        self.resetCongratulation()
                        if not self.easyMode :
                            self._cascadeLockTimer = 0
                            self.cascadeIsFree = False
                            self.notes[correctedJoyId] = n
                            velocity = self.heightToVelocity(pos, correctedJoyId)                    
                            if velocity != None :
                                wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)                                      
                    self.buttonDown[correctedJoyId] = True
            
        if event.type == pygame.JOYBUTTONUP:
            joyName = pygame.joystick.Joystick(event.joy).get_name()
            correctedJoyId = constants.joyNames.index(joyName)
            if self.activeWiimotes[correctedJoyId]:
                self.buttonDown[correctedJoyId] = False
                wiimote = self.wiimotes[correctedJoyId]
                if not self.easyMode:
                    wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
                self.velocityLock[correctedJoyId] = False
            
        if event.type == pygame.MOUSEMOTION:
            
            self.updateCursorPositionFromMouse(event)
            
            correctedJoyId = 0
            while not self.activeWiimotes[correctedJoyId] :
                correctedJoyId += 1
            wiimote = self.wiimotes[correctedJoyId]
            pos = self.cursorPositions[correctedJoyId]

            if self.buttonDown[correctedJoyId]:
                self.wiimotes[correctedJoyId].cursor.flash()
                if self.notes[correctedJoyId] != None:
                    velocity = self.heightToVelocity(pos, correctedJoyId)
                    if velocity != None :
                        CCHexCode = wiimote.getCCHexCode()
                        wiimote.port.write_short(CCHexCode, 07, velocity)
                if self.cascade and self.cascadeIsFree :
                    n = self.widthToNote(pos)
                    if self.highlightedNote == n:
                        wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
                        self.notes[correctedJoyId] = n
                        velocity = self.heightToVelocity(pos, correctedJoyId)
                        self.velocityLock[correctedJoyId] = True
                        wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
                        self.moveToNextNote()
                        self._cascadeLockTimer = 0
                        self.cascadeIsFree = False             
        
        if event.type == pygame.MOUSEBUTTONDOWN:
            
            if event.button == 1:
                correctedJoyId = 0
                while not self.activeWiimotes[correctedJoyId] :
                    correctedJoyId += 1
                wiimote = self.wiimotes[correctedJoyId]
                pos = self.cursorPositions[correctedJoyId]
                self.wiimotes[correctedJoyId].cursor.flash()
                if self.replay:
                    self.clicks += 1
                    if self.firstClickTime == None :
                        self.firstClickTime = self.eventLog.getCurrentTime()
                        
                if not self.buttonDown[correctedJoyId]:
                    n = self.widthToNote(pos)
                    if self.highlightedNote == n:
                        self._cascadeLockTimer = 0
                        self.cascadeIsFree = False
                        if self.easyMode:
                            wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
                        self.notes[correctedJoyId] = n
                        velocity = self.heightToVelocity(pos, correctedJoyId)
                        self.velocityLock[correctedJoyId] = True
                        wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)
                        self.congratulate(self.noteRects[self.notes[correctedJoyId]],pos[1])
                        if self.replay:
                            self.clicksIn += 1
                            if self.firstClickInTime == None :
                                self.firstClickInTime = self.eventLog.getCurrentTime()
                 
                        self.moveToNextNote()
                    else :
                        self.resetCongratulation()
                        if not self.easyMode :
                            self._cascadeLockTimer = 0
                            self.cascadeIsFree = False
                            self.notes[correctedJoyId] = n
                            velocity = self.heightToVelocity(pos, correctedJoyId)                    
                            if velocity != None :
                                wiimote.playNoteByNoteNumber(self.midiNoteNumbers[self.notes[correctedJoyId]],velocity)                                      
                    self.buttonDown[correctedJoyId] = True                    
            
            if event.button == 2:
                
                self.done = True
        
        if event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1 :
                correctedJoyId = 0
                while not self.activeWiimotes[correctedJoyId] :
                    correctedJoyId += 1
                wiimote = self.wiimotes[correctedJoyId]
                self.buttonDown[correctedJoyId] = False
                if not self.easyMode:
                    wiimote.stopNoteByNoteNumber(self.savedMidiNoteNumbers[self.notes[correctedJoyId]])
                self.velocityLock[correctedJoyId] = False
        
    def hasChanged(self):
        changed = False
        if self.song != None:
            if self.blinkOn != self.savedBlinkOn or self.highlightedNote != self.savedHighlightedNote:
                self.savedBlinkOn = self.blinkOn
                self.savedHighlightedNote = self.highlightedNote
                changed = True
        return(changed)
    
    def updateCursorPositionFromMouse(self, mouseEvent):
        correctedJoyId = 0
        while not self.activeWiimotes[correctedJoyId] :
            correctedJoyId += 1
        self.cursorPositions[correctedJoyId] = mouseEvent.pos
        
    def moveToNextNote(self):
        self.savedMidiNoteNumbers = self.midiNoteNumbers[:]
        self.highlightedNote, self.highlightedNoteNumber, self.syllabus, self.cascadeLockLengthMultiplier = self.songIterator.next()
        self.midiNoteNumbers[self.highlightedNote] = self.highlightedNoteNumber
