from panda3d.core import *
from direct.interval.IntervalGlobal import *
from toontown.toonbase.ToonBaseGlobal import *
from toontown.toonbase import TTLocalizer
from direct.gui.DirectFrame import DirectFrame
from direct.gui.DirectGui import DGG
from direct.task.Task import Task
from direct.fsm import ClassicFSM, State
from direct.directnotify import DirectNotifyGlobal
from .DistributedMinigame import *
from . import MinigameAvatarScorePanel, ArrowKeys, ToonBlitzAssetMgr, TwoDCamera
from . import TwoDSectionMgr, ToonBlitzGlobals, TwoDGameToonSD
from toontown.toonbase import ToontownTimer
from .TwoDWalk import *
from .TwoDDrive import *
COLOR_RED = VBase4(1, 0, 0, 0.3)

class DistributedTwoDGame(DistributedMinigame):
    notify = DirectNotifyGlobal.directNotify.newCategory('DistributedTwoDGame')
    UpdateLocalToonTask = 'ToonBlitzUpdateLocalToonTask'
    EndGameTaskName = 'endTwoDGame'

    def __init__(self, cr):
        DistributedMinigame.__init__(self, cr)
        self.gameFSM = ClassicFSM.ClassicFSM('DistributedTwoDGame', [State.State('off', self.enterOff, self.exitOff, ['play']),
         State.State('play', self.enterPlay, self.exitPlay, ['cleanup', 'pause', 'showScores']),
         State.State('pause', self.enterPause, self.exitPause, ['cleanup', 'play', 'showScores']),
         State.State('showScores', self.enterShowScores, self.exitShowScores, ['cleanup']),
         State.State('cleanup', self.enterCleanup, self.exitCleanup, [])], 'off', 'cleanup')
        self.addChildGameFSM(self.gameFSM)
        self.reportedDone = False
        self.showCollSpheres = False

    def getTitle(self):
        return TTLocalizer.TwoDGameTitle

    def getInstructions(self):
        return TTLocalizer.TwoDGameInstructions

    def getMaxDuration(self):
        return 200

    def __defineConstants(self):
        self.TOON_SPEED = 12.0
        self.MAX_FRAME_MOVE = 1
        self.isHeadInFloor = False
        self.timeToRunToElevator = 1.5

    def setSectionsSelected(self, sectionsSelected):
        self.sectionsSelected = sectionsSelected

    def load(self):
        self.notify.debug('load')
        DistributedMinigame.load(self)
        self.__defineConstants()
        self.assetMgr = ToonBlitzAssetMgr.ToonBlitzAssetMgr(self)
        self.cameraMgr = TwoDCamera.TwoDCamera(camera)
        self.sectionMgr = TwoDSectionMgr.TwoDSectionMgr(self, self.sectionsSelected)
        self.gameStartX = -40.0
        endSection = self.sectionMgr.sections[-1]
        self.gameEndX = endSection.spawnPointMgr.gameEndX
        self.gameLength = self.gameEndX - self.gameStartX
        self.toonSDs = {}
        avId = self.localAvId
        toonSD = TwoDGameToonSD.TwoDGameToonSD(avId, self)
        self.toonSDs[avId] = toonSD
        self.toonSDs[avId].createHeadFrame(0)

    def unload(self):
        self.notify.debug('unload')
        DistributedMinigame.unload(self)
        taskMgr.remove(self.UpdateLocalToonTask)
        for avId in list(self.toonSDs.keys()):
            toonSD = self.toonSDs[avId]
            toonSD.destroy()

        del self.toonSDs
        self.cameraMgr.destroy()
        del self.cameraMgr
        self.sectionMgr.destroy()
        del self.sectionMgr
        for panel in self.scorePanels:
            panel.cleanup()

        del self.scorePanels
        self.assetMgr.destroy()
        del self.assetMgr
        self.removeChildGameFSM(self.gameFSM)
        del self.gameFSM

    def onstage(self):
        self.notify.debug('onstage')
        DistributedMinigame.onstage(self)
        self.scorePanels = []
        self.assetMgr.onstage()
        lt = base.localAvatar
        lt.reparentTo(render)
        lt.hideName()
        self.__placeToon(self.localAvId)
        lt.setAnimState('Happy', 1.0)
        lt.setSpeed(0, 0)
        base.localAvatar.collisionsOn()
        base.localAvatar.setTransparency(1)
        self.setupHeadCollision()
        self.cameraMgr.onstage()
        toonSD = self.toonSDs[self.localAvId]
        toonSD.enter()
        toonSD.fsm.request('normal')
        self.twoDDrive = TwoDDrive(self, self.TOON_SPEED, maxFrameMove=self.MAX_FRAME_MOVE)

    def offstage(self):
        self.notify.debug('offstage')
        self.assetMgr.offstage()
        for avId in list(self.toonSDs.keys()):
            self.toonSDs[avId].exit()

        base.localAvatar.setTransparency(0)
        self.ignore('enterheadCollSphere-into-floor1')
        base.localAvatar.controlManager.currentControls.cTrav.removeCollider(self.headCollNP)
        self.headCollNP.removeNode()
        del self.headCollNP
        base.localAvatar.laffMeter.start()
        DistributedMinigame.offstage(self)

    def setGameReady(self):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameReady')
        if DistributedMinigame.setGameReady(self):
            return
        drawNum = 0
        for avId in self.remoteAvIdList:
            toon = self.getAvatar(avId)
            if toon:
                drawNum += 1
                toon.reparentTo(render)
                toon.setAnimState('Happy', 1.0)
                toon.hideName()
                toon.startSmooth()
                toon.startLookAround()
                distCNP = toon.find('**/distAvatarCollNode*')
                distCNP.node().setIntoCollideMask(BitMask32.allOff())
                toonSD = TwoDGameToonSD.TwoDGameToonSD(avId, self)
                self.toonSDs[avId] = toonSD
                toonSD.enter()
                toonSD.fsm.request('normal')
                self.toonSDs[avId].createHeadFrame(drawNum)

    def setGameStart(self, timestamp):
        if not self.hasLocalToon:
            return
        self.notify.debug('setGameStart')
        DistributedMinigame.setGameStart(self, timestamp)
        self.twoDWalk = TwoDWalk(self.twoDDrive, broadcast=not self.isSinglePlayer())
        self.scores = [0] * self.numPlayers
        spacing = 0.4
        for i in range(self.numPlayers):
            avId = self.avIdList[i]
            avName = self.getAvatarName(avId)
            scorePanel = MinigameAvatarScorePanel.MinigameAvatarScorePanel(avId, avName)
            scorePanel.setScale(0.9)
            scorePanel.setPos(0.75 - spacing * (self.numPlayers - 1 - i), 0.0, 0.85)
            scorePanel.makeTransparent(0.75)
            self.scorePanels.append(scorePanel)

        self.gameFSM.request('play', [timestamp])

    def enterOff(self):
        self.notify.debug('enterOff')

    def exitOff(self):
        pass

    def enterPlay(self, timestamp):
        self.notify.debug('enterPlay')
        elapsedTime = globalClockDelta.localElapsedTime(timestamp)
        self.sectionMgr.enterPlay(elapsedTime)
        handlers = [None,
         None,
         None,
         None,
         self.shootKeyHandler]
        self.twoDDrive.arrowKeys.setPressHandlers(handlers)
        self.twoDWalk.start()
        self.accept('jumpStart', self.startJump)
        self.accept('enemyHit', self.localToonHitByEnemy)
        self.accept('twoDTreasureGrabbed', self.__treasureGrabbed)
        self.accept('enemyShot', self.__enemyShot)
        taskMgr.remove(self.UpdateLocalToonTask)
        taskMgr.add(self.__updateLocalToonTask, self.UpdateLocalToonTask, priority=1)
        base.localAvatar.laffMeter.stop()
        self.timer = ToontownTimer.ToontownTimer()
        self.timer.posInTopRightCorner()
        self.timer.setTime(ToonBlitzGlobals.GameDuration[self.getSafezoneId()])
        self.timer.countdown(ToonBlitzGlobals.GameDuration[self.getSafezoneId()], self.timerExpired)
        return

    def exitPlay(self):
        handlers = [None,
         None,
         None,
         None,
         None]
        self.twoDDrive.arrowKeys.setPressHandlers(handlers)
        if self.toonSDs[self.localAvId].fsm.getCurrentState().getName() != 'victory':
            base.localAvatar.b_setAnimState('Happy', 1.0)
        self.ignore('jumpStart')
        self.ignore('enemyHit')
        self.ignore('twoDTreasureGrabbed')
        return

    def enterPause(self):
        self.notify.debug('enterPause')

    def exitPause(self):
        pass

    def enterShowScores(self):
        self.notify.debug('enterShowScores')
        lerpTrack = Parallel()
        lerpDur = 0.5
        tY = 0.6
        bY = -.6
        lX = -.7
        cX = 0
        rX = 0.7
        scorePanelLocs = (((cX, bY),),
         ((lX, bY), (rX, bY)),
         ((cX, bY), (lX, bY), (rX, bY)),
         ((lX, tY),
          (rX, tY),
          (lX, bY),
          (rX, bY)))
        scorePanelLocs = scorePanelLocs[self.numPlayers - 1]
        for i in range(self.numPlayers):
            panel = self.scorePanels[i]
            pos = scorePanelLocs[i]
            lerpTrack.append(Parallel(LerpPosInterval(panel, lerpDur, Point3(pos[0], 0, pos[1]), blendType='easeInOut'), LerpScaleInterval(panel, lerpDur, Vec3(panel.getScale()) * 1.5, blendType='easeInOut')))

        self.showScoreTrack = Parallel(lerpTrack, self.getElevatorCloseTrack(), Sequence(Wait(ToonBlitzGlobals.ShowScoresDuration), Func(self.gameOver)))
        self.showScoreTrack.start()

    def exitShowScores(self):
        self.showScoreTrack.pause()
        del self.showScoreTrack

    def enterCleanup(self):
        self.notify.debug('enterCleanup')
        self.timer.stop()
        self.timer.destroy()
        del self.timer
        taskMgr.remove(self.EndGameTaskName)
        self.twoDWalk.stop()
        self.twoDWalk.destroy()
        del self.twoDWalk
        self.twoDDrive = None
        del self.twoDDrive
        return

    def exitCleanup(self):
        pass

    def acceptInputs(self):
        if hasattr(self, 'twoDDrive'):
            handlers = [None,
             None,
             None,
             None,
             self.shootKeyHandler]
            self.twoDDrive.arrowKeys.setPressHandlers(handlers)
            self.twoDDrive.start()
        return

    def ignoreInputs(self):
        if hasattr(self, 'twoDDrive'):
            handlers = [None,
             None,
             None,
             None,
             None]
            self.twoDDrive.arrowKeys.setPressHandlers(handlers)
            self.twoDDrive.lastAction = None
            self.twoDDrive.stop()
        return

    def __updateLocalToonTask(self, task):
        dt = globalClock.getDt()
        self.cameraMgr.update()
        if self.gameFSM.getCurrentState().getName() == 'play':
            if not self.toonSDs[self.localAvId].fsm.getCurrentState().getName() == 'victory':
                if not base.localAvatar.getY() == 0:
                    base.localAvatar.setY(0)
        if base.localAvatar.getZ() < -2.0:
            self.localToonFellDown()
        for avId in list(self.toonSDs.keys()):
            self.toonSDs[avId].update()

        return task.cont

    def handleDisabledAvatar(self, avId):
        self.notify.debug('handleDisabledAvatar')
        self.notify.debug('avatar ' + str(avId) + ' disabled')
        self.toonSDs[avId].exit(unexpectedExit=True)
        del self.toonSDs[avId]
        DistributedMinigame.handleDisabledAvatar(self, avId)

    def setupHeadCollision(self):
        collSphere = CollisionSphere(0, 0, 0, 1)
        collSphere.setTangible(1)
        collNode = CollisionNode('headCollSphere')
        collNode.setFromCollideMask(ToontownGlobals.WallBitmask)
        collNode.setIntoCollideMask(BitMask32.allOff())
        collNode.addSolid(collSphere)
        head = base.localAvatar.getPart('head', '1000')
        self.headCollNP = head.attachNewNode(collNode)
        self.headCollNP.setPos(0, 0, 0.0)
        animal = base.localAvatar.style.getAnimal()
        if animal == 'dog' or animal == 'bear' or animal == 'horse':
            torso = base.localAvatar.style.torso
            legs = base.localAvatar.style.legs
            if (torso == 'ls' or torso == 'ld') and legs == 'l':
                self.headCollNP.setZ(-1.3)
            else:
                self.headCollNP.setZ(-0.7)
        elif animal == 'mouse' or animal == 'duck':
            self.headCollNP.setZ(0.5)
        elif animal == 'cat':
            self.headCollNP.setZ(-0.3)
        elif animal == 'rabbit':
            self.headCollNP.setZ(-0.5)
        elif animal == 'monkey':
            self.headCollNP.setZ(0.3)
        elif animal == 'pig':
            self.headCollNP.setZ(-0.7)
        self.headCollNP.hide()
        if self.showCollSpheres:
            self.headCollNP.show()
        headCollisionEvent = CollisionHandlerEvent()
        headCollisionEvent.addInPattern('enter%fn-into-%in')
        headCollisionEvent.addOutPattern('%fn-exit-%in')
        cTrav = base.localAvatar.controlManager.currentControls.cTrav
        cTrav.addCollider(self.headCollNP, headCollisionEvent)
        self.accept('enterheadCollSphere-into-floor1', self.__handleHeadCollisionIntoFloor)
        self.accept('headCollSphere-exit-floor1', self.__handleHeadCollisionExitFloor)

    def __handleHeadCollisionIntoFloor(self, cevent):
        self.isHeadInFloor = True
        if base.localAvatar.controlManager.currentControls.lifter.getVelocity() > 0:
            base.localAvatar.controlManager.currentControls.lifter.setVelocity(0.0)
            self.assetMgr.playHeadCollideSound()

    def __handleHeadCollisionExitFloor(self, cevent):
        self.isHeadInFloor = False

    def __placeToon(self, avId):
        toon = self.getAvatar(avId)
        i = self.avIdList.index(avId)
        pos = Point3(ToonBlitzGlobals.ToonStartingPosition[0] + i, ToonBlitzGlobals.ToonStartingPosition[1], ToonBlitzGlobals.ToonStartingPosition[2])
        toon.setPos(pos)
        toon.setHpr(-90, 0, 0)

    def startJump(self):
        self.assetMgr.playJumpSound()

    def checkValidity(self, avId):
        if not self.hasLocalToon:
            return False
        if self.gameFSM.getCurrentState() == None or self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring msg: av %s performing some action.' % avId)
            return False
        toon = self.getAvatar(avId)
        if toon == None:
            return False
        return True

    def shootKeyHandler(self):
        self.toonSDs[self.localAvId].shootGun()
        timestamp = globalClockDelta.localToNetworkTime(globalClock.getFrameTime())
        self.sendUpdate('showShootGun', [self.localAvId, timestamp])

    def showShootGun(self, avId, timestamp):
        if self.checkValidity(avId):
            self.notify.debug('avatar %s is shooting water gun' % avId)
            if avId != self.localAvId:
                self.toonSDs[avId].shootGun()

    def localToonFellDown(self):
        if self.toonSDs[self.localAvId].fsm.getCurrentState().getName() != 'fallDown':
            self.toonSDs[self.localAvId].fsm.request('fallDown')
            timestamp = globalClockDelta.localToNetworkTime(globalClock.getFrameTime())
            self.updateScore(self.localAvId, ToonBlitzGlobals.ScoreLossPerFallDown[self.getSafezoneId()])
            self.sendUpdate('toonFellDown', [self.localAvId, timestamp])

    def toonFellDown(self, avId, timestamp):
        if self.checkValidity(avId):
            self.notify.debug('avatar %s fell down.' % avId)
            if avId != self.localAvId:
                self.updateScore(avId, ToonBlitzGlobals.ScoreLossPerFallDown[self.getSafezoneId()])
                self.toonSDs[avId].fsm.request('fallDown')

    def localToonHitByEnemy(self):
        currToonState = self.toonSDs[self.localAvId].fsm.getCurrentState().getName()
        if not (currToonState == 'fallBack' or currToonState == 'squish'):
            self.toonSDs[self.localAvId].fsm.request('fallBack')
            timestamp = globalClockDelta.localToNetworkTime(globalClock.getFrameTime())
            self.updateScore(self.localAvId, ToonBlitzGlobals.ScoreLossPerEnemyCollision[self.getSafezoneId()])
            self.sendUpdate('toonHitByEnemy', [self.localAvId, timestamp])

    def toonHitByEnemy(self, avId, timestamp):
        if self.checkValidity(avId):
            self.notify.debug('avatar %s hit by a suit' % avId)
            if avId != self.localAvId:
                self.updateScore(avId, ToonBlitzGlobals.ScoreLossPerEnemyCollision[self.getSafezoneId()])
                self.toonSDs[avId].fsm.request('fallBack')

    def localToonSquished(self):
        currToonState = self.toonSDs[self.localAvId].fsm.getCurrentState().getName()
        if not (currToonState == 'fallBack' or currToonState == 'squish'):
            self.toonSDs[self.localAvId].fsm.request('squish')
            timestamp = globalClockDelta.localToNetworkTime(globalClock.getFrameTime())
            self.updateScore(self.localAvId, ToonBlitzGlobals.ScoreLossPerStomperSquish[self.getSafezoneId()])
            self.sendUpdate('toonSquished', [self.localAvId, timestamp])

    def toonSquished(self, avId, timestamp):
        if self.checkValidity(avId):
            self.notify.debug('avatar %s is squished.' % avId)
            if avId != self.localAvId:
                self.updateScore(avId, ToonBlitzGlobals.ScoreLossPerStomperSquish[self.getSafezoneId()])
                self.toonSDs[avId].fsm.request('squish')

    def localToonVictory(self):
        if not ToonBlitzGlobals.EndlessGame:
            self.ignoreInputs()
        if not self.toonSDs[self.localAvId].fsm.getCurrentState().getName() == 'victory':
            self.toonSDs[self.localAvId].fsm.request('victory')
            timestamp = globalClockDelta.localToNetworkTime(globalClock.getFrameTime())
            self.sendUpdate('toonVictory', [self.localAvId, timestamp])

    def toonVictory(self, avId, timestamp):
        if self.checkValidity(avId):
            self.notify.debug('avatar %s achieves victory' % avId)
            if avId != self.localAvId:
                self.toonSDs[avId].fsm.request('victory')

    def addVictoryScore(self, avId, score):
        if not self.hasLocalToon:
            return
        self.updateScore(avId, score)
        if avId == self.localAvId:
            self.assetMgr.threeSparkles.play()

    def __treasureGrabbed(self, sectionIndex, treasureIndex):
        self.notify.debug('treasure %s-%s grabbed' % (sectionIndex, treasureIndex))
        section = self.sectionMgr.sections[sectionIndex]
        section.treasureMgr.treasures[treasureIndex].hideTreasure()
        self.assetMgr.treasureGrabSound.play()
        self.sendUpdate('claimTreasure', [sectionIndex, treasureIndex])

    def setTreasureGrabbed(self, avId, sectionIndex, treasureIndex):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() == 'play':
            self.notify.debug('treasure %s-%s grabbed by %s' % (sectionIndex, treasureIndex, avId))
            numSections = len(self.sectionMgr.sections)
            if sectionIndex < numSections:
                section = self.sectionMgr.sections[sectionIndex]
                numTreasures = len(section.treasureMgr.treasures)
                if treasureIndex < numTreasures:
                    treasure = section.treasureMgr.treasures[treasureIndex]
                    if avId != self.localAvId:
                        treasure.hideTreasure()
                    self.updateScore(avId, ToonBlitzGlobals.ScoreGainPerTreasure * treasure.value)
                else:
                    self.notify.error('WHOA!! treasureIndex %s is out of range; numTreasures = %s' % (treasureIndex, numTreasures))
                    base.localAvatar.sendLogMessage('treasureIndex %s is out of range; numTreasures = %s' % (treasureIndex, numTreasures))
            else:
                self.notify.error('WHOA!! sectionIndex %s is out of range; numSections = %s' % (sectionIndex, numSections))
                base.localAvatar.sendLogMessage('sectionIndex %s is out of range; numSections = %s' % (sectionIndex, numSections))

    def __enemyShot(self, sectionIndex, enemyIndex):
        self.sectionMgr.sections[sectionIndex].enemyMgr.enemies[enemyIndex].doShotTrack()
        self.sendUpdate('claimEnemyShot', [sectionIndex, enemyIndex])

    def setEnemyShot(self, avId, sectionIndex, enemyIndex, enemyHealth):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() == 'play':
            self.notify.debug('enemy %s is shot by %s. Health left %s' % (enemyIndex, avId, enemyHealth))
            if enemyHealth > 0:
                if not avId == self.localAvId:
                    self.sectionMgr.sections[sectionIndex].enemyMgr.enemies[enemyIndex].doShotTrack()
            else:
                enemy = self.sectionMgr.sections[sectionIndex].enemyMgr.enemies[enemyIndex]
                treasureSpawnPoint = Point3(enemy.suit.getX(), enemy.suit.getY(), enemy.suit.getZ() + enemy.suit.height / 2.0)
                self.spawnTreasure(sectionIndex, enemyIndex, treasureSpawnPoint)
                enemy.doDeathTrack()

    def updateScore(self, avId, deltaScore):
        i = self.avIdList.index(avId)
        self.scores[i] += deltaScore
        self.scorePanels[i].setScore(self.scores[i])
        self.toonSDs[avId].showScoreText(deltaScore)

    def spawnTreasure(self, sectionIndex, enemyIndex, pos):
        if self.gameFSM.getCurrentState().getName() == 'play':
            section = self.sectionMgr.sections[sectionIndex]
            treasure = section.treasureMgr.enemyTreasures[enemyIndex]
            treasure.setTreasurePos(pos)
            treasure.popupEnemyTreasure()

    def timerExpired(self):
        self.notify.debug('timer expired')
        if not self.reportedDone:
            if not ToonBlitzGlobals.EndlessGame:
                self.ignoreInputs()
            self.reportedDone = True
            self.sendUpdate('reportDone')

    def setEveryoneDone(self):
        if not self.hasLocalToon:
            return
        if self.gameFSM.getCurrentState().getName() != 'play':
            self.notify.warning('ignoring setEveryoneDone msg')
            return
        self.notify.debug('setEveryoneDone')

        def endGame(task, self = self):
            if not ToonBlitzGlobals.EndlessGame:
                self.gameFSM.request('showScores')
            return Task.done

        self.timer.hide()
        taskMgr.doMethodLater(1, endGame, self.EndGameTaskName)

    def getElevatorCloseTrack(self):
        leftDoor = self.sectionMgr.exitElevator.find('**/doorL')
        rightDoor = self.sectionMgr.exitElevator.find('**/doorR')
        leftDoorClose = LerpPosInterval(leftDoor, 2, Point3(3.02, 0, 0))
        rightDoorClose = LerpPosInterval(rightDoor, 2, Point3(-3.02, 0, 0))
        return Sequence(Wait(self.timeToRunToElevator), Parallel(leftDoorClose, rightDoorClose))

    def debugStartPause(self):
        self.sectionMgr.enterPause()

    def debugEndPause(self):
        self.sectionMgr.exitPause()