import os

from core.helper import enum, getPositionOnGroundForEntity, select
from core.terraform.helper import placeContentInArea

from siege import game, Entity
from siege.log import Log
from siege.io import DataStream, File
from siege.network import NetworkManager
from siege.util import minutes, PixelRect, Random, Rect, RegionRect, seconds, Sizes, TileRect, Timer, WeightedRandomGenerator
from siege.world import World
from siege.world.realm import BiomeData, Layer
from siege.worldgen import Biome

RegionState = enum("Hostile", "Neutral", "Purified")
RegionSpecials = enum('Spawner', 'Swarm', 'Miniboss', 'Nexus')


class ConflictSystem(object):
    NAME = "conflict"

    def getName(self):
        return self.NAME

    def __init__(self):
        # {realm.uid: ConflictRealm}
        self.realms = {}

    def update(self, frameTime):
        if NetworkManager.isHost:
            for realm in self.realms.values():
                realm.update(frameTime)

    def handleCreateRealm(self, realm):
        conflictRealm = ConflictRealm(realm)
        conflictRealm.load(World.get())
        self.realms[realm.uid] = conflictRealm

    def handleDestroyRealm(self, realm):
        del self.realms[realm.uid]

    def handleWorldUnloaded(self, world):
        self.realms.clear()

    def save(self, world, asynchronous):
        for realm in self.realms.values():
            realm.save(world, asynchronous)

    @staticmethod
    def register():
        if NetworkManager.isHost():
            system = ConflictSystem()
            game.registerSystem(ConflictSystem.NAME, system)
            game.events['create_realm'].listen(system.handleCreateRealm)
            game.events['destroy_realm'].listen(system.handleDestroyRealm)
            game.events["world_unloaded"].listen(system.handleWorldUnloaded)
            game.events["world_saved"].listen(system.save)

    @staticmethod
    def unregister():
        if game.hasSystem(ConflictSystem.NAME):
            system = game.conflict
            game.events['create_realm'].remove(system.handleCreateRealm)
            game.events['destroy_realm'].remove(system.handleDestroyRealm)
            game.events["world_unloaded"].remove(system.handleWorldUnloaded)
            game.events["world_saved"].remove(system.save)


class ConflictRealm(object):
    def __init__(self, realm):
        self.realm = realm
        self.regions = []
        for uid in xrange(realm.size.regionCount):
            self.regions.append(ConflictRegion(realm, uid))
        self.updateTimer = Timer(seconds(1))
        self.realm.loader.onRegionLoad.listen(self.handleRegionLoad)
        self.realm.loader.onRegionUnload.listen(self.handleRegionUnload)

    def update(self, frameTime):
        for region in self.regions:
            region.update(self, frameTime)
        self.updateTimer.update(frameTime)
        if self.updateTimer.expired():
            self.updateTimer.reset(seconds(1))
            # Get all players in realm
            for region in self.regions:
                region.players = []
                region.nearbyPlayers = []
            for player in self.realm.players:
                self.updateRegionsPlayerIsNearby(player)
            for region in self.regions:
                region.updateMonsters(frameTime)

    def getRegionsInColumn(self, region):
        regionHeight = self.realm.size.regionHeight
        start = region * regionHeight
        return xrange(start, start + regionHeight)

    def handleRegionLoad(self, region, segments, timeElapsed):
        # Provided region id is a vertical slice of regions
        for regionId in self.getRegionsInColumn(region):
            region = self.regions[regionId]
            # Try to replace spawners as entity ids in region with entity
            for index, spawner in enumerate(region.spawners):
                if not isinstance(spawner, Entity):
                    entity = game.entity.get(spawner)
                    if entity is not None:
                        region.spawners[index] = entity
                        entity.onDestroyed.listen(region.handleSpawnerDeath)

    def handleRegionUnload(self, region, segments):
        # Provided region id is a vertical slice of regions
        for regionId in self.getRegionsInColumn(region):
            region = self.regions[regionId]
            # Replace entity with just the entity id since it is being unloaded
            for index, spawner in enumerate(region.spawners):
                if isinstance(spawner, Entity):
                    region.spawners[index] = spawner.id

    def createSpawner(self, region):
        area = region.getArea()
        biome = region.getBiomeWithWeight(area)
        if biome is None:
            return None
        biomeArea = Rect(biome.area)
        intersection = Rect()
        if not biomeArea.intersects(area, intersection, self.realm.size.width):
            return None
        spawners = game.spawner.getSpawnersForBiome(biome.biome.name)
        if not spawners:
            return None
        spawner = Random.get(spawners)
        entity = placeContentInArea(self.realm, spawner, intersection, validation=placementValidation)
        return entity

    def updateRegionsPlayerIsNearby(self, player):
        position = player.entity.getPosition()
        regionUid = self.getRegionUid(position)
        self.regions[regionUid].players.append(player)
        self.regions[regionUid].nearbyPlayers.append(player)
        for neighborRegion in self.getNeighboringRegions(regionUid):
            neighborRegion.nearbyPlayers.append(player)

    def isNeighboringRegionsHostile(self, conflictRegion):
        hostility = sum([region.hostility for region in self.getNeighboringRegions(conflictRegion.uid)])
        return hostility >= 30

    def getRegionUid(self, position):
        regionHeight = self.realm.size.regionHeight
        return (int(position.x / Sizes.REGION) * regionHeight) + int(position.y / Sizes.REGION)

    def getNeighboringRegions(self, regionUid):
        regionCount = self.realm.size.regionCount
        regionHeight = self.realm.size.regionHeight
        neighbors = []
        # Right Neighbor
        neighbor = regionUid + regionHeight
        if neighbor >= regionCount:
            neighbor -= regionCount
        neighbors.append(self.regions[neighbor])
        # Left Neighbor
        neighbor = regionUid - regionHeight
        if neighbor < 0:
            neighbor += regionCount
        neighbors.append(self.regions[neighbor])
        # Below Neighbor
        if (regionUid + 1) % regionHeight != 0:
            neighbors.append(self.regions[regionUid + 1])
        # Above Neighbor
        if regionUid % regionHeight != 0:
            neighbors.append(self.regions[regionUid - 1])
        return neighbors

    def save(self, world, asynchronous):
        stream = DataStream()
        for region in self.regions:
            region.save(stream)
        if asynchronous:
            game.file.asyncWrite(self.getPath(), stream)
        else:
            File.save(self.getPath(), stream)

    def load(self, world):
        path = self.getPath()
        if os.path.exists(path):
            stream = DataStream()
            File.load(path, stream)
            for region in self.regions:
                region.load(stream)

    def getPath(self):
        return os.path.join(self.realm.path, "conflict")


