import random

from functools import partial
from collections import defaultdict

from siege.log import Log
from siege.world.realm import Layer, BiomeData
from siege.worldgen import Terraform, FillMode
from siege.util import TileRect, TileVector, Vector, Random, WeightedRandomGenerator

from core.helper import select
from core.terraform.stamps import shouldFlipX, SurfaceStamp, PlacedSurfaceStamp
from core.terraform.stamps import CAVE_ENTRANCE_COLOR


def GenerateSurfaceBiomes(game, world, realm, data):
    """ Continue to place down biomes until we have no more room. """

    biomeTypes = [b.data() for b in realm.biomeTracker.biomes if b.data().layer == 'surface']
    gen = BiomeGenerator(biomeTypes, realm.size)

    gen.addMinimums()
    while not gen.isFull():
        gen.addRandom()

    gen.expandBiomesToFit()

    biomeSpecs = gen.biomeSpecs

    Log.debug(str(gen._biomeCounts))

    previous = None
    result = []
    attempts = 0
    position = Random.get(realm.size.tileWidth)

    # Create actual BiomeData instances
    while biomeSpecs and attempts is not len(biomeSpecs):
        spec = select(biomeSpecs)
        biome = spec.biomeType
        width = spec.tileWidth
        if previous and not previous.isCompatible(biome):
            biomeSpecs.append(spec)
            attempts += 1
            continue
        attempts = 0
        previous = biome
        height = biome.height.getRandom()
        biomeData = BiomeData(biome)
        biomeData.area = TileRect(position, data['surfaceLevel'] - height, width, height)
        biomeData.ratio = Vector(width / float(biome.width.max * biome.widthIncrement), height / float(biome.height.max))
        result.append(biomeData)
        position += width
        if position >= realm.size.tileWidth:
            # Loop around the world seam
            position -= realm.size.tileWidth

    if attempts > 0:
        # TODO: This could become a stackoverflow if there really are no possible combinations...
        GenerateSurfaceBiomes(game, world, realm, data)
        return

    for biome in result:
        realm.biomeTracker.add(biome)
    data['biomes'] = result


def PlaceSurfaceBiomeStamps(game, world, realm, data):
    prevStamp = None
    allStamps = []
    stampAmounts = []

    surfaceBiomes = [b for b in data['biomes'] if b.biome.layer == "surface"]

    for index, biomeData in enumerate(surfaceBiomes):
        stamper = SurfaceBiomeStamper(data['surfaceLevel'], index, biomeData, prevStamp)
        placedStamps = stamper.placeBiomeStamps()
        prevStamp = placedStamps[-1]

        allStamps += placedStamps
        stampAmounts.append((biomeData, len(placedStamps)))

    _convergeSeamStamps(surfaceBiomes, allStamps)
    data['allStamps'] = allStamps

    startIndex = 0
    biomeStamps = {}
    actions = []
    data['caveEntrances'] = []
    realm.biomeTracker.undergroundThreshold = [0] * realm.size.tileWidth

    for biomeData, amount in stampAmounts:
        placedStamps = allStamps[startIndex:startIndex + amount]
        actions.append(partial(ApplySurfaceStamps, placedStamps, biomeData.biome))
        _findCaveEntrances(data['caveEntrances'], placedStamps)
        startIndex += amount
        biomeStamps[biomeData.uid] = placedStamps
    data['biomeStamps'] = biomeStamps

    actions.append(lambda game, world, realm, data: realm.biomeTracker.initializeUndergroundThreshold())

    return actions


def ApplySurfaceStamps(stamps, biome, game, world, realm, data):
    """ Go through the list of stamps and apply them to the world. """
    UNDERGROUND_TOLERANCE = 4

    rules = biome.getRules()
    rules[0] = (0, 0)

    for placed in stamps:
        placed.applyStamp(realm.layers, realm.automata, rules)
        Terraform.populateAreaEmptyThreshold(realm.layers[Layer.Ground], placed.getArea(), UNDERGROUND_TOLERANCE, realm.biomeTracker.undergroundThreshold)

        # fill from the bottom of the stamp to the bottom of the biome
        y = placed.position.y + placed.stamp.getSize().y
        area = TileRect(placed.position.x, y, placed.stamp.getSize().x, data['surfaceLevel'] - y)
        Terraform.fillTiles(realm.layers[Layer.Ground], area, biome.groundId, FillMode.ALL)
        Terraform.fillTiles(realm.layers[Layer.Wall], area, biome.wallId, FillMode.ALL)


