from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectAI import DistributedObjectAI
from direct.fsm.FSM import FSM
from toontown.estate.DistributedEstateAI import DistributedEstateAI
from toontown.estate.DistributedHouseAI import DistributedHouseAI
import HouseGlobals
import functools

class LoadHouseFSM(FSM):
    notify = DirectNotifyGlobal.directNotify.newCategory('LoadHouseFSM')

    def __init__(self, mgr, estate, houseIndex, toon, callback):
        FSM.__init__(self, 'LoadHouseFSM')
        self.mgr = mgr
        self.estate = estate
        self.houseIndex = houseIndex
        self.toon = toon
        self.callback = callback

        self.done = False

    def start(self):
        # We have a few different cases here:
        if self.toon is None:
            # Case #1: There isn't a Toon in that estate slot. Make a blank house.

            # Because this state completes so fast, we'll use taskMgr to delay
            # it until the next iteration. This solves re-entrancy problems.
            taskMgr.doMethodLater(0.0, self.demand,
                                  'makeBlankHouse-%s' % id(self),
                                  extraArgs=['MakeBlankHouse'])
            return

        self.houseId = self.toon.get('setHouseId', [0])[0]
        if self.houseId  == 0:
            # Case #2: There is a Toon, but no setHouseId. Gotta make one.
            self.demand('CreateHouse')
        else:
            # Case #3: Toon with a setHouseId. Load it.
            self.demand('LoadHouse')

    def enterMakeBlankHouse(self):
        self.house = DistributedHouseAI(self.mgr.air)
        self.house.setHousePos(self.houseIndex)
        self.house.setColor(self.houseIndex)
        self.house.generateWithRequired(self.estate.zoneId)
        self.estate.houses[self.houseIndex] = self.house
        self.demand('Off')

    def enterCreateHouse(self):
        self.mgr.air.dbInterface.createObject(
            self.mgr.air.dbId,
            self.mgr.air.dclassesByName['DistributedHouseAI'],
            {
                'setName' : [self.toon['setName'][0]],
                'setAvatarId' : [self.toon['ID']],
            },
            self.__handleCreate)

    def __handleCreate(self, doId):
        if self.state != 'CreateHouse':
            return

        # Update the avatar's houseId:
        av = self.mgr.air.doId2do.get(self.toon['ID'])
        if av:
            av.b_setHouseId(doId)
        else:
            self.mgr.air.dbInterface.updateObject(
                self.mgr.air.dbId,
                self.toon['ID'],
                self.mgr.air.dclassesByName['DistributedToonAI'],
                {'setHouseId': [doId]})

        self.houseId = doId
        self.demand('LoadHouse')

    def enterLoadHouse(self):
        # Activate the house:
        self.mgr.air.sendActivate(self.houseId, self.mgr.air.districtId, self.estate.zoneId,
                                  self.mgr.air.dclassesByName['DistributedHouseAI'],
                                  {'setHousePos': [self.houseIndex],
                                   'setColor': [self.houseIndex],
                                   'setName': [self.toon['setName'][0]],
                                   'setAvatarId': [self.toon['ID']]})

        # Now we wait for the house to show up... We do this by hanging a messenger
        # hook which the DistributedHouseAI throws once it spawns.
        self.acceptOnce('generate-%d' % self.houseId, self.__gotHouse)

    def __gotHouse(self, house):
        self.house = house

        self.estate.houses[self.houseIndex] = self.house

        self.demand('Off')

    def exitLoadHouse(self):
        self.ignore('generate-%d' % self.houseId)

    def enterOff(self):
        self.done = True
        self.callback(self.house)

class LoadPetFSM(FSM):
    def __init__(self, mgr, estate, toon, callback):
        FSM.__init__(self, 'LoadPetFSM')
        self.mgr = mgr
        self.estate = estate
        self.toon = toon
        self.callback = callback

        self.done = False

    def start(self):
        self.petId = self.toon['setPetId'][0]
        if not self.petId in self.mgr.air.doId2do:
            self.mgr.air.sendActivate(self.petId, self.mgr.air.districtId, self.estate.zoneId)
            self.acceptOnce('generate-%d' % self.petId, self.__generated)
        else:
            self.__generated(self.mgr.air.doId2do[self.petId])

    def __generated(self, pet):
        self.pet = pet
        self.estate.pets.append(pet)
        self.demand('Off')

    def enterOff(self):
        self.done = True
        self.callback(self.pet)

