from direct.distributed import DistributedObjectAI
from direct.directnotify import DirectNotifyGlobal
from toontown.toonbase import ToontownGlobals
from toontown.golf import DistributedGolfHoleAI
from panda3d.core import *
from direct.fsm.FSM import FSM
from toontown.ai.ToonBarrier import *
from toontown.golf import GolfGlobals
import functools
INITIAL = 0
EXITED = 1
EXPECTED = 2
JOINED = 3
READY = 4
ONHOLE = 5
BALLIN = 6
JOIN_TIMEOUT = 30
READY_TIMEOUT = 30
EXIT_TIMEOUT = 30
REWARD_TIMEOUT = 30

class DistributedGolfCourseAI(DistributedObjectAI.DistributedObjectAI, FSM):
    notify = directNotify.newCategory('DistributedGolfCourseAI')
    defaultTransitions = {'Off': ['WaitJoin'], 'WaitJoin': ['WaitReadyCourse', 'Cleanup'], 'WaitReadyCourse': ['WaitReadyHole', 'Cleanup'], 'WaitReadyHole': ['PlayHole', 'Cleanup', 'WaitLeaveHole', 'WaitReward'], 'PlayHole': ['PlayHole', 'WaitLeaveHole', 'Cleanup', 'WaitReward'], 'WaitLeaveHole': ['WaitReadyHole', 'WaitLeaveCourse', 'Cleanup', 'WaitReward'], 'WaitReward': ['WaitLeaveCourse', 'Cleanup', 'WaitLeaveHole'], 'WaitLeaveCourse': ['Cleanup'], 'Cleanup': ['Off']}

    def __init__(self, zoneId, avIds, courseId, preferredHoleId=None):
        FSM.__init__(self, 'GolfCourse_%s_FSM' % zoneId)
        DistributedObjectAI.DistributedObjectAI.__init__(self, simbase.air)
        self.notify.debug('GOLF COURSE: init')
        self.zoneId = zoneId
        self.currentHole = None
        self.avIdList = []
        self.avStateDict = {}
        self.addExpectedGolfers(avIds)
        self.courseId = courseId
        self.preferredHoleId = preferredHoleId
        self.courseInfo = GolfGlobals.CourseInfo[self.courseId]
        self.numHoles = self.courseInfo['numHoles']
        self.holeIds = self.calcHolesToUse()
        self.notify.debug('self.holeIds = %s' % self.holeIds)
        self.numHolesPlayed = 0
        self.curHoleIndex = 0
        self.trophyListLen = 0
        self.courseBestListLen = 0
        self.holeBestListLen = 0
        self.cupListLen = 0
        self.scores = {}
        self.aimTimes = {}
        self.startingHistory = {}
        self.endingHistory = {}
        self.startingHoleBest = {}
        self.endingHoleBest = {}
        self.startingCourseBest = {}
        self.endingCourseBest = {}
        self.startingCups = {}
        self.endingCups = {}
        self.initHistory()
        self.newTrophies = {}
        self.newHoleBest = {}
        self.newCourseBest = {}
        self.newCups = {}
        self.drivingToons = []
        self.__barrier = None
        self.winnerByTieBreak = 0
        return

    def initHistory(self):
        for avId in self.avIdList:
            av = simbase.air.doId2do.get(avId)
            if av:
                history = av.getGolfHistory()
                self.startingHistory[avId] = history[:]
                self.endingHistory[avId] = history[:]
                holeBest = av.getGolfHoleBest()
                self.startingHoleBest[avId] = holeBest[:]
                self.endingHoleBest[avId] = holeBest[:]
                courseBest = av.getGolfCourseBest()
                self.startingCourseBest[avId] = courseBest[:]
                self.endingCourseBest[avId] = courseBest[:]

    def generate(self):
        DistributedObjectAI.DistributedObjectAI.generate(self)
        self.grabGolfers()

    def delete(self):
        self.notify.debug('GOLF COURSE: delete: deleting AI GolfCourse object')
        if hasattr(self, 'rewardBarrier'):
            self.rewardBarrier.cleanup()
            del self.rewardBarrier
        if self.currentHole:
            self.notify.debug('calling requestDelete on hole %d' % self.currentHole.doId)
            self.currentHole.requestDelete()
            self.currentHole = None
        self.ignoreAll()
        from toontown.golf import GolfManagerAI
        GolfManagerAI.GolfManagerAI().removeCourse(self)
        if self.__barrier:
            self.__barrier.cleanup()
            self.__barrier = None
        DistributedObjectAI.DistributedObjectAI.delete(self)
        return

    def load(self):
        self.b_setCourseReady()
        self.request('WaitReadyCourse')

    def getZoneId(self):
        return self.zoneId

    def addExpectedGolfers(self, avIdList):
        self.notify.debug('Sending %s to course %s' % (avIdList, self.zoneId))
        for avId in avIdList:
            golfer = simbase.air.doId2do.get(avId)
            if golfer:
                if avId not in self.avIdList:
                    self.avIdList.append(avId)
                    self.avStateDict[avId] = INITIAL
                elif self.avStateDict[avId] == EXITED:
                    if self.isGenerated():
                        pass
                else:
                    self.notify.warning('GOLF COURSE: trying to grab golfer %s that is already on the course' % avId)

    def grabGolfers(self):
        for avId in self.avIdList:
            golfer = simbase.air.doId2do.get(avId)
            if golfer:
                if self.avStateDict[avId] == INITIAL:
                    self.avStateDict[avId] = EXPECTED

        self.request('WaitJoin')

    def getGolferIds(self):
        return self.avIdList

    def checkGolferPlaying(self, avId):
        if self.avStateDict[avId] == ONHOLE:
            return 1
        else:
            return 0

    def b_setCourseReady(self):
        self.setCourseReady()
        self.d_setCourseReady()

    def d_setCourseReady(self):
        self.notify.debug('GOLF COURSE: Sending setCourseReady')
        self.sendUpdate('setCourseReady', [self.numHoles, self.holeIds, self.calcCoursePar()])

    def setCourseReady(self):
        self.notify.debug('GOLF COURSE: setCourseReady: golf course ready with avatars: %s' % self.avIdList)
        self.trophyListLen = 0
        self.courseBestListLen = 0
        self.holeBestListLen = 0
        self.cupListLen = 0
        self.normalExit = 1

    def d_setPlayHole(self):
        self.notify.debug('GOLF COURSE: setPlayHole: play on golf hole about to start')
        self.sendUpdate('setPlayHole', [])

    def b_setCourseExit(self):
        self.d_setCourseExit()
        self.setCourseExit()

    def d_setCourseExit(self):
        self.notify.debug('GOLF COURSE: Sending setGameExit')
        self.sendUpdate('setCourseExit', [])

    def setCourseExit(self):
        self.notify.debug('GOLF COURSE: setGameExit')

    def handleExitedAvatar(self, avId):
        self.notify.warning('GOLF COURSE: handleExitedAvatar: avatar id exited: ' + str(avId))
        self.avStateDict[avId] = EXITED
        self.sendUpdate('avExited', [avId])
        if self.currentHole and not self.haveAllGolfersExited():
            self.currentHole.avatarDropped(avId)
        if self.haveAllGolfersExited():
            self.setCourseAbort()
        elif self.isCurHoleDone():
            if self.isPlayingLastHole():
                if self.state not in ['WaitReward', 'WaitReadyHole']:
                    self.safeDemand('WaitReward')
            else:
                self.notify.debug('allBalls are in holes, calling holeOver')
                self.holeOver()
        if hasattr(self, 'rewardBarrier'):
            if self.rewardBarrier:
                self.rewardBarrier.clear(avId)
        if hasattr(self, '__barrier'):
            if self.__barrier:
                self.__.clear(avId)

    def startNextHole(self):
        self.notify.debugStateCall(self)
        holeId = self.holeIds[self.numHolesPlayed]
        self.currentHole = DistributedGolfHoleAI.DistributedGolfHoleAI(self.zoneId, golfCourse=self, holeId=holeId)
        self.currentHole.generateWithRequired(self.zoneId)
        self.d_setCurHoleDoId(self.currentHole.doId)
        self.safeDemand('WaitReadyHole')

    def holeOver(self):
        self.notify.debug('GOLF COURSE: holeOver')
        self.numHolesPlayed += 1
        if self.numHolesPlayed < self.numHoles:
            self.b_setCurHoleIndex(self.numHolesPlayed)
        self.safeDemand('WaitLeaveHole')

    def setCourseAbort(self):
        self.notify.debug('GOLF COURSE: setGameAbort')
        self.normalExit = 0
        self.sendUpdate('setCourseAbort', [0])
        self.safeDemand('Cleanup')

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

    def exitOff(self):
        self.notify.debug('GOLF COURSE: exitOff')

    def enterWaitJoin(self):
        self.notify.debug('GOLF COURSE: enterWaitJoin')
        for avId in self.avIdList:
            self.avStateDict[avId] = EXPECTED
            self.acceptOnce(self.air.getAvatarExitEvent(avId), self.handleExitedAvatar, extraArgs=[avId])

        def allAvatarsJoined(self=self):
            self.notify.debug('GOLF COURSE: all avatars joined')
            self.load()

        def handleTimeout(avIds, self=self):
            self.notify.debug('GOLF COURSE: timed out waiting for clients %s to join' % avIds)
            for avId in self.avStateDict:
                if not self.avStateDict[avId] == JOINED:
                    self.handleExitedAvatar(avId)

            if self.haveAllGolfersExited():
                self.setCourseAbort()
            else:
                self.load()

        self.__barrier = ToonBarrier('waitClientsJoin', self.uniqueName('waitClientsJoin'), self.avIdList, JOIN_TIMEOUT, allAvatarsJoined, handleTimeout)

    def exitWaitJoin(self):
        self.notify.debugStateCall(self)
        self.__barrier.cleanup()
        self.__barrier = None
        return

    def setAvatarJoined(self):
        avId = self.air.getAvatarIdFromSender()
        self.notify.debug('GOLF COURSE: setAvatarJoined: avatar id joined: ' + str(avId))
        self.avStateDict[avId] = JOINED
        self.notify.debug('GOLF COURSE: setAvatarJoined: new states: ' + str(self.avStateDict))
        if hasattr(self, '_DistributedGolfCourseAI__barrier') and self.__barrier:
            self.__barrier.clear(avId)
        else:
            self.notify.warning('setAvatarJoined avId=%d but barrier is invalid' % avId)

    def exitFrameworkWaitClientsJoin(self):
        self.__barrier.cleanup()
        del self.__barrier

    def enterWaitReadyCourse(self):
        self.notify.debug('GOLF COURSE: enterWaitReadyCourse')

        def allAvatarsInCourse(self=self):
            self.notify.debug('GOLF COURSE: all avatars ready course')
            for avId in self.avIdList:
                blankScoreList = [
                 0] * self.numHoles
                self.scores[avId] = blankScoreList
                self.aimTimes[avId] = 0

            self.notify.debug('self.scores = %s' % self.scores)
            self.startNextHole()

        def handleTimeout(avIds, self=self):
            self.notify.debug("GOLF COURSE: Course timed out waiting for clients %s to report 'ready'" % avIds)
            if self.haveAllGolfersExited():
                self.setCourseAbort()
            else:
                allAvatarsInCourse()

        self.__barrier = ToonBarrier('WaitReadyCourse', self.uniqueName('WaitReadyCourse'), self.avIdList, READY_TIMEOUT, allAvatarsInCourse, handleTimeout)
        for avId in list(self.avStateDict.keys()):
            if self.avStateDict[avId] == READY:
                self.__barrier.clear(avId)

    def setAvatarReadyCourse(self):
        avId = self.air.getAvatarIdFromSender()
        self.notify.debug('GOLF COURSE: setAvatarReadyCourse: avatar id ready: ' + str(avId))
        self.avStateDict[avId] = READY
        self.notify.debug('GOLF COURSE: setAvatarReadyCourse: new avId states: ' + str(self.avStateDict))
        if self.state == 'WaitReadyCourse':
            self.__barrier.clear(avId)

    def exitWaitReadyCourse(self):
        self.notify.debugStateCall(self)
        self.__barrier.cleanup()
        self.__barrier = None
        return

    def enterWaitReadyHole(self):
        self.notify.debug('GOLF COURSE: enterWaitReadyHole')

        def allAvatarsInHole(self=self):
            self.notify.debug('GOLF COURSE: all avatars ready hole')
            if self.safeDemand('PlayHole'):
                self.d_setPlayHole()

        def handleTimeout(avIds, self=self):
            self.notify.debug("GOLF COURSE: Hole timed out waiting for clients %s to report 'ready'" % avIds)
            if self.haveAllGolfersExited():
                self.setCourseAbort()
            else:
                if self.safeDemand('PlayHole'):
                    self.d_setPlayHole()

        stillPlaying = self.getStillPlayingAvIds()
        self.__barrier = ToonBarrier('WaitReadyHole', self.uniqueName('WaitReadyHole'), stillPlaying, READY_TIMEOUT, allAvatarsInHole, handleTimeout)
        for avId in list(self.avStateDict.keys()):
            if self.avStateDict[avId] == ONHOLE:
                self.__barrier.clear(avId)

    def exitWaitReadyHole(self):
        self.notify.debugStateCall(self)
        if hasattr(self, '__barrier'):
            self.__barrier.cleanup()
            self.__barrier = None
        return

    def getStillPlayingAvIds(self):
        retval = []
        for avId in self.avIdList:
            av = simbase.air.doId2do.get(avId)
            if av:
                if avId in self.avStateDict and not self.avStateDict[avId] == EXITED:
                    retval.append(avId)

        return retval

    def avatarReadyHole(self, avId):
        if self.state not in ['WaitJoin', 'WaitReadyCourse', 'WaitReadyHole']:
            self.notify.debug('GOLF COURSE: Ignoring setAvatarReadyHole message')
            return
        self.notify.debug('GOLF COURSE: setAvatarReadyHole: avatar id ready: ' + str(avId))
        self.avStateDict[avId] = ONHOLE
        self.notify.debug('GOLF COURSE: setAvatarReadyHole: new avId states: ' + str(self.avStateDict))
        if self.state == 'WaitReadyHole':
            self.__barrier.clear(avId)

    def enterPlayHole(self):
        self.notify.debug('GOLF COURSE: enterPlayHole')
        if self.currentHole and not self.currentHole.playStarted:
            self.currentHole.startPlay()

    def exitPlayHole(self):
        self.notify.debug('GOLF COURSE: exitPlayHole')

    def enterWaitLeaveHole(self):
        self.notify.debugStateCall(self)
        self.notify.debug('calling requestDelete on hole %d' % self.currentHole.doId)
        self.currentHole.requestDelete()
        self.currentHole = None
        if self.numHolesPlayed >= self.numHoles:
            pass
        else:
            self.startNextHole()
        return

    def exitWaitLeaveHole(self):
        pass

    def enterWaitReward(self):
        self.updateHistoryForCourseComplete()
        self.awardTrophies()
        self.awardCups()
        self.awardHoleBest()
        self.awardCourseBest()
        self.recordHoleInOne()
        self.recordCourseUnderPar()
        trophiesList = []
        for index in range(len(self.avIdList)):
            avId = self.avIdList[index]
            if avId in self.newTrophies:
                oneTrophyList = self.newTrophies[avId]
                trophiesList.append(oneTrophyList)
            else:
                trophiesList.append([])

        while len(trophiesList) < GolfGlobals.MAX_PLAYERS_PER_HOLE:
            trophiesList.append([])

        holeBestList = []
        for index in range(len(self.avIdList)):
            avId = self.avIdList[index]
            if avId in self.newHoleBest:
                oneTrophyList = self.newHoleBest[avId]
                holeBestList.append(oneTrophyList)
            else:
                holeBestList.append([])

        while len(holeBestList) < GolfGlobals.MAX_PLAYERS_PER_HOLE:
            holeBestList.append([])

        courseBestList = []
        for index in range(len(self.avIdList)):
            avId = self.avIdList[index]
            if avId in self.newCourseBest:
                oneTrophyList = self.newCourseBest[avId]
                courseBestList.append(oneTrophyList)
            else:
                courseBestList.append([])

        while len(courseBestList) < GolfGlobals.MAX_PLAYERS_PER_HOLE:
            courseBestList.append([])

        cupList = []
        for index in range(len(self.avIdList)):
            avId = self.avIdList[index]
            if avId in self.newCups:
                oneCupList = self.newCups[avId]
                cupList.append(oneCupList)
                self.cupListLen = self.cupListLen + 1
            else:
                cupList.append([])

        while len(cupList) < GolfGlobals.MAX_PLAYERS_PER_HOLE:
            cupList.append([])

        REWARD_TIMEOUT = (self.trophyListLen + self.holeBestListLen + self.courseBestListLen + self.cupListLen) * 5 + 19
        aimTimesList = [
         0] * 4
        aimIndex = 0
        stillPlaying = self.getStillPlayingAvIds()
        for avId in self.avIdList:
            if avId in stillPlaying:
                aimTime = 0
                if avId in self.aimTimes:
                    aimTime = self.aimTimes[avId]
                aimTimesList[aimIndex] = aimTime
            aimIndex += 1

        self.sendUpdate('setReward', [trophiesList, self.rankings, holeBestList, courseBestList, cupList, self.winnerByTieBreak, aimTimesList[0], aimTimesList[1], aimTimesList[2], aimTimesList[3]])

        def allAvatarsRewarded(self=self):
            self.notify.debug('GOLF COURSE: all avatars rewarded')
            self.rewardDone()

        def handleRewardTimeout(avIds, self=self):
            self.notify.debug('GOLF COURSE: timed out waiting for clients %s to finish reward' % avIds)
            self.rewardDone()

        stillPlaying = self.getStillPlayingAvIds()
        self.rewardBarrier = ToonBarrier('waitReward', self.uniqueName('waitReward'), stillPlaying, REWARD_TIMEOUT, allAvatarsRewarded, handleRewardTimeout)

    def exitWaitReward(self):
        pass

    def enterWaitLeaveCourse(self):
        self.notify.debugStateCall(self)
        self.setCourseAbort()

    def exitWaitLeaveCourse(self):
        pass

    def enterCleanup(self):
        self.notify.debug('GOLF COURSE: enterCleanup')
        self.requestDelete()

    def exitCleanup(self):
        self.notify.debug('GOLF COURSE: exitCleanup')

    def isCurHoleDone(self):
        retval = False
        if self.areAllBallsInHole():
            retval = True
        else:
            retval = True
            for state in list(self.avStateDict.values()):
                if not (state == BALLIN or state == EXITED):
                    retval = False
                    break

        return retval

    def areAllBallsInHole(self):
        self.notify.debug('areAllBallsInHole, self.avStateDict=%s' % self.avStateDict)
        allBallsInHole = True
        for state in list(self.avStateDict.values()):
            if state != BALLIN:
                allBallsInHole = False

        return allBallsInHole

    def isPlayingLastHole(self):
        retval = self.numHoles - self.numHolesPlayed == 1
        return retval

    def setBallIn(self, avId):
        self.notify.debug('setBallIn %d' % avId)
        if self.avStateDict[avId] == BALLIN:
            self.notify.debug('setBallIn already in BALLIN state, just returning')
            return
        self.avStateDict[avId] = BALLIN
        self.updateHistoryForBallIn(avId)
        if self.isCurHoleDone():
            if self.isPlayingLastHole():
                if self.state != 'WaitReward':
                    self.safeDemand('WaitReward')
            else:
                self.notify.debug('allBalls are in holes, calling holeOver')
                self.holeOver()

    def updateHistoryForBallIn(self, avId):
        if self.currentHole == None:
            return
        holeId = self.currentHole.holeId
        holeInfo = GolfGlobals.HoleInfo[holeId]
        par = holeInfo['par']
        holeIndex = self.numHolesPlayed
        if holeIndex >= self.numHoles:
            self.notify.warning('updateHistoryForBallIn invalid holeIndex %d' % holeIndex)
            holeIndex = self.numHoles - 1
        else:
            if holeIndex < 0:
                self.notify.warning('updateHistoryForBallIn invalid holeIndex %d' % holeIndex)
                holeIndex = 0
        strokes = self.scores[avId][holeIndex]
        self.notify.debug('self.scores = %s' % self.scores)
        diff = strokes - par
        if strokes == 1:
            self.incrementEndingHistory(avId, GolfGlobals.HoleInOneShots)
        if diff <= -2:
            self.incrementEndingHistory(avId, GolfGlobals.EagleOrBetterShots)
        if diff <= -1:
            self.incrementEndingHistory(avId, GolfGlobals.BirdieOrBetterShots)
        if diff <= 0:
            self.endingHistory[avId][GolfGlobals.ParOrBetterShots] += 1
        if strokes < self.endingHoleBest[avId][holeId] or self.endingHoleBest[avId][holeId] == 0:
            self.endingHoleBest[avId][holeId] = strokes
        return

    def incrementEndingHistory(self, avId, historyIndex):
        if avId in self.endingHistory and historyIndex in GolfGlobals.TrophyRequirements:
            maximumAmount = GolfGlobals.TrophyRequirements[historyIndex][-1]
            if self.endingHistory[avId][historyIndex] < maximumAmount:
                self.endingHistory[avId][historyIndex] += 1

    def getCourseId(self):
        return self.courseId

    def abortCurrentHole(self):
        holeId = self.currentHole.holeId
        holeDoId = self.currentHole.doId
        self.currentHole.finishHole()
        return (
         holeId, holeDoId)

    def calcHolesToUse(self):
        retval = []
        if simbase.air.config.GetBool('golf-course-randomized', 1):
            retval = self.calcHolesToUseRandomized(self.courseId)
            self.notify.debug('randomized courses!')
            for x in range(len(retval)):
                self.notify.debug('Hole is: %s' % retval[x])

        else:
            validHoles = self.calcUniqueHoles(self.courseId)
            if self.preferredHoleId in validHoles:
                retval.append(self.preferredHoleId)
            while len(retval) < self.numHoles:
                for holeId in GolfGlobals.CourseInfo[self.courseId]['holeIds']:
                    if type(holeId) == type(0):
                        retval.append(holeId)
                    elif type(holeId) == type(()):
                        retval.append(holeId[0])
                    else:
                        self.notify.warning('cant handle %s' % self.holeId)
                    if len(retval) >= self.numHoles:
                        break

        return retval

    def incrementScore(self, avId):
        self.notify.debug('incrementScore self.scores=%s avId=%s' % (self.scores, avId))
        self.scores[avId][self.numHolesPlayed] += 1
        self.notify.debug('after increment self.score=%s' % self.scores)
        self.sendScores()

    def sendScores(self):
        self.notify.debug('sendScores self.scores = %s' % self.scores)
        scorelist = []
        for avId in self.avIdList:
            for score in self.scores[avId]:
                scorelist.append(score)

        self.sendUpdate('setScores', [scorelist])
        self.notify.debug('sendScores end self.scores = %s' % self.scores)

    def getCurHoleIndex(self):
        return self.curHoleIndex

    def b_setCurHoleIndex(self, holeIndex):
        self.setCurHoleIndex(holeIndex)
        self.d_setCurHoleIndex(holeIndex)

    def d_setCurHoleIndex(self, holeIndex):
        self.sendUpdate('setCurHoleIndex', [holeIndex])

    def setCurHoleIndex(self, holeIndex):
        self.curHoleIndex = holeIndex

    def setDoneReward(self):
        avId = self.air.getAvatarIdFromSender()
        self.notify.debug('got rewardDone from %d' % avId)
        if hasattr(self, 'rewardBarrier'):
            self.rewardBarrier.clear(avId)
        self.sendUpdate('setCourseAbort', [avId])

    def rewardDone(self):
        self.notify.debug('rewardDone')
        self.holeOver()
        self.safeDemand('WaitLeaveCourse')

    def updateHistoryForCourseComplete(self):
        self.calcRankings()
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            self.incrementEndingHistory(avId, GolfGlobals.CoursesCompleted)
            coursePar = self.calcCoursePar()
            totalScore = self.getTotalScore(avId)
            if totalScore < coursePar:
                self.incrementEndingHistory(avId, GolfGlobals.CoursesUnderPar)
            if len(stillPlaying) > 1:
                self.incrementEndingHistory(avId, GolfGlobals.MultiPlayerCoursesCompleted)
                if self.rankingsById[avId] == 1:
                    if self.courseId == 0:
                        self.incrementEndingHistory(avId, GolfGlobals.CourseZeroWins)
                    elif self.courseId == 1:
                        self.incrementEndingHistory(avId, GolfGlobals.CourseOneWins)
                    elif self.courseId == 2:
                        self.incrementEndingHistory(avId, GolfGlobals.CourseTwoWins)
                    else:
                        self.notify.warning('unhandled case, self.courseId=%s' % self.courseId)
            if totalScore < self.endingCourseBest[avId][self.courseId] or self.endingCourseBest[avId][self.courseId] == 0:
                self.endingCourseBest[avId][self.courseId] = totalScore

    def calcRankings(self):
        stillPlaying = self.getStillPlayingAvIds()
        self.rankings = []
        totalScores = []
        for avId in self.avIdList:
            aimTime = 0
            if avId in self.aimTimes:
                aimTime = self.aimTimes[avId]
            if avId in stillPlaying:
                totalScores.append((avId, self.getTotalScore(avId), aimTime))
            else:
                totalScores.append((avId, 255, aimTime))

        def scoreCompareNoTime(tupleA, tupleB):
            if tupleA[1] > tupleB[1]:
                return 1
            elif tupleA[1] == tupleB[1]:
                return 0
            else:
                return -1

        def scoreCompareWithTime(tupleA, tupleB):
            if tupleA[1] > tupleB[1]:
                return 1
            elif tupleA[1] == tupleB[1]:
                if tupleA[2] > tupleB[2]:
                    return 1
                elif tupleA[2] == tupleB[2]:
                    return 0
                else:
                    return -1
            else:
                return -1

        if GolfGlobals.TIME_TIE_BREAKER:
            totalScores.sort(key=functools.cmp_to_key(scoreCompareWithTime))
        else:
            totalScores.sort(key=functools.cmp_to_key(scoreCompareNoTime))
        curRank = 0
        oldScore = 0
        oldTime = 0
        self.rankingsById = {}
        for scoreTuple in totalScores:
            time = scoreTuple[2]
            score = scoreTuple[1]
            avId = scoreTuple[0]
            if score > oldScore or GolfGlobals.TIME_TIE_BREAKER and score == oldScore and time > oldTime:
                curRank += 1
                oldScore = score
                oldTime = time
            self.rankingsById[avId] = curRank

        tiedForFirst = []
        tempRank = 0
        oldScore = 0
        oldTime = 0
        for scoreTuple in totalScores:
            time = scoreTuple[2]
            score = scoreTuple[1]
            avId = scoreTuple[0]
            if score > oldScore:
                tempRank += 1
                oldScore = score
                oldTime = time
            if tempRank == 1:
                tiedForFirst.append(avId)

        for avId in self.avIdList:
            if avId in stillPlaying:
                self.rankings.append(self.rankingsById[avId])
            else:
                self.rankings.append(-1)

        if len(tiedForFirst) >= 2 and not GolfGlobals.TIME_TIE_BREAKER:
            winnerAvId = random.choice(tiedForFirst)
            winnerIndex = self.avIdList.index(winnerAvId)
            self.winnerByTieBreak = winnerAvId
            for index in range(len(self.rankings)):
                if self.rankings[index] > 0 and index != winnerIndex:
                    self.rankings[index] += 1

            for avId in self.rankingsById:
                if self.rankingsById[avId] > 0 and avId != winnerAvId:
                    self.rankingsById[avId] += 1

        elif len(tiedForFirst) >= 2:
            winnerAvId = totalScores[0][0]
            self.winnerByTieBreak = winnerAvId

    def awardTrophies(self):
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            av = simbase.air.doId2do.get(avId)
            if av:
                oldHistory = self.startingHistory[avId]
                endingHistory = self.endingHistory[avId]
                oldTrophies = GolfGlobals.calcTrophyListFromHistory(oldHistory)
                endingTrophies = GolfGlobals.calcTrophyListFromHistory(endingHistory)
                av.b_setGolfHistory(endingHistory)
                newTrophies = []
                for index in range(len(oldTrophies)):
                    if not oldTrophies[index] and endingTrophies[index]:
                        self.notify.debug('New Trophy %d' % index)
                        self.air.writeServerEvent('golf_trophy', avId, '%s' % index)
                        newTrophies.append(True)
                        self.trophyListLen = self.trophyListLen + 1
                    else:
                        newTrophies.append(False)

                self.newTrophies[avId] = newTrophies

    def awardCups(self):
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            av = simbase.air.doId2do.get(avId)
            if av:
                oldHistory = self.startingHistory[avId]
                endingHistory = self.endingHistory[avId]
                oldCups = GolfGlobals.calcCupListFromHistory(oldHistory)
                endingCups = GolfGlobals.calcCupListFromHistory(endingHistory)
                newCups = []
                for index in range(len(oldCups)):
                    if not oldCups[index] and endingCups[index]:
                        self.notify.debug('New Trophy %d' % index)
                        newCups.append(True)
                        self.air.writeServerEvent('golf_cup', avId, '%s' % index)
                        newMaxHp = av.getMaxHp() + 1
                        av.b_setMaxHp(newMaxHp)
                        av.toonUp(newMaxHp)
                    else:
                        newCups.append(False)

                self.newCups[avId] = newCups

    def awardHoleBest(self):
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            av = simbase.air.doId2do.get(avId)
            if av:
                oldHoleBest = self.startingHoleBest[avId]
                endingHoleBest = self.endingHoleBest[avId]
                av.b_setGolfHoleBest(endingHoleBest)
                newHoleBest = []
                longestHoleBestList = 0
                for index in range(len(oldHoleBest)):
                    if endingHoleBest[index] < oldHoleBest[index]:
                        self.notify.debug('New HoleBest %d' % index)
                        newHoleBest.append(True)
                        longestHoleBestList = longestHoleBestList + 1
                    else:
                        newHoleBest.append(False)

                if longestHoleBestList > self.holeBestListLen:
                    self.holeBestListLen = longestHoleBestList
                self.newHoleBest[avId] = newHoleBest

    def awardCourseBest(self):
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            av = simbase.air.doId2do.get(avId)
            if av:
                oldCourseBest = self.startingCourseBest[avId]
                endingCourseBest = self.endingCourseBest[avId]
                av.b_setGolfCourseBest(endingCourseBest)
                newCourseBest = []
                longestCourseBestList = 0
                for index in range(len(oldCourseBest)):
                    if endingCourseBest[index] < oldCourseBest[index]:
                        self.notify.debug('New CourseBest %d' % index)
                        newCourseBest.append(True)
                        longestCourseBestList = longestCourseBestList + 1
                    else:
                        newCourseBest.append(False)

                if longestCourseBestList > self.courseBestListLen:
                    self.courseBestListLen = longestCourseBestList
                self.newCourseBest[avId] = newCourseBest

    def haveAllGolfersExited(self):
        retval = True
        for avId in self.avStateDict:
            if not self.avStateDict[avId] == EXITED:
                retval = False
                break

        return retval

    def getCurHoleDoId(self):
        retval = 0
        if self.currentHole:
            retval = self.currentHole.doId
        return retval

    def d_setCurHoleDoId(self, curHoleDoId):
        self.sendUpdate('setCurHoleDoId', [curHoleDoId])

    def calcCoursePar(self):
        retval = 0
        for holeId in self.holeIds:
            holeInfo = GolfGlobals.HoleInfo[holeId]
            retval += holeInfo['par']

        return retval

    def getTotalScore(self, avId):
        retval = 0
        if avId in self.scores:
            for holeScore in self.scores[avId]:
                retval += holeScore

        return retval

    def getCurHoleScore(self, avId):
        retval = 0
        if avId in self.scores and self.numHolesPlayed < len(self.scores[avId]):
            retval = self.scores[avId][self.numHolesPlayed]
        return retval

    def toggleDrivePermission(self, avId):
        if avId in self.drivingToons:
            self.drivingToons.remove(avId)
            self.sendUpdate('changeDrivePermission', [avId, 0])
            retval = False
        else:
            self.drivingToons.append(avId)
            self.sendUpdate('changeDrivePermission', [avId, 1])
            retval = True
        return retval

    def safeDemand(self, newState):
        doingDemand = False
        if self.state == 'Cleanup':
            pass
        else:
            if self.state in self.defaultTransitions:
                if newState in self.defaultTransitions[self.state]:
                    self.demand(newState)
                    doingDemand = True
            else:
                if self.state == None:
                    self.demand(newState)
                    doingDemand = True
            if not doingDemand:
                self.notify.warning('doId=%d ignoring demand from %s to %s' % (self.doId, self.state, newState))
        return doingDemand

    def setAvatarExited(self):
        avId = self.air.getAvatarIdFromSender()
        self.handleExitedAvatar(avId)

    def createChoicesList(self, courseId, possibleHoles):
        retval = []
        holeIds = GolfGlobals.CourseInfo[courseId]['holeIds']
        for holeOrTuple in holeIds:
            if type(holeOrTuple) == type(()):
                holeId = holeOrTuple[0]
                weight = holeOrTuple[1]
            else:
                if type(holeOrTuple) == type(0):
                    holeId = holeOrTuple
                    weight = 1
                else:
                    self.notify.warning('cant handle %s' % holeOrTuple)
                    continue
            if holeId in possibleHoles:
                retval += [holeId] * weight

        return retval

    def calcUniqueHoles(self, courseId):
        uniqueHoles = set()
        for holeOrTuple in GolfGlobals.CourseInfo[courseId]['holeIds']:
            if type(holeOrTuple) == type(()):
                uniqueHoles.add(holeOrTuple[0])
            elif type(holeOrTuple) == type(0):
                uniqueHoles.add(holeOrTuple)
            else:
                self.notify.warning('cant handle %s' % holeOrTuple)

        return uniqueHoles

    def calcHolesToUseRandomized(self, courseId):
        retval = []
        numHoles = GolfGlobals.CourseInfo[courseId]['numHoles']
        uniqueHoles = self.calcUniqueHoles(courseId)
        curHolesChosen = set()
        while len(retval) < numHoles:
            if uniqueHoles == curHolesChosen:
                curHolesChosen = set()
            possibleHoles = uniqueHoles - curHolesChosen
            choicesList = self.createChoicesList(courseId, possibleHoles)
            if not self.preferredHoleId == None and self.preferredHoleId in choicesList and self.preferredHoleId not in curHolesChosen:
                holeChosen = self.preferredHoleId
            else:
                holeChosen = random.choice(choicesList)
            retval.append(holeChosen)
            curHolesChosen.add(holeChosen)

        return retval

    def recordHoleInOne(self):
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            scoreList = self.scores[avId]
            for holeIndex in range(len(scoreList)):
                strokes = scoreList[holeIndex]
                if strokes == 1:
                    holeId = self.holeIds[holeIndex]
                    self.air.writeServerEvent('golf_ace', avId, '%d|%d|%s' % (self.courseId, holeId, stillPlaying))

    def recordCourseUnderPar(self):
        coursePar = self.calcCoursePar()
        stillPlaying = self.getStillPlayingAvIds()
        for avId in stillPlaying:
            totalScore = self.getTotalScore(avId)
            netScore = totalScore - coursePar
            if netScore < 0:
                self.air.writeServerEvent('golf_underPar', avId, '%d|%d|%s' % (self.courseId, netScore, stillPlaying))

    def addAimTime(self, avId, aimTime):
        if avId in self.aimTimes:
            self.aimTimes[avId] += aimTime