def QueueBiomeActions(game, world, realm, data):
    actions = []
    for biomeData in data['biomes']:
        for action in biomeData.biome.getActions(biomeData):
            actions.append(partial(action, biomeData.area, biomeData.ratio.x))
    return actions


def QueueBiomePostActions(game, world, realm, data):
    actions = []
    for biomeData in data['biomes']:
        for action in biomeData.biome.getPostActions(biomeData):
            actions.append(partial(action, biomeData.area, biomeData.ratio.x))
    return actions


class BiomeSpec(object):
    def __init__(self, biomeType, tileWidth):
        self.biomeType = biomeType
        self.tileWidth = tileWidth

    def expandToFit(self, remainingSpace):
        if (remainingSpace % self.biomeType.widthIncrement) != 0:
            return False

        newWidth = self.tileWidth + remainingSpace
        if (newWidth / self.biomeType.widthIncrement) > self.biomeType.width.max:
            return False

        self.tileWidth = newWidth
        return True


class BiomeGenerator(object):
    def __init__(self, biomeTypes, realmSize):
        self.biomeTypes = biomeTypes
        self.realmSize = realmSize

        self.biomeSpecs = []

        self._biomeCounts = defaultdict(lambda: 0)
        self._remainingSpace = realmSize.tileWidth
        self._weights = [biome.frequency for biome in biomeTypes]

    def addMinimums(self):
        for biomeType in self.biomeTypes:
            count = biomeType.getOccurrences(self.realmSize).min
            for _ in range(count):
                self.addBiome(biomeType)

    def addRandom(self):
        self.addBiome(self._chooseRandomBiomeType())

    def addBiome(self, biomeType):
        self._checkBiomeTypeCap(biomeType)
        self._checkBiomeTypeWidthMinimum(biomeType)

        tileWidth = self._getWidthForBiomeType(biomeType)
        self.biomeSpecs.append(BiomeSpec(biomeType, tileWidth))

        self._decrementSpace(tileWidth)
        self._incrementCount(biomeType)

    def biomeTypeCapReached(self, biomeType):
        maxOccurrences = biomeType.getOccurrences(self.realmSize).max
        return self._biomeCounts[biomeType] == maxOccurrences

    def biomeTypeSpaceAvailable(self, biomeType):
        minTileWidth = (biomeType.width.min * biomeType.widthIncrement)
        return self._remainingSpace >= minTileWidth

    def isFull(self):
        """ Returns True if all biomeTypes are maxed out or too big.

        Over time, _decrementSpace and _incrementCount will set the weights for
        all biomeTypes to zero as they reach their occurrence cap, or as the
        remaining space decreases below their minimum required tileWidth.

        """
        return sum(self._weights) == 0

    def expandBiomesToFit(self):
        if self._remainingSpace == 0:
            return

        Log.debug("Filling remaining width of {0} tiles".format(self._remainingSpace))

        for spec in self.biomeSpecs:
            if spec.expandToFit(self._remainingSpace):
                return
            else:
                continue

        raise RuntimeError("Couldn't expand any biomes to fill remaining space.")

    def _getWidthForBiomeType(self, biomeType):
        tileWidth = biomeType.width.getRandom() * biomeType.widthIncrement

        if tileWidth > self._remainingSpace:
            if (self._remainingSpace % biomeType.widthIncrement) == 0:
                return self._remainingSpace
            else:
                raise RuntimeError("Biome width increment is not divisible by remaining space; panicking.")
        else:
            return tileWidth

    def _chooseRandomBiomeType(self):
        wrg = WeightedRandomGenerator(self._weights)
        biomeType = self.biomeTypes[wrg()]
        return biomeType

    def _decrementSpace(self, tileWidth):
        self._remainingSpace -= tileWidth
        for biomeType in self.biomeTypes:
            if not self.biomeTypeSpaceAvailable(biomeType):
                self._deactivateBiomeType(biomeType)

    def _incrementCount(self, biomeType):
        self._biomeCounts[biomeType] += 1
        if self.biomeTypeCapReached(biomeType):
            self._deactivateBiomeType(biomeType)

    def _deactivateBiomeType(self, biomeType):
        index = self.biomeTypes.index(biomeType)
        self._weights[index] = 0

    def _checkBiomeTypeCap(self, biomeType):
        if self.biomeTypeCapReached(biomeType):
            raise RuntimeError("Can't add another {0} biome, maximum occurrences already reached.".format(biomeType))

    def _checkBiomeTypeWidthMinimum(self, biomeType):
        if not self.biomeTypeSpaceAvailable(biomeType):
            raise RuntimeError("Can't add another {0} biome, minimum width exceeds available space.".format(biomeType))