class ConflictRegion(object):
    BASE_MONSTER_COUNT = 4
    HOSTILITY_INCREMENT = 10
    HOSTILITY_CAP = 50
    HOSTILITY_TIMER = minutes(30)
    NEUTRAL_TIMER = minutes(45)

    def __init__(self, realm, uid):
        self.realm = realm
        self.uid = uid
        self.state = RegionState.Hostile
        self.hostility = 0
        self.updateTimer = Timer()
        # Players that are in the region
        self.players = []
        # Players that are nearby region (includes self.players)
        self.nearbyPlayers = []
        self.timeActive = 0
        self.inactiveTimer = Timer()
        self.timeSinceLastSpawn = seconds(60)
        self.monsterReserve = 0
        self.monsters = []
        self.spawners = []

    @property
    def isHostile(self):
        return self.state == RegionState.Hostile

    @property
    def isNeutral(self):
        return self.state == RegionState.Neutral

    @property
    def isPurified(self):
        return self.state == RegionState.Purified

    def becomeHostile(self):
        self.state = RegionState.Hostile
        self.updateTimer.reset(self.HOSTILITY_TIMER)

    def becomeNeutral(self):
        self.state = RegionState.Neutral
        self.updateTimer.reset(self.NEUTRAL_TIMER)

    def getArea(self):
        regionHeight = self.realm.size.regionHeight
        return RegionRect(self.uid / regionHeight, self.uid % regionHeight, 1, 1).asRect()

    def update(self, conflictRealm, frameTime):
        if self.timeActive > 0:
            self.timeActive += frameTime
        self.timeSinceLastSpawn += frameTime
        self.updateTimer.update(frameTime)
        if not self.updateTimer.expired():
            return
        if self.isNeutral:
            if conflictRealm.isNeighboringRegionsHostile(self):
                self.becomeHostile()
            else:
                self.updateTimer.reset(self.NEUTRAL_TIMER)
        if self.isHostile:
            self.updateTimer.reset(self.HOSTILITY_TIMER)
            increment = min(self.HOSTILITY_CAP - self.hostility, self.HOSTILITY_INCREMENT)
            self.hostility += increment
            if increment < self.HOSTILITY_INCREMENT:
                # Did not have enough space to fully increment hostility
                self.monsterReserve += increment
                return
            possibleSpecials = [RegionSpecials.Swarm]
            if self.hostility >= 20:
                possibleSpecials.append(RegionSpecials.Spawner)
            ##TODO: Add in RegionSpecials.Miniboss and RegionSpecials.Nexus
            special = Random.get(possibleSpecials)
            if special == RegionSpecials.Swarm:
                self.monsterReserve += self.HOSTILITY_INCREMENT
            elif special == RegionSpecials.Spawner:
                for attempts in xrange(3):
                    spawner = conflictRealm.createSpawner(self)
                    if spawner:
                        self.monsterReserve += 5
                        self.addSpawner(spawner)
                        break

    def updateMonsters(self, frameTime):
        monsters = list(self.monsters)
        for monster in monsters:
            if not self.isPlayerNearby(monster):
                Log.info("Despawning monster: {}".format(monster))
                monster.destroy(unload=True)
                self.monsterReserve += 1
        if not self.nearbyPlayers and not self.monsters:
            self.active = False
            return
        elif self.monsterReserve == 0 or self.timeSinceLastSpawn < seconds(15):
            return
        monsterCount = self.getMaxMonsterCount()
        if monsterCount <= len(self.monsters):
            # Already have enough monsters spawned
            return
        elif self.timeActive < seconds(3):
            count = monsterCount - len(self.monsters)
            for _ in xrange(count):
                if not self.spawnMonster():
                    break
            if self.timeActive == 0:
                self.timeActive = 1
        else:
            self.spawnMonster()

    def addSpawner(self, spawner):
        spawner.onDestroyed.listen(self.handleSpawnerDeath)
        self.spawners.append(spawner)

    def isPlayerNearby(self, monster):
        for player in self.nearbyPlayers:
            if monster.getPosition().getDistance(player.entity.getPosition(), self.realm.size.width) <= 4000:
                return True
        # Did not find any nearby players. Player may been in a near region though.
        player, distance = self.realm.getClosestPlayer(monster.getPosition())
        return distance <= 4000

    def spawnMonster(self):
        nearbySpawners = self.getNearbySpawners()
        if nearbySpawners and Random.getFloat() <= 0.6:
            # Spawning a monster from a spawner
            spawnerEntity = Random.get(nearbySpawners)
            spawnerEntity.spawner.spawnMonster(self.getCombatLevel(), self.addSpawnedMonster)
            return True
        else:
            # Spawning a monster offscreen. Find a valid place and determine monster type
            for attempts in xrange(3):
                monster = self.spawnMonsterOffscreen()
                if monster:
                    self.addSpawnedMonster(monster)
                    return True
        return False

    def addSpawnedMonster(self, monster):
        monster.onDestroyed.listen(self.handleMonsterDeath)
        self.monsters.append(monster)
        self.timeSinceLastSpawn = 0
        self.monsterReserve -= 1

    def getMaxMonsterCount(self):
        multiplier = 1.0
        # Add 40% more monsters per additional player in the area
        multiplier += (len(self.nearbyPlayers) - 1) * 0.4
        # Add 50% more monsters if it is night
        if World.get().time.isNight():
            multiplier += multiplier * 0.5
        baseMonsterCount = self.BASE_MONSTER_COUNT
        baseMonsterCount += self.hostility / 13
        #TODO: Decide if swarm should affect monster count
        # if self.hasSwarm:
        #     baseMonsterCount += 2
        monsterCount = baseMonsterCount * multiplier
        return int(min(monsterCount, self.monsterReserve))

    def getNearbySpawners(self):
        spawners = []
        for spawner in self.spawners:
            if not isinstance(spawner, Entity) or not spawner.spawner.canSpawnMonster():
                continue
            for player in self.nearbyPlayers:
                distance = spawner.getPosition().getDistance(player.entity.getPosition(), self.realm.size.width)
                if distance < 300:
                    spawners.append(spawner)
        return spawners

    def spawnMonsterOffscreen(self):
        area = self.getSpawnArea()
        if area is None:
            return None
        biome = self.getBiomeWithWeight(area)
        if biome is None:
            return None
        # With biome determine monster type
        monsters = [monster for monster in game.monster.getMonstersForBiome(biome.biome.name)]
        #TODO: Weight monster type based on spawners and swarms
        if not monsters:
            return None
        monsterWeights = [entity.monster.weight for entity in monsters]
        monsterWrg = WeightedRandomGenerator(monsterWeights)
        monster = monsters[monsterWrg()]
        # With monster type and area determine monster position
        biomeArea = Rect(biome.area)
        intersection = Rect()
        if not biomeArea.intersects(area, intersection, self.realm.size.width):
            return None
        position = getPositionOnGroundForEntity(self.realm, TileRect(intersection), monster)
        if position is None or not placementValidation(self.realm, monster.content, position):
            return None
        # Create monster using type content and position
        entity = game.entity.create(monster.content)
        entity.monster.create()
        entity.setPosition(position)
        level = self.getCombatLevel()
        entity.combat.setLevel(level)
        World.get().move(entity, self.realm, Layer.Active)
        return entity

    def getSpawnArea(self):
        players = list(self.players)
        while players:
            player = select(players)
            area = self.getValidOffscreenArea(player.viewport)
            if area:
                return area
        return None

    def getValidOffscreenArea(self, viewport):
        def getSideOfOffscreen(onLeft):
            area = PixelRect(viewport)
            area.x += -900 if onLeft else area.width + 100
            area.y -= 200
            area.width = 800
            area.height += 400
            for player in self.nearbyPlayers:
                if area.intersects(player.viewport):
                    return None
            return Rect(area)

        onLeft = Random.getBool()
        area = getSideOfOffscreen(onLeft)
        if area is None:
            area = getSideOfOffscreen(not onLeft)
        return area

    def getBiomeWithWeight(self, area):
        biomeTracker = self.realm.biomeTracker
        nearbyBiomes = biomeTracker.getBiomes(area)
        biomes = []
        biomeWeights = []
        # Check if underground
        undergroundThreshold = biomeTracker.undergroundThreshold
        tileArea = TileRect(area)
        loopWidth = self.realm.size.tileWidth
        for x in xrange(tileArea.left, tileArea.right):
            if x >= loopWidth:
                x -= loopWidth
            if undergroundThreshold[x] <= tileArea.y:
                biome = Biome()
                biome.name = "Underground"
                underground = BiomeData(biome)
                underground.area = tileArea
                biomes.append(underground)
                biomeWeights.append(tileArea.width * tileArea.height)
                break
        for biome in nearbyBiomes:
            intersection = Rect()
            biomeArea = Rect(biome.area)
            if biomeArea.intersects(area, intersection, self.realm.size.width):
                weight = int(intersection.width * intersection.height)
                biomes.append(biome)
                biomeWeights.append(weight)
        if biomeWeights:
            biomeWrg = WeightedRandomGenerator(biomeWeights)
            return biomes[biomeWrg()]
        else:
            return None

    def getCombatLevel(self):
        level = sum((player.entity.combat.level.get() for player in self.nearbyPlayers)) / len(self.nearbyPlayers)
        level += self.hostility / 10 - 2
        return max(1, level)

    def decreaseHostility(self, amount):
        self.hostility -= amount
        if self.hostility == 0:
            self.becomeNeutral()

    def delayHostility(self, amount):
        if self.isHostile:
            self.updateTimer += amount

    def handleMonsterDeath(self, entity, isUnloading):
        if entity in self.monsters:
            self.monsters.remove(entity)
        if not isUnloading:
            self.decreaseHostility(1)

    def handleSpawnerDeath(self, entity, isUnloading):
        if entity in self.spawners:
            self.spawners.remove(entity)
        if not isUnloading:
            self.decreaseHostility(5)

    def save(self, stream):
        stream.writeUint8(self.state)
        stream.writeUint8(self.hostility)
        stream.writeUint32(self.updateTimer.remaining())
        stream.writeUint8(self.monsterReserve + len(self.monsters))
        stream.writeUint32(self.timeSinceLastSpawn)
        stream.writeUint8(len(self.spawners))
        for spawner in self.spawners:
            entityId = spawner.id if isinstance(spawner, Entity) else spawner
            stream.writeUint32(entityId)

    def load(self, stream):
        self.state = stream.readUint8()
        self.hostility = stream.readUint8()
        self.updateTimer.reset(stream.readUint32())
        self.monsterReserve = stream.readUint8()
        self.timeSinceLastSpawn = stream.readUint32()
        count = stream.readUint8()
        for _ in xrange(count):
            entityId = stream.readUint32()
            spawner = game.entity.get(entityId)
            if spawner is not None:
                self.addSpawner(spawner)
            else:
                self.spawners.append(entityId)


def placementValidation(realm, contentEntity, position):
    wall = realm.layers[Layer.Wall]
    tilePos = wall.getTilePosition(position)
    return wall.isEmpty(tilePos) or 'natural' in wall.getTileComponent(tilePos).getGroups()