class LoadEstateFSM(FSM):
    def __init__(self, mgr, callback):
        FSM.__init__(self, 'LoadEstateFSM')
        self.mgr = mgr
        self.callback = callback

        self.estate = None

    def start(self, accountId, zoneId):
        self.accountId = accountId
        self.zoneId = zoneId
        self.demand('QueryAccount')

    def enterQueryAccount(self):
        self.mgr.air.dbInterface.queryObject(self.mgr.air.dbId, self.accountId,
                                             self.__gotAccount)

    def __gotAccount(self, dclass, fields):
        if self.state != 'QueryAccount':
            return # We must have aborted or something...

        if dclass != self.mgr.air.dclassesByName['AccountAI']:
            self.mgr.notify.warning('Account %d has non-account dclass %d!' %
                                    (self.accountId, dclass))
            self.demand('Failure')
            return

        self.accountFields = fields

        self.estateId = fields.get('ESTATE_ID', 0)
        self.demand('QueryToons')

    def enterQueryToons(self):
        self.toonIds = self.accountFields.get('ACCOUNT_AV_SET', [0]*6)
        self.toons = {}

        for index, toonId in enumerate(self.toonIds):
            if toonId == 0:
                self.toons[index] = None
                continue
            self.mgr.air.dbInterface.queryObject(
                self.mgr.air.dbId, toonId,
                functools.partial(self.__gotToon, index=index))

    def __gotToon(self, dclass, fields, index):
        if self.state != 'QueryToons':
            return # We must have aborted or something...

        if dclass != self.mgr.air.dclassesByName['DistributedToonAI']:
            self.mgr.notify.warning('Account %d has avatar %d with non-Toon dclass %d!' %
                                    (self.accountId, self.toonIds[index], dclass))
            self.demand('Failure')
            return

        fields['ID'] = self.toonIds[index]
        self.toons[index] = fields
        if len(self.toons) == 6:
            self.__gotAllToons()

    def __gotAllToons(self):
        # Okay, we have all of our Toons, now we can proceed with estate!
        if self.estateId:
            # We already have an estate, load it!
            self.demand('LoadEstate')
        else:
            # We don't have one yet, make one!
            self.demand('CreateEstate')

    def enterCreateEstate(self):
        # We have to ask the DB server to construct a blank estate object...
        self.mgr.air.dbInterface.createObject(
            self.mgr.air.dbId,
            self.mgr.air.dclassesByName['DistributedEstateAI'],
            {},
            self.__handleEstateCreate)

    def __handleEstateCreate(self, estateId):
        if self.state != 'CreateEstate':
            return # We must have aborted or something...
        self.estateId = estateId

        # Update our account so we can store this new estate object.
        self.mgr.air.dbInterface.updateObject(
            self.mgr.air.dbId,
            self.accountId,
            self.mgr.air.dclassesByName['AccountAI'],
            { 'ESTATE_ID': estateId }
        )

        self.demand('LoadEstate')

    def enterLoadEstate(self):
        # Activate the estate:
        self.mgr.air.sendActivate(self.estateId, self.mgr.air.districtId, self.zoneId)

        # Now we wait for the estate to show up... We do this by hanging a messenger
        # hook which the DistributedEstateAI throws once it spawns.
        self.acceptOnce('generate-%d' % self.estateId, self.__gotEstate)

    def __gotEstate(self, estate):
        self.estate = estate
        estate.pets = []

        self.estate.toons = self.toonIds
        self.estate.updateToons()

        # Gotcha! Now we need to load houses:
        self.demand('LoadHouses')

    def exitLoadEstate(self):
        self.ignore('generate-%d' % self.estateId)

    def enterLoadHouses(self):
        self.houseFSMs = []

        for houseIndex in xrange(6):
            fsm = LoadHouseFSM(self.mgr, self.estate, houseIndex,
                               self.toons[houseIndex], self.__houseDone)
            self.houseFSMs.append(fsm)
            fsm.start()

    def __houseDone(self, house):
        if self.state != 'LoadHouses':
            # We aren't loading houses, so we probably got cancelled. Therefore,
            # the only sensible thing to do is simply destroy the house.
            house.requestDelete()
            return

        # A houseFSM just finished! Let's see if all of them are done:
        if all(houseFSM.done for houseFSM in self.houseFSMs):
            self.demand('LoadPets')

    def enterLoadPets(self):
        self.petFSMs = []
        for houseIndex in range(6):
            toon = self.toons[houseIndex]
            if toon and toon['setPetId'][0] != 0:
                fsm = LoadPetFSM(self.mgr, self.estate, toon, self.__petDone)
                self.petFSMs.append(fsm)
                fsm.start()

        if not self.petFSMs:
            taskMgr.doMethodLater(0, lambda: self.demand('Finished'), 'nopets', extraArgs=[])

    def __petDone(self, pet):
        if self.state != 'LoadPets':
            pet.requestDelete()
            return

        # A petFSM just finished! Let's see if all of them are done:
        if all(petFSM.done for petFSM in self.petFSMs):
            self.demand('Finished')

    def enterFinished(self):
        self.callback(True)

    def enterFailure(self):
        self.cancel()

        self.callback(False)

    def cancel(self):
        if self.estate:
            self.estate.destroy()
            self.estate = None

        self.demand('Off')