def _findCaveEntrances(caveEntrances, placedStamps):
    for stamp in placedStamps:
        if stamp.stamp.has(CAVE_ENTRANCE_COLOR):
            # Get cave entrance connectors and calculate final point and add to list...
            for entrance in stamp.stamp.getColor(CAVE_ENTRANCE_COLOR):
                caveEntrances.append(TileVector(stamp.position.x + entrance.x, stamp.position.y + entrance.y))


def _convergeSeamStamps(surfaceBiomes, allStamps):
    if allStamps[-1].getRight() == allStamps[0].getLeft():
        return True

    biomeIndex = -1

    stampIndex = 0
    convergeDistance = allStamps[0].getLeft() - allStamps[-1].getRight()
    convergenceRequired = True

    while convergenceRequired:
        biomeIndex += 1
        realBiome = surfaceBiomes[biomeIndex]
        realBiome.biome.reset(realBiome.area)
        remainingSpace = realBiome.area.width

        while remainingSpace > 0 and convergenceRequired:
            currentStamp = allStamps[stampIndex]
            prevStamp = allStamps[stampIndex - 1]
            nextStamp = allStamps[stampIndex + 1]

            candidates = list(realBiome.biome.getStamps(remainingSpace))
            stamps = []
            perfectStamps = []
            for stamp in candidates:
                surfaceStamp = SurfaceStamp(stamp)
                if abs(surfaceStamp.slope()) < abs(convergeDistance):
                    stamps.append(stamp)
                elif abs(surfaceStamp.slope()) == abs(convergeDistance):
                    perfectStamps.append(stamp)
            if perfectStamps:
                stamps = perfectStamps

            if len(stamps) == 0:
                stamps = list(candidates)

            stamp = random.choice(stamps)

            shouldFlipStamp = stamp.forceFlipX() or not ((SurfaceStamp(stamp).slope() >= 0) ^ (convergeDistance < 0))

            currentStamp = PlacedSurfaceStamp(stamp, currentStamp.position, shouldFlipStamp)
            currentStamp.alignWith(prevStamp)
            allStamps[stampIndex] = currentStamp

            if currentStamp.exceedsBiomeHeight(realBiome):
                realBiome.area.height += realBiome.area.y - currentStamp.position.y
                realBiome.area.y = currentStamp.position.y

            convergeDistance = nextStamp.getLeft() - currentStamp.getRight()
            convergenceRequired = convergeDistance != 0
            stampIndex += 1
            if stampIndex >= len(allStamps):
                stampIndex = 0

            remainingSpace -= stamp.getSize().x


class SurfaceBiomeStamper(object):
    def __init__(self, surfaceLevel, index, realBiome, previousStamp):
        self.surfaceLevel = surfaceLevel
        self.index = index
        self.realBiome = realBiome
        self.previousStamp = previousStamp

    def placeBiomeStamps(self):
        remainingSpace = self.realBiome.area.width
        stampPosition = TileVector(self.realBiome.area.x, 0)

        placedStamps = []

        self.realBiome.biome.reset(self.realBiome.area)
        candidates = list(self.realBiome.biome.getStamps(remainingSpace))

        while remainingSpace > 0:
            stampIndex = Random.get(len(candidates) - 1)
            stamp = candidates[stampIndex]
            del candidates[stampIndex]

            stampPosition.y = self.surfaceLevel - stamp.getSize().y
            placedStamp = self._placeStamp(stamp, stampPosition, remainingSpace)

            if placedStamp is None:
                continue
            else:
                placedStamps.append(placedStamp)
                self.previousStamp = placedStamp

                remainingSpace -= stamp.getSize().x
                stampPosition.x += stamp.getSize().x

                candidates = list(self.realBiome.biome.getStamps(remainingSpace))

        return placedStamps

    def _placeStamp(self, stamp, stampPosition, remainingSpace):
        currentStamp = PlacedSurfaceStamp(stamp, stampPosition, shouldFlipX(stamp))

        if self._isFirstStamp():
            pass
        else:
            currentStamp.alignWith(self.previousStamp)

        if not self._isValidStamp(currentStamp, remainingSpace):
            return None

        if currentStamp.exceedsBiomeHeight(self.realBiome):
            self.realBiome.area.height = self.surfaceLevel - self.realBiome.area.y
            self.realBiome.area.y = currentStamp.position.y

        return currentStamp

    def _isFirstStamp(self):
        return not self.previousStamp

    def _isValidStamp(self, placedStamp, remainingSpace):
        if placedStamp.getRight() > self.surfaceLevel:
            return False
        return True
