from panda3d.core import *
from direct.showbase.PythonUtil import weightedChoice, randFloat, Functor
from direct.showbase.PythonUtil import list2dict
from direct.showbase import DirectObject
from direct.distributed import DistributedObject, DistributedObjectAI
from direct.directnotify import DirectNotifyGlobal
from direct.task import Task
from direct.fsm import FSM
from toontown.pets import PetConstants, PetObserve, PetGoal, PetGoalMgr
from toontown.pets import PetTricks, PetLookerAI
import random, types

class PetBrain(DirectObject.DirectObject):
    notify = DirectNotifyGlobal.directNotify.newCategory('PetBrain')

    def __init__(self, pet):
        self.pet = pet
        self.focus = None
        self.started = 0
        self.inMovie = 0
        self.chaseNode = self.pet.getRender().attachNewNode('PetChaseNode')
        self.goalMgr = PetGoalMgr.PetGoalMgr(self.pet)
        self.doId2goals = {}
        self.nearbyAvs = {}
        self.avAwareness = {}
        self.lastInteractTime = {}
        self.nextAwarenessIndex = 0

    def destroy(self):
        taskMgr.remove(self.getTeleportTaskName())
        self.stop()
        self.goalMgr.destroy()
        self.chaseNode.removeNode()
        del self.chaseNode
        del self.focus
        del self.pet
        if self.doId2goals:
            self.notify.warning('destroy(): self.doId2goals is not empty: %s' % self.doId2goals.keys())
            for goalList in self.doId2goals.values():
                for goal in goalList:
                    goal.destroy()

        del self.doId2goals
        del self.avAwareness

    def getThinkTaskName(self):
        return 'petThink-%s' % self.pet.doId

    def getTeleportTaskName(self):
        return 'petTeleport-%s' % self.pet.doId

    def getObserveEventAttendedByAvStart(self, otherDoId):
        return 'petObserveAttendedByAvStart-%s-%s' % (self.pet.doId, otherDoId)

    def getObserveEventAttendedByAvStop(self, otherDoId):
        return 'petObserveAttendedByAvStop-%s-%s' % (self.pet.doId, otherDoId)

    def getObserveEventAttendingAvStart(self, otherDoId):
        return 'petObserveAttendingAvStart-%s-%s' % (self.pet.doId, otherDoId)

    def getObserveEventAttendingAvStop(self, otherDoId):
        return 'petObserveAttendingAvStop-%s-%s' % (self.pet.doId, otherDoId)

    def start(self):
        PetBrain.notify.debug('start: %s' % self.pet.doId)
        self.lookers = {}
        self.lookees = {}
        self.accept(PetLookerAI.getStartLookedAtByOtherEvent(self.pet.doId), self._handleLookedAtByOtherStart)
        self.accept(PetLookerAI.getStopLookedAtByOtherEvent(self.pet.doId), self._handleLookedAtByOtherStop)
        self.accept(PetLookerAI.getStartLookingAtOtherEvent(self.pet.doId), self._handleLookingAtOtherStart)
        self.accept(PetLookerAI.getStopLookingAtOtherEvent(self.pet.doId), self._handleLookingAtOtherStop)
        self.globalGoals = [PetGoal.Wander()]
        for goal in self.globalGoals:
            self.goalMgr.addGoal(goal)

        for doId in self.pet._getNearbyAvatarDict():
            self._handleAvatarArrive(doId)

        self.tLastLonelinessUpdate = globalClock.getFrameTime()
        taskMgr.doMethodLater(simbase.petThinkPeriod * random.random(), self._think, self.getThinkTaskName())
        self.started = 1

    def stop(self):
        PetBrain.notify.debug('stop: %s' % self.pet.doId)
        if not self.started:
            return
        self.started = 0
        del self.lookers
        del self.lookees
        for doId in self.pet._getNearbyAvatarDict():
            self._handleAvatarLeave(doId)

        for goal in self.globalGoals:
            self.goalMgr.removeGoal(goal)
            goal.destroy()

        del self.globalGoals
        self.clearFocus()
        taskMgr.remove(self.getThinkTaskName())
        self.ignore(PetLookerAI.getStartLookedAtByOtherEvent(self.pet.doId))
        self.ignore(PetLookerAI.getStopLookedAtByOtherEvent(self.pet.doId))
        self.ignore(PetLookerAI.getStartLookingAtOtherEvent(self.pet.doId))
        self.ignore(PetLookerAI.getStopLookingAtOtherEvent(self.pet.doId))

    def observe(self, petObserve):
        if petObserve.isForgettable():
            if random.random() < 0.05 * self.pet.traits.forgetfulness:
                return
        petObserve._influence(self)

    def updateLastInteractTime(self, avId):
        if avId in self.lastInteractTime:
            self.lastInteractTime[avId] = globalClock.getFrameTime()

    def _think(self, task = None):
        if not self.inMovie:
            self._updatePriorities()
            if len(self.nearbyAvs) > PetConstants.MaxAvatarAwareness:
                self.nextAwarenessIndex %= len(self.nearbyAvs)
                self._considerBecomeAwareOf(self.nearbyAvs.keys()[self.nextAwarenessIndex])
                self.nextAwarenessIndex += 1
            curT = globalClock.getFrameTime()
            tSinceLastLonelinessUpdate = curT - self.tLastLonelinessUpdate
            if tSinceLastLonelinessUpdate >= PetConstants.LonelinessUpdatePeriod:
                self.tLastLonelinessUpdate = curT
                numLookers = len(self.lookers)
                if numLookers:
                    dt = tSinceLastLonelinessUpdate
                    self.pet.lerpMood('loneliness', max(-1.0, dt * -.003 * numLookers))
                    if numLookers > 5:
                        self.pet.lerpMood('excitement', min(1.0, dt * 0.001 * numLookers))
        taskMgr.doMethodLater(simbase.petThinkPeriod, self._think, self.getThinkTaskName())
        return Task.done

    def _updatePriorities(self):
        self.goalMgr.updatePriorities()

    def _handleLookingAtOtherStart(self, avId):
        if avId in self.lookees:
            PetBrain.notify.warning('%s: already looking at av %s' % (self.pet.doId, avId))
            return
        self.lookees[avId] = avId
        self.observe(PetObserve.PetActionObserve(PetObserve.Actions.ATTENDING_START, avId))

    def _handleLookingAtOtherStop(self, avId):
        if avId not in self.lookees:
            PetBrain.notify.warning('%s: not looking at av %s' % (self.pet.doId, avId))
            return
        del self.lookees[avId]
        self.observe(PetObserve.PetActionObserve(PetObserve.Actions.ATTENDING_STOP, avId))

    def _handleLookedAtByOtherStart(self, avId):
        if avId in self.lookers:
            PetBrain.notify.warning('%s: av %s already looking at me' % (self.pet.doId, avId))
            return
        self.lookers[avId] = avId
        self.observe(PetObserve.PetActionObserve(PetObserve.Actions.ATTENDED_START, avId))

    def _handleLookedAtByOtherStop(self, avId):
        if avId not in self.lookers:
            PetBrain.notify.warning('%s: av %s not looking at me' % (self.pet.doId, avId))
            return
        del self.lookers[avId]
        self.observe(PetObserve.PetActionObserve(PetObserve.Actions.ATTENDED_STOP, avId))

    def lookedAtBy(self, avId):
        return avId in self.lookers

    def lookingAt(self, avId):
        return avId in self.lookees

    def getAvIdsLookingAtUs(self):
        return self.lookers

    def getAvIdsWeAreLookingAt(self):
        return self.lookees

    def setFocus(self, object):
        if isinstance(self.focus, DistributedObjectAI.DistributedObjectAI):
            self.ignore(self.focus.getDeleteEvent())
            self.lastInteractTime.setdefault(self.focus.doId, 0)
        PetBrain.notify.debug('setFocus: %s' % object)
        self.focus = object
        if isinstance(self.focus, DistributedObjectAI.DistributedObjectAI):
            self.accept(self.focus.getDeleteEvent(), self._handleFocusHasLeft)

    def getFocus(self):
        return self.focus

    def clearFocus(self):
        self.setFocus(None)
        return

    def _handleFocusHasLeft(self):
        if self.focus.isEmpty():
            self.chaseNode.setPos(self.pet, 0, 0, 0)
        else:
            self.chaseNode.setPos(self.focus, 0, 0, 0)
        self._inspectSpot(self.chaseNode)

    def _chase(self, target):
        if callable(target):
            target = target()
        if target is None:
            return 0
        self.setFocus(target)
        self.pet.actionFSM.request('Chase', target)
        return 1

    def _wander(self):
        self.clearFocus()
        self.pet.actionFSM.request('Wander')
        return 1

    def _unstick(self):
        self.clearFocus()
        self.pet.actionFSM.request('Unstick')
        return 1

    def _flee(self, chaser):
        if callable(chaser):
            chaser = chaser()
        if chaser is None:
            return 0
        self.setFocus(chaser)
        self.pet.actionFSM.request('Flee', chaser)
        return 1

    def _inspectSpot(self, spot = None):
        if spot is None:
            spot = NodePath('randomSpot')
            spot.setPos(randFloat(-20, 20), randFloat(-20, 20), 0)
        self.setFocus(spot)
        self.pet.actionFSM.request('InspectSpot', spot)
        return 1

    def _stay(self, avatar):
        self.setFocus(avatar)
        self.pet.actionFSM.request('Stay', avatar)
        return 1

    def _doTrick(self, trickId, avatar):
        self.setFocus(avatar)
        self.pet.actionFSM.request('Trick', avatar, trickId)
        return 1

    def _heal(self, avatar):
        if callable(avatar):
            avatar = avatar()
        if avatar is None:
            return 0
        self.setFocus(avatar)
        self.pet.actionFSM.request('Heal', avatar)
        return 1

    def _startMovie(self):
        self.setFocus(None)
        self.pet.actionFSM.request('Movie')
        self.inMovie = 1
        return

    def _endMovie(self):
        self.inMovie = 0

    def _handleGenericObserve(self, observe):
        pass

    def _handleActionObserve(self, observe):
        action = observe.getAction()
        avId = observe.getAvId()
        OA = PetObserve.Actions
        dbg = PetBrain.notify.debug
        if action == OA.ATTENDED_START:
            dbg('avatar %s is looking at me' % avId)
            self.pet.lerpMoods({'boredom': -.1,
             'excitement': 0.05,
             'loneliness': -.05})
            messenger.send(self.getObserveEventAttendedByAvStart(avId))
        elif action == OA.ATTENDED_STOP:
            dbg('avatar %s is no longer looking at me' % avId)
            messenger.send(self.getObserveEventAttendedByAvStop(avId))
        elif action == OA.ATTENDING_START:
            dbg('I am looking at avatar %s' % avId)
            messenger.send(self.getObserveEventAttendingAvStart(avId))
        elif action == OA.ATTENDING_STOP:
            dbg('I am no longer looking at avatar %s' % avId)
            messenger.send(self.getObserveEventAttendingAvStop(avId))
        elif action == OA.CHANGE_ZONE:
            if avId != self.pet.doId:
                oldZoneId, newZoneId = observe.getData()
                PetBrain.notify.debug('%s.CHANGE_ZONE: %s, %s->%s' % (self.pet.doId,
                 avId,
                 oldZoneId,
                 newZoneId))
                myZoneId = self.pet.zoneId
                if newZoneId != oldZoneId:
                    if newZoneId == myZoneId:
                        self._handleAvatarArrive(avId)
                    elif oldZoneId == myZoneId:
                        self._handleAvatarLeave(avId)
                if self.pet.inEstate:
                    if avId in (self.pet.ownerId, self.pet.estateOwnerId):
                        if oldZoneId in self.pet.estateZones and newZoneId not in self.pet.estateZones:
                            if avId == self.pet.ownerId:
                                self._handleOwnerLeave()
                            else:
                                self._handleEstateOwnerLeave()
        elif action == OA.LOGOUT:
            if avId == self.pet.ownerId:
                self._handleOwnerLeave()
            elif avId == self.pet.estateOwnerId:
                self._handleEstateOwnerLeave()
        elif action == OA.FEED:
            dbg('avatar %s is feeding me' % avId)
            self.pet.lerpMoods({'affection': 0.35,
             'anger': -.07,
             'boredom': -.5,
             'excitement': 0.5,
             'fatigue': -.2,
             'hunger': -.5,
             'loneliness': -.08,
             'playfulness': 0.1,
             'restlessness': -.05,
             'sadness': -.2})
            self.updateLastInteractTime(avId)
            avatar = simbase.air.doId2do.get(avId)
            if avatar is not None:
                avatar.setHatePets(0)
        elif action == OA.SCRATCH:
            dbg('avatar %s is scratching me' % avId)
            self.pet.lerpMoods({'affection': 0.45,
             'anger': -.1,
             'boredom': -.8,
             'excitement': 0.5,
             'fatigue': -.25,
             'loneliness': -.2,
             'playfulness': 0.1,
             'restlessness': -.2,
             'sadness': -.2})
            self.updateLastInteractTime(avId)
            avatar = simbase.air.doId2do.get(avId)
            if avatar is not None:
                avatar.setHatePets(0)
        elif action == OA.GARDEN:
            dbg('avatar %s is gardening' % avId)
            avatar = simbase.air.doId2do.get(avId)
            if avatar is not None:
                if self.getFocus() == avatar:
                    self._wander()
        return

    def _handlePhraseObserve(self, observe):

        def _handleGettingFriendlyAttention(avId, self = self):
            self.pet.lerpMoods({'boredom': -.85,
             'restlessness': -.1,
             'playfulness': 0.2,
             'loneliness': -.4,
             'sadness': -.1,
             'fatigue': -.05,
             'excitement': 0.05,
             'anger': -.05})
            self.updateLastInteractTime(avId)

        def _handleComeHere(avId, self = self):
            avatar = simbase.air.doId2do.get(avId)
            if avatar:
                self.pet.mover.walkToAvatar(avatar)
                avatar.setHatePets(0)

        def _handleFollowMe(avId, self = self):
            avatar = simbase.air.doId2do.get(avId)
            if avatar:
                self.pet.mover.walkToAvatar(avatar)
                avatar.setHatePets(0)

        def _handleStay(avId, self = self):
            avatar = simbase.air.doId2do.get(avId)
            if avatar:
                self._stay(avatar)

        def _handleCriticism(avId, self = self):
            ownerFactor = 0.5
            if avId == self.pet.ownerId:
                ownerFactor = 1.0
            self.pet.lerpMoods({'affection': -.4,
             'anger': 0.4,
             'boredom': -.3,
             'confusion': 0.05,
             'fatigue': 0.2,
             'playfulness': -.1,
             'sadness': 0.5 * ownerFactor})

        def _handleGoAway(avId, self = self):
            avatar = simbase.air.doId2do.get(avId)
            if avatar is not None:
                if self.getFocus() == avatar:
                    self._wander()
            return

        def _handleDoTrick(trickId, avId, self = self):
            looked = self.lookedAtBy(avId) or config.GetBool('pet-brain-ignore-looked-tricks', True)
            avatar = simbase.air.doId2do.get(avId)
            if avatar:
                if looked:
                    if not self.pet._willDoTrick(trickId):
                        trickId = PetTricks.Tricks.BALK
                    self._doTrick(trickId, avatar)

        phrase = observe.getPetPhrase()
        avId = observe.getAvId()
        OP = PetObserve.Phrases
        if phrase in list2dict([OP.COME,
         OP.FOLLOW_ME,
         OP.STAY,
         OP.NEED_LAFF,
         OP.NEED_GAGS,
         OP.NEED_JB,
         OP.HI,
         OP.SOOTHE,
         OP.PRAISE,
         OP.HAPPY,
         OP.QUESTION,
         OP.FRIENDLY,
         OP.LETS_PLAY,
         OP.DO_TRICK]):
            _handleGettingFriendlyAttention(avId)
        if phrase == OP.COME:
            _handleComeHere(avId)
        if phrase == OP.FOLLOW_ME:
            _handleFollowMe(avId)
        if phrase == OP.STAY:
            _handleStay(avId)
        if phrase == OP.CRITICISM:
            _handleCriticism(avId)
        if phrase == OP.GO_AWAY:
            _handleGoAway(avId)
        if phrase == OP.DO_TRICK:
            _handleDoTrick(observe.getTrickId(), avId)

    def _addGoalsReAvatar(self, avId):
        av = self.pet.air.doId2do.get(avId)
        if av is None:
            PetBrain.notify.warning('%s._addGoalsReAvatar: %s not in doId2do' % (self.pet.doId, avId))
            return
        if avId not in self.doId2goals:
            goals = [PetGoal.ChaseAvatar(av), PetGoal.FleeFromAvatar(av)]
            self.doId2goals[avId] = goals
            self.lastInteractTime.setdefault(avId, 0)
        for goal in self.doId2goals[avId]:
            self.goalMgr.addGoal(goal)

        return

    def _removeGoalsReAvatar(self, avId):
        if avId not in self.doId2goals:
            PetBrain.notify.warning('no goals re av %s to remove' % avId)
            return
        for goal in self.doId2goals[avId]:
            self.goalMgr.removeGoal(goal)
            goal.destroy()

        del self.doId2goals[avId]

    def _considerBecomeAwareOf(self, avId):
        av = simbase.air.doId2do.get(avId)
        if av is None:
            PetBrain.notify.warning('_considerBecomeAwareOf: av %s does not exist' % avId)
            return
        if avId in self.avAwareness:
            return

        def becomeAwareOf(avId, self = self):
            self.avAwareness[avId] = None
            self._addGoalsReAvatar(avId)
            return

        if len(self.avAwareness) < PetConstants.MaxAvatarAwareness:
            becomeAwareOf(avId)
            return

        def calcInterest(avId, self = self):
            if avId == self.pet.ownerId:
                return 100.0
            return random.random()

        avInterest = calcInterest(avId)
        minInterest = avInterest
        minInterestAvId = avId
        for awAvId in self.avAwareness:
            i = calcInterest(awAvId)
            if i < minInterest:
                minInterest = i
                minInterestAvId = awAvId
                break

        if minInterestAvId != avId:
            self._removeAwarenessOf(minInterestAvId)
            becomeAwareOf(avId)
        return

    def _removeAwarenessOf(self, avId):
        if avId in self.avAwareness:
            self._removeGoalsReAvatar(avId)
            del self.avAwareness[avId]

    def _handleAvatarArrive(self, avId):
        PetBrain.notify.debug('%s._handleAvatarArrive: %s' % (self.pet.doId, avId))
        if avId in self.nearbyAvs:
            PetBrain.notify.warning('%s already in self.nearbyAvs' % avId)
            return
        self.nearbyAvs[avId] = None
        excitement = 0.3
        if avId == self.pet.ownerId:
            excitement = 0.7
        self.pet.lerpMoods({'excitement': 0.7,
         'loneliness': -.4})
        self._considerBecomeAwareOf(avId)
        return

    def _handleAvatarLeave(self, avId):
        PetBrain.notify.debug('%s._handleAvatarLeave: %s' % (self.pet.doId, avId))
        if avId not in self.nearbyAvs:
            PetBrain.notify.warning('av %s not in self.nearbyAvs' % avId)
            return
        del self.nearbyAvs[avId]
        self.pet.lerpMoods({'loneliness': 0.1})
        self._removeAwarenessOf(avId)

    def _handleOwnerLeave(self):
        self.pet.teleportOut()
        taskMgr.doMethodLater(PetConstants.TELEPORT_OUT_DURATION, self.pet.requestDelete, self.getTeleportTaskName())

    def _handleEstateOwnerLeave(self):
        self.pet.teleportOut()
        taskMgr.doMethodLater(PetConstants.TELEPORT_OUT_DURATION, self.pet.requestDelete, self.getTeleportTaskName())