class EstateManagerAI(DistributedObjectAI):
    notify = DirectNotifyGlobal.directNotify.newCategory("EstateManagerAI")

    def __init__(self, air):
        DistributedObjectAI.__init__(self, air)

        self.estate2toons = {}
        self.toon2estate = {}
        self.estate2timeout = {}

    def getEstateZone(self, avId):
        senderId = self.air.getAvatarIdFromSender()
        accId = self.air.getAccountIdFromSender()

        toon = self.air.doId2do.get(senderId)
        if not toon:
            self.air.writeServerEvent('suspicious', senderId, 'Sent getEstateZone() but not on district!')
            return

        # If there's an avId included, then the Toon is interested in visiting a
        # friend. We do NOT load the estate, we simply see if it's already up...
        if avId and avId != senderId:
            av = self.air.doId2do.get(avId)
            if av and av.dclass == self.air.dclassesByName['DistributedToonAI']:
                estate = self._lookupEstate(av)
                if estate:
                    # Yep, there it is!
                    avId = estate.owner.doId
                    zoneId = estate.zoneId
                    self._mapToEstate(toon, estate)
                    self._unloadEstate(toon) # In case they're doing estate->estate TP.
                    self.sendUpdateToAvatarId(senderId, 'setEstateZone', [avId, zoneId])

            # Bummer, couldn't find avId at an estate...
            self.sendUpdateToAvatarId(senderId, 'setEstateZone', [0, 0])
            return

        # The Toon definitely wants to go to his own estate...

        estate = getattr(toon, 'estate', None)
        if estate:
            # They already have an estate loaded, so let's just return it:
            self._mapToEstate(toon, toon.estate)
            self.sendUpdateToAvatarId(senderId, 'setEstateZone', [senderId, estate.zoneId])

            # If a timeout is active, cancel it:
            if estate in self.estate2timeout:
                self.estate2timeout[estate].remove()
                del self.estate2timeout[estate]

            return

        if getattr(toon, 'loadEstateFSM', None):
            # We already have a loading operation underway; ignore this second
            # request since the first operation will setEstateZone() when it
            # finishes anyway.
            return

        zoneId = self.air.allocateZone()

        def estateLoaded(success):
            if success:
                toon.estate = toon.loadEstateFSM.estate
                toon.estate.owner = toon
                self._mapToEstate(toon, toon.estate)
                self.sendUpdateToAvatarId(senderId, 'setEstateZone', [senderId, zoneId])
            else:
                # Estate loading failed??!
                self.sendUpdateToAvatarId(senderId, 'setEstateZone', [0, 0])

                # And I guess we won't need our zoneId anymore...
                self.air.deallocateZone(zoneId)

            toon.loadEstateFSM = None

        self.acceptOnce(self.air.getAvatarExitEvent(toon.doId), self._unloadEstate, extraArgs=[toon])

        toon.loadEstateFSM = LoadEstateFSM(self, estateLoaded)
        toon.loadEstateFSM.start(accId, zoneId)

    def exitEstate(self):
        senderId = self.air.getAvatarIdFromSender()
        toon = self.air.doId2do.get(senderId)

        if not toon:
            self.air.writeServerEvent('suspicious', senderId, 'Sent exitEstate() but not on district!')
            return

        self._unmapFromEstate(toon)
        self._unloadEstate(toon)

    def _unloadEstate(self, toon):
        if getattr(toon, 'estate', None):
            estate = toon.estate
            if estate not in self.estate2timeout:
                self.estate2timeout[estate] = \
                    taskMgr.doMethodLater(HouseGlobals.BOOT_GRACE_PERIOD,
                                          self._cleanupEstate,
                                          estate.uniqueName('emai-cleanup-task'),
                                          extraArgs=[estate])
            self._sendToonsToPlayground(toon.estate, 0) # This is a warning only...

        if getattr(toon, 'loadEstateFSM', None):
            self.air.deallocateZone(toon.loadEstateFSM.zoneId)
            toon.loadEstateFSM.cancel()
            toon.loadEstateFSM = None

        self.ignore(self.air.getAvatarExitEvent(toon.doId))

    def _cleanupEstate(self, estate):
        # Boot all Toons from estate:
        self._sendToonsToPlayground(estate, 1)

        # Clean up toon<->estate mappings...
        for toon in self.estate2toons.get(estate, []):
            try:
                del self.toon2estate[toon]
            except KeyError:
                pass
        try:
            del self.estate2toons[estate]
        except KeyError:
            pass

        # Clean up timeout, if it exists:
        if estate in self.estate2timeout:
            del self.estate2timeout[estate]

        # Destroy estate and unmap from owner:
        estate.destroy()
        estate.owner.estate = None

        # Destroy pets:
        for pet in estate.pets:
            pet.requestDelete()

        estate.pets = []

        # Free estate's zone:
        self.air.deallocateZone(estate.zoneId)

    def _sendToonsToPlayground(self, estate, reason):
        for toon in self.estate2toons.get(estate, []):
            self.sendUpdateToAvatarId(toon.doId, 'sendAvToPlayground',
                                      [toon.doId, reason])

    def _mapToEstate(self, toon, estate):
        self._unmapFromEstate(toon)

        self.estate2toons.setdefault(estate, []).append(toon)
        self.toon2estate[toon] = estate

    def _unmapFromEstate(self, toon):
        estate = self.toon2estate.get(toon)
        if not estate: return
        del self.toon2estate[toon]

        try:
            self.estate2toons[estate].remove(toon)
        except (KeyError, ValueError):
            pass

    def _lookupEstate(self, toon):
        return self.toon2estate.get(toon)

    def getOwnerFromZone(self, avId):
        return False