from panda3d.core import Vec3, NodePath
from direct.distributed.ClockDelta import globalClockDelta
from otp.avatar.SpeedMonitor import SpeedMonitor
from toontown.cogdominium.CogdoMaze import CogdoMazeFactory
from toontown.cogdominium.DistCogdoMazeGameBase import DistCogdoMazeGameBase
from .DistCogdoGameAI import DistCogdoGameAI
from . import CogdoMazeGameGlobals as Globals
cogdoMazeTimeScoreRatio = 0.5
cogdoMazePerfectTime = 90
cogdoMazeMaxTime = 210
cogdoMazePickupScoreRatio = 0.7

class DistCogdoMazeGameAI(DistCogdoGameAI, DistCogdoMazeGameBase):
    notify = directNotify.newCategory('DistCogdoMazeGameAI')
    TimerExpiredTaskName = 'CMG_TimerExpiredTask'
    TimeoutTimerTaskName = 'CMG_timeoutTimerTask'
    CountdownTimerTaskName = 'CMG_countdownTimerTask'
    AnnounceGameDoneTimerTaskName = 'CMG_AnnounceGameDoneTimerTask'
    SkipCogdoGames = simbase.config.GetBool('skip-cogdo-game', 0)

    def __init__(self, air, id):
        DistCogdoGameAI.__init__(self, air, id)
        self.doorIsOpen = False
        self.toonsInDoor = []
        self.pickups = []
        self.pickupsDropped = 0
        self.maxPickups = 0
        self.numPickedUp = 0
        self.suits = {}
        self.lastRequestId = None
        self.requestStartTime = globalClock.getFrameTime()
        self.requestCount = None
        self.jokeLastRequestId = None
        self.jokeRequestStartTime = globalClock.getFrameTime()
        self.jokeRequestCount = None
        if __debug__ and simbase.config.GetBool('schellgames-dev', True):
            self.accept('onCodeReload', self.__sgOnCodeReload)

    def setExteriorZone(self, exteriorZone):
        DistCogdoGameAI.setExteriorZone(self, exteriorZone)
        self.difficulty = self.getDifficulty()
        self.createSuits()

    def createSuits(self):
        serialNum = 0
        self._numSuits = []
        extraSuits = 0
        for i in range(len(Globals.NumSuits)):
            extraSuits = int(round(self.difficulty * Globals.SuitsModifier[i]))
            self._numSuits.append(Globals.NumSuits[i] + extraSuits)

        self.bosses = self._numSuits[0]
        for i in range(self._numSuits[0]):
            self.suits[serialNum] = Globals.SuitData[Globals.SuitTypes.Boss]['hp']
            serialNum += 1

        for i in range(self._numSuits[1]):
            self.suits[serialNum] = Globals.SuitData[Globals.SuitTypes.FastMinion]['hp']
            serialNum += 1

        for i in range(self._numSuits[2]):
            self.suits[serialNum] = Globals.SuitData[Globals.SuitTypes.SlowMinion]['hp']
            serialNum += 1

        self._totalSuits = serialNum
        self.maxPickups = self._numSuits[0] * Globals.SuitData[0]['memos']
        self.maxPickups += self._numSuits[1] * Globals.SuitData[1]['memos']
        self.maxPickups += self._numSuits[2] * Globals.SuitData[2]['memos']

    def generate(self):
        DistCogdoGameAI.generate(self)
        mazeFactory = self.createMazeFactory(self.createRandomNumGen())
        waterCoolerList = []
        mazeModel = mazeFactory._loadAndBuildMazeModel()
        for waterCooler in mazeModel.findAllMatches('**/*waterCooler'):
            waterCoolerList.append((waterCooler.getPos(mazeModel), waterCooler.getHpr(mazeModel)))

        waterCoolerList.sort()
        baseNp = NodePath('base')
        rotNp = baseNp.attachNewNode('rot')
        childNp = rotNp.attachNewNode('child')
        childNp.setPos(*Globals.WaterCoolerTriggerOffset)
        self._waterCoolerPosList = []
        for (pos, hpr) in waterCoolerList:
            rotNp.setHpr(hpr)
            offset = childNp.getPos(baseNp)
            self._waterCoolerPosList.append(pos + offset)

        self._speedMonitor = SpeedMonitor('cogdoMazeGame-%s' % self.doId)
        self._toonId2speedToken = {}

    def delete(self):
        self.ignoreAll()
        self._speedMonitor.destroy()
        DistCogdoGameAI.delete(self)

    def isDoorOpen(self):
        return self.doorIsOpen

    def isToonInDoor(self, toonId):
        return toonId in self.toonsInDoor

    def areAllToonsInDoor(self):
        return self.getNumPlayers() == len(self.toonsInDoor)

    def getCurrentNetworkTime(self):
        return globalClockDelta.localToNetworkTime(globalClock.getRealTime())

    def suitHit(self, suitType, suitNum):
        avId = simbase.air.getAvatarIdFromSender()
        toon = simbase.air.doId2do.get(avId)
        if not toon:
            simbase.air.writeServerEvent('suspicious', avId, 'CogdoMazeGame.suitHit: toon not present?')
            return False

        result = True
        if self.lastRequestId == avId:
            self.requestCount += 1
            now = globalClock.getFrameTime()
            elapsed = now - self.requestStartTime
            if elapsed > 10:
                self.requestCount = 1
                self.requestStartTime = now
            else:
                secondsPerGrab = elapsed / self.requestCount
                if self.requestCount >= 3 and secondsPerGrab <= 0.4:
                    simbase.air.writeServerEvent('suspicious', avId, 'suitHit %s suits in %s seconds' % (self.requestCount, elapsed))
                    if simbase.config.GetBool('want-ban-cogdo-maze-suit-hit', False):
                        toon.ban('suitHit %s suits in %s seconds' % (self.requestCount, elapsed))

                    result = False

        else:
            self.lastRequestId = avId
            self.requestCount = 1
            self.requestStartTime = globalClock.getFrameTime()
        if result:
            self.suits[suitNum] -= 1
            hp = self.suits[suitNum]
            if hp <= 0:
                self.suitDestroyed(suitType, suitNum)

        return result

    def suitDestroyed(self, suitType, suitNum):
        if suitType == Globals.SuitTypes.Boss:
            self.bosses -= 1
            if self.bosses <= 0:
                self.openDoor()

        self.createPickups(suitType)
        del self.suits[suitNum]

    def createPickups(self, suitType):
        for i in range(Globals.SuitData[suitType]['memos']):
            self.pickups.append(self.pickupsDropped)
            self.pickupsDropped += 1

    def _removeToonFromGame(self, toonId):
        if self.fsm.getCurrentState().getName() == 'Game':
            if toonId not in self._toonId2speedToken:
                simbase.air.writeServerEvent('avoid_crash', toonId, 'CogdoMazeGame._removeToonFromGame: toon not in _toonId2speedToken')
            else:
                token = self._toonId2speedToken.pop(toonId)
                self._speedMonitor.removeNodepath(token)
            if self.areAllToonsInDoor():
                self._handleGameFinished()

    def handleToonDisconnected(self, toonId):
        DistCogdoGameAI.handleToonDisconnected(self, toonId)
        self._removeToonFromGame(toonId)

    def handleToonWentSad(self, toonId):
        DistCogdoGameAI.handleToonWentSad(self, toonId)
        self._removeToonFromGame(toonId)

    def getNumSuits(self):
        return list(self._numSuits)

    def d_broadcastPickup(self, senderId, pickupNum, networkTime):
        self.sendUpdate('pickUp', [
            senderId,
            pickupNum,
            networkTime])

    def requestAction(self, action, data):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestAction outside of Game state')
            return False

        if action == Globals.GameActions.EnterDoor:
            if not self.doorIsOpen:
                self.logSuspiciousEvent(senderId, "CogdoMazeGameAI.requestAction(EnterDoor): door isn't open yet")
            elif senderId not in self.toonsInDoor:
                self.toonsInDoor.append(senderId)
                self.d_broadcastDoAction(action, senderId)
                if self.areAllToonsInDoor():
                    self._handleGameFinished()

            else:
                self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestAction: toon already in door')
        elif action == Globals.GameActions.RevealDoor:
            self.d_broadcastDoAction(action, senderId)
        else:
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestAction: invalid action %s' % action)

    def d_broadcastDoAction(self, action, data = 0, networkTime = 0):
        self.sendUpdate('doAction', [
            action,
            data,
            networkTime])

    def requestUseGag(self, x, y, h, networkTime):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestUseGag outside of Game state')
            return False

        self.d_broadcastToonUsedGag(senderId, x, y, h, networkTime)

    def d_broadcastToonUsedGag(self, toonId, x, y, h, networkTime):
        self.sendUpdate('toonUsedGag', [
            toonId,
            x,
            y,
            h,
            networkTime])

    def requestSuitHitByGag(self, suitType, suitNum):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestSuitHitByGag outside of Game state')
            return False

        if suitType not in list(Globals.SuitTypes):
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestSuitHitByGag: invalid suit type %s' % suitType)
            return False

        if suitNum not in list(self.suits.keys()):
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestSuitHitByGag: invalid suit num %s' % suitNum)
            return False

        resultValid = self.suitHit(suitType, suitNum)
        if resultValid:
            self.d_broadcastSuitHitByGag(senderId, suitType, suitNum)

    def d_broadcastSuitHitByGag(self, toonId, suitType, suitNum):
        self.sendUpdate('suitHitByGag', [
            toonId,
            suitType,
            suitNum])

    def requestHitBySuit(self, suitType, suitNum, networkTime):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestHitBySuit outside of Game state')
            return False

        if suitType not in list(Globals.SuitTypes):
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestHitBySuit: invalid suit type %s' % suitType)
            return False

        if suitNum not in list(self.suits.keys()):
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestHitBySuit: invalid suit num %s' % suitNum)
            return False

        toon = self.air.doId2do[senderId]
        self.d_broadcastToonHitBySuit(senderId, suitType, suitNum, networkTime)
        damage = Globals.SuitData[suitType]['toonDamage']
        damage += int(round(damage * Globals.DamageModifier * self.difficulty))
        toon.takeDamage(damage, quietly = 0)

    def d_broadcastToonHitBySuit(self, toonId, suitType, suitNum, networkTime):
        self.sendUpdate('toonHitBySuit', [
            toonId,
            suitType,
            suitNum,
            networkTime])

    def requestHitByDrop(self):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestHitByDrop outside of Game state')
            return False

        toon = self.air.doId2do[senderId]
        self.d_broadcastToonHitByDrop(senderId)
        if Globals.DropDamage > 0:
            damage = Globals.DropDamage
            damage += int(round(damage * Globals.DamageModifier * self.difficulty))
            toon.takeDamage(damage, quietly = 0)

    def d_broadcastToonHitByDrop(self, toonId):
        self.sendUpdate('toonHitByDrop', [
            toonId])

    def requestGag(self, waterCoolerIndex):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestGag outside of Game state')
            return False

        if waterCoolerIndex >= len(self._waterCoolerPosList):
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestGag: invalid waterCoolerIndex')
            return

        wcPos = self._waterCoolerPosList[waterCoolerIndex]
        toon = self.air.doId2do.get(senderId)
        if not toon:
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestGag: toon not present')
            return

        distance = (toon.getPos() - wcPos).length()
        threshold = (Globals.WaterCoolerTriggerRadius + Globals.PlayerCollisionRadius) * 1.05
        if distance > threshold:
            self._toonHackingRequestGag(senderId)
            return

        self.d_broadcastHasGag(senderId, self.getCurrentNetworkTime())

    def d_broadcastHasGag(self, senderId, networkTime):
        self.sendUpdate('hasGag', [
            senderId,
            networkTime])

    def requestPickUp(self, pickupNum):
        senderId = self.air.getAvatarIdFromSender()
        if not self._validateSenderId(senderId):
            return False

        if self.fsm.getCurrentState().getName() != 'Game':
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestPickUp outside of Game state')
            return False

        if pickupNum not in self.pickups:
            self.logSuspiciousEvent(senderId, 'CogdoMazeGameAI.requestPickUp: invalid pickupNum %s' % pickupNum)
            return False

        toon = simbase.air.doId2do.get(senderId)
        if not toon:
            simbase.air.writeServerEvent('suspicious', senderId, 'CogdoMazeGame.requestPickUp: toon not present?')
            return False

        result = True
        if self.jokeLastRequestId == senderId:
            self.jokeRequestCount += 1
            now = globalClock.getFrameTime()
            elapsed = now - self.jokeRequestStartTime
            if elapsed > 10:
                self.jokeRequestCount = 1
                self.jokeRequestStartTime = now
            else:
                secondsPerGrab = elapsed / self.jokeRequestCount
                if self.jokeRequestCount >= 4 and secondsPerGrab <= 0.03:
                    simbase.air.writeServerEvent('suspicious', senderId, 'requestPickup %s jokes in %s seconds' % (self.jokeRequestCount, elapsed))
                    if simbase.config.GetBool('want-ban-cogdo-maze-request-pickup', False):
                        toon.ban('requestPickup %s jokes in %s seconds' % (self.jokeRequestCount, elapsed))

                    result = False

        else:
            self.jokeLastRequestId = senderId
            self.jokeRequestCount = 1
            self.jokeRequestStartTime = globalClock.getFrameTime()
        if result:
            self.numPickedUp += 1
            self.pickups.remove(pickupNum)
            self.d_broadcastPickup(senderId, pickupNum, self.getCurrentNetworkTime())

    def enterGame(self):
        DistCogdoGameAI.enterGame(self)
        endTime = Globals.SecondsUntilTimeout - (globalClock.getRealTime() - self.getStartTime())
        self._countdownTimerTask = taskMgr.doMethodLater(endTime - Globals.SecondsForTimeAlert, self.handleCountdownTimer, self.taskName(DistCogdoMazeGameAI.CountdownTimerTaskName), [])
        self._startGameTimer()
        self._timeoutTimerTask = taskMgr.doMethodLater(endTime, self.handleEndGameTimerExpired, self.taskName(DistCogdoMazeGameAI.TimeoutTimerTaskName), [])
        if self.SkipCogdoGames:
            self.fsm.request('Finish')

        for toonId in self.getToonIds():
            toon = self.air.doId2do.get(toonId)
            if toon:
                token = self._speedMonitor.addNodepath(toon)
                self._toonId2speedToken[toonId] = token
                self._speedMonitor.setSpeedLimit(token, config.GetFloat('cogdo-maze-speed-limit', Globals.ToonRunSpeed * 1.1), Functor(self._toonOverSpeedLimit, toonId))

    def _toonOverSpeedLimit(self, toonId, speed):
        self._bootPlayerForHacking(toonId, 'speeding in cogdo maze game (%.2f feet/sec)' % speed, config.GetBool('want-ban-cogdo-maze-speeding', 0))

    def _toonHackingRequestGag(self, toonId):
        simbase.air.writeServerEvent('suspicious', toonId, 'CogdoMazeGame: toon caught hacking requestGag')
        self._bootPlayerForHacking(toonId, 'hacking cogdo maze game requestGag', config.GetBool('want-ban-cogdo-maze-requestgag-hacking', 0))

    def _bootPlayerForHacking(self, toonId, reason, wantBan):
        toon = simbase.air.doId2do.get(toonId)
        if not toon:
            simbase.air.writeServerEvent('suspicious', toonId, 'CogdoMazeGame._bootPlayerForHacking(%s): toon not present' % (reason,))
            return

        if wantBan:
            toon.ban(reason)

    def _removeEndGameTimerTask(self):
        if hasattr(self, '_gameTimerExpiredTask'):
            taskMgr.remove(self._gameTimerExpiredTask)
            del self._gameTimerExpiredTask

    def _removeTimeoutTimerTask(self):
        if hasattr(self, '_timeoutTimerTask'):
            taskMgr.remove(self._timeoutTimerTask)
            del self._timeoutTimerTask

    def _removeCountdownTimerTask(self):
        if hasattr(self, '_countdownTimerTask'):
            taskMgr.remove(self._countdownTimerTask)
            del self._countdownTimerTask

    def _startGameTimer(self):
        self.d_broadcastDoAction(Globals.GameActions.Countdown, networkTime = self.getCurrentNetworkTime())

    def openDoor(self):
        self._removeTimeoutTimerTask()
        self._removeCountdownTimerTask()
        self.doorIsOpen = True
        self.d_broadcastDoAction(Globals.GameActions.OpenDoor, networkTime = self.getCurrentNetworkTime())
        self._gameTimerExpiredTask = taskMgr.doMethodLater(Globals.SecondsUntilGameEnds, self.handleEndGameTimerExpired, self.taskName(DistCogdoMazeGameAI.TimerExpiredTaskName), [])

    def handleCountdownTimer(self):
        self.d_broadcastDoAction(Globals.GameActions.TimeAlert, networkTime = self.getCurrentNetworkTime())

    def handleEndGameTimerExpired(self):
        self._handleGameFinished()

    def exitGame(self):
        DistCogdoGameAI.exitGame(self)
        for (toonId, token) in self._toonId2speedToken.items():
            self._speedMonitor.removeNodepath(token)

        self._toonId2speedToken = {}
        self._removeTimeoutTimerTask()
        self._removeEndGameTimerTask()

    def enterFinish(self):
        DistCogdoGameAI.enterFinish(self)
        if self.numPickedUp > self.maxPickups:
            self.logSuspiciousEvent(0, 'CogdoMazeGameAI: collected more memos than possible: %s, players: %s' % (self.numPickedUp, self.getToonIds()))

        time = globalClock.getRealTime() - self.getStartTime()
        adjustedTime = min(max(time - cogdoMazePerfectTime, 0), cogdoMazeMaxTime)
        timeScore = 1 - adjustedTime / cogdoMazeMaxTime
        pickupScore = 0
        if self.maxPickups:
            pickupScore = float(self.numPickedUp) / self.maxPickups

        weightedPickup = pickupScore * cogdoMazePickupScoreRatio
        weightedTime = timeScore * cogdoMazeTimeScoreRatio
        score = min(weightedPickup + weightedTime, 1.0)
        self.air.writeServerEvent('CogdoMazeGame', self._interior.toons, 'Memos: %s/%s Weighted Memos: %s Time: %s Weighted Time: %s Score: %s' % (self.numPickedUp, self.maxPickups, weightedPickup, time, weightedTime, score))
        self.setScore(score)
        self._announceGameDoneTask = taskMgr.doMethodLater(Globals.FinishDurationSeconds, self.announceGameDone, self.taskName(DistCogdoMazeGameAI.AnnounceGameDoneTimerTaskName), [])

    def exitFinish(self):
        DistCogdoGameAI.exitFinish(self)
        taskMgr.remove(self._announceGameDoneTask)
        del self._announceGameDoneTask