from functools import partial
import os
import math

from core.helper import select, sendMessage
from core.component.travel import TravelComponent

from siege import game
from siege.io import DataStream, File
from siege.log import Log
from siege.network import Message, NetworkManager, MessageHandler
from siege.subsystem import Subsystem
from siege.util import Vector, Timer, seconds, Sizes
from siege.world import World


class TravelPlayer(object):
    def __init__(self, uid=0):
        self.uid = uid
        self.attuned = 0
        self.crystals = []
        self.current = 0

    def isAttunedTo(self, crystal):
        return self.attuned == crystal.entityId

    def write(self, stream):
        stream.writeUint32(self.uid)
        stream.writeUint32(self.attuned)
        stream.writeUint32(len(self.crystals))
        for entityId in self.crystals:
            stream.writeUint32(entityId)

    def read(self, stream):
        self.uid = stream.readUint32()
        self.attuned = stream.readUint32()
        crystalsCount = stream.readUint32()
        for _ in xrange(crystalsCount):
            self.crystals.append(stream.readUint32())


class TravelCrystal(object):
    def __init__(self, entity=None):
        if entity:
            self.name = entity.name
            self.entityId = entity.id
            self.position = entity.getPosition()
        else:
            self.name = ""
            self.entityId = 0
            self.position = Vector()
        self.distance = 0
        self.cost = 0
        self.biome = ""

    def setupForUse(self, travelPlayer, interactedCrystal):
        #This crystal may not be loaded but the interactedCrystal is definitely
        loopWidth = game.entity.get(interactedCrystal.entityId).loopWidth
        distance = self.position.getDistance(interactedCrystal.position, loopWidth)
        self.distance = int(distance / Sizes.TILE)
        self.cost = 1
        if not travelPlayer.isAttunedTo(self):
            self.cost += int(math.pow(self.distance / 100, 0.65))

    def pack(self, packet):
        packet.writeString(self.name)
        packet.writeUint32(self.entityId)
        packet.writeString(self.biome)
        packet.writeVector(self.position)
        packet.writeUint32(self.distance)
        packet.writeUint8(self.cost)

    def unpack(self, packet):
        self.name = packet.readString()
        self.entityId = packet.readUint32()
        self.biome = packet.readString()
        self.position = packet.readVector()
        self.distance = packet.readUint32()
        self.cost = packet.readUint8()

    def write(self, stream):
        self.pack(stream)

    def read(self, stream):
        self.unpack(stream)

    def __repr__(self):
        return "<TravelCrystal({0.name}, {0.entityId}, {0.biome}, {0.position}, {0.distance}, {0.cost})>".format(self)


class TravelSystem(Subsystem):
    NAME = "travel"
    CRYSTAL_UPDATE_TIME = seconds(15)

    def __init__(self):
        Subsystem.__init__(self)
        if NetworkManager.isHost():
            server = game.network.getServer()
            server.register(Message.TRAVEL_ATTUNE, MessageHandler.create(self._handleAttune))
            server.register(Message.TRAVEL_SELECT, MessageHandler.create(self._handleTravel))
        Log.info("Creating TravelSystem")
        self.players = {}
        self.availableWaypointNames = BIOME_WAYPOINT_NAMES
        self.crystals = {}
        self.loadedCrystals = {}
        self.updateTimer = Timer(self.CRYSTAL_UPDATE_TIME)
        self.freeze()

    def getName(self):
        return self.NAME

    def create(self, entity, type, definition):
        component = TravelComponent(definition)
        entity.add(component)
        if not entity.isContentEntity():
            self.add(entity)

    def add(self, entity):
        if entity.has(TravelComponent.CID) and not entity.isContentEntity():
            self.loadedCrystals[entity] = entity.travel
            entity.event["interact"].listen(self.handleTouchCrystal)

    def remove(self, entity):
        if entity in self.loadedCrystals:
            entity.event["interact"].remove(self.handleTouchCrystal)
            del self.loadedCrystals[entity]

    def clear(self, entity):
        self.loadedCrystals.clear()

    def update(self, frameTime):
        if not NetworkManager.isHost():
            return
        self.updateTimer.update(frameTime)
        if self.updateTimer.expired():
            self.updateTimer.reset(self.CRYSTAL_UPDATE_TIME)
            for entity, component in self.loadedCrystals.iteritems():
                component.update(self.CRYSTAL_UPDATE_TIME)

    def registerCrystal(self, crystalEntity):
        game.entity.get(crystalEntity.id)
        biomeData = crystalEntity.realm.biomeTracker.getBiome(crystalEntity.getPosition())
        biomeName = biomeData.biome.name if biomeData else "Underground"
        crystalEntity.setName(pickNameForCrystal(crystalEntity, biomeName, self.availableWaypointNames) + " Crystal")
        crystal = TravelCrystal(crystalEntity)
        crystal.biome = biomeName
        self.crystals[crystalEntity.id] = crystal

    def handleTouchCrystal(self, player, entity, position):
        if self.activateCrystal(player, entity):
            sendMessage('general', 'System', "TravelActivate", player.networkId, crystal=entity.name)
        self._sendTravelUiInfo(player.networkId, entity, self._getPlayer(player))
        self.players[player.uid].current = entity.id

    def activateCrystal(self, player, entity):
        travelPlayer = self._getPlayer(player)
        if entity.id not in travelPlayer.crystals:
            travelPlayer.crystals.append(entity.id)
            return True
        return False

    def attuneCrystal(self, player, entity):
        travelPlayer = self._getPlayer(player)
        travelPlayer.attuned = entity.id

    @staticmethod
    def useWayShard(args):
        player = args['player']
        if game.travel._useWayShard(player):
            args['decrement'] = 1
        else:
            args['cancel'] = True
            sendMessage('general', 'System', "TravelAttuneNeeded", player.networkId)

    def _useWayShard(self, player):
        travelPlayer = self._getPlayer(player)
        if not travelPlayer.attuned:
            return False
        self.travel(player, travelPlayer.attuned)
        return True

    def travel(self, player, crystalId):
        crystal = self.crystals[crystalId]
        World.get().requestRegion(World.get().realm, crystal.position, partial(self._completeTravel, player, crystalId))

    def _completeTravel(self, player, crystalId):
        entity = game.entity.get(crystalId)
        crystalArea = entity.render.getRect()
        # the position of the player is the bottom center, not the top left like for items
        destination = Vector(crystalArea.x, crystalArea.y + crystalArea.height)
        player.entity.setPosition(destination)
        stream = DataStream(Message.PLAYER_POSITION)
        stream.writeVector(destination)
        game.network.getServer().send(player.networkId, stream)
        player.entity.physics.wake()
        if player.entity.has('descent'):
            player.entity.descent.reset()
        packet = DataStream(Message.TRAVEL)
        game.network.getServer().send(player.networkId, packet)
        sendMessage('general', 'System', "TravelComplete", player=player.entity.name, crystal=entity.name)

    def _sendTravelUiInfo(self, networkId, crystalEntity, travelPlayer):
        packet = DataStream(Message.TRAVEL_SHOW_UI)
        packet.writeUint32(travelPlayer.attuned)
        currentCrystal = self.crystals[crystalEntity.id]
        currentCrystal.pack(packet)
        packet.writeUint32(len(travelPlayer.crystals) - 1)
        for entityId in travelPlayer.crystals:
            if entityId == crystalEntity.id:
                continue
            crystal = self.crystals[entityId]
            crystal.setupForUse(travelPlayer, currentCrystal)
            crystal.pack(packet)
        game.network.getServer().send(networkId, packet)

    def _getPlayer(self, player):
        uid = player.uid
        if uid not in self.players:
            self.players[uid] = TravelPlayer(uid)
        return self.players[uid]

    def _handleAttune(self, client, packet):
        player = World.get().getPlayer(client)
        crystalId = packet.readUint32()
        travelPlayer = self._getPlayer(player)
        travelPlayer.attuned = crystalId
        sendMessage('general', 'System', "TravelAttune", player.networkId, crystal=game.entity.get(crystalId).name)

    def _handleTravel(self, client, packet):
        player = World.get().getPlayer(client)
        crystalId = packet.readUint32()
        if crystalId not in self.crystals:
            Log.info("Invalid crystal id")
            return
        shardContent = game.content.get("way_shard")
        crystal = self.crystals[crystalId]
        crystal.setupForUse(self._getPlayer(player), self.crystals[self.players[player.uid].current])
        player.entity.inventory.remove(shardContent, crystal.cost)
        self.travel(player, crystalId)

    def _getPath(self):
        return os.path.join(World.get().path, "travel")

    def _saveData(self, world, asynchronous):
        stream = DataStream()
        stream.writeUint32(len(self.players))
        for travelPlayer in self.players.values():
            travelPlayer.write(stream)

        stream.writeUint32(len(self.crystals))
        for entityId, travelCrystal in self.crystals.iteritems():
            travelCrystal.write(stream)

        stream.writeUint32(len(self.availableWaypointNames))
        for biomeName, waypointNames in self.availableWaypointNames.iteritems():
            stream.writeString(biomeName)
            stream.writeUint32(len(waypointNames))
            for waypointName in waypointNames:
                stream.writeString(waypointName)
        if asynchronous:
            game.file.asyncWrite(self._getPath(), stream)
        else:
            File.save(self._getPath(), stream)

    def _loadData(self, world):
        Log.info("Loading TravelSystem")
        if not os.path.isfile(self._getPath()):
            self.players = {}
            self.availableWaypointNames = BIOME_WAYPOINT_NAMES
        else:
            stream = DataStream()
            File.load(self._getPath(), stream)

            players = {}
            playersCount = stream.readUint32()
            for _ in xrange(playersCount):
                travelPlayer = TravelPlayer()
                travelPlayer.read(stream)
                players[travelPlayer.uid] = travelPlayer

            self.crystals.clear()
            crystalsCount = stream.readUint32()
            for _ in xrange(crystalsCount):
                crystal = TravelCrystal()
                crystal.read(stream)
                self.crystals[crystal.entityId] = crystal

            availableWaypointNames = {}
            availableCount = stream.readUint32()
            for _ in xrange(availableCount):
                biomeName = stream.readString()
                availableWaypointNames[biomeName] = []
                namesCount = stream.readUint32()
                for _ in xrange(namesCount):
                    availableWaypointNames[biomeName].append(stream.readString())

            self.players = players
            self.availableWaypointNames = availableWaypointNames

    @staticmethod
    def register():
        travel = TravelSystem()
        game.registerSubsystem(travel, TravelSystem.NAME, TravelComponent.TYPE)
        game.events['world_saved'].listen(travel._saveData)
        game.events['world_created'].listen(travel._loadData)

    @staticmethod
    def unregister():
        if game.hasSystem(TravelSystem.NAME):
            travelSystem = game.travel
            game.events['world_saved'].remove(travelSystem._saveData)
            game.events['world_created'].remove(travelSystem._loadData)
            game.unregisterSystem(TravelSystem.NAME)


def pickNameForCrystal(entity, biomeName, availableNames):
    """ Pick a biome-appropriate, unique name for a given crystal.

    Removes the name from the list of available names to ensure uniqueness.

    """
    if biomeName in availableNames:
        # default to generic if this biome has run out of names
        biomeNames = availableNames[biomeName] or availableNames['generic']
    else:
        biomeNames = availableNames['generic']
    if not biomeNames:
        # against all odds, ALL biome-appropriate AND generic names have
        # been used. give up and return a whitebread name
        return "Normal"
    else:
        return select(biomeNames).capitalize()


BIOME_WAYPOINT_NAMES = {
    "Plains": "leaf grassy gaia dandelion sunbeam stargazing rainy".split(" "),
    "Scorched": "ashen soot oil cracked petrified dust desolate".split(" "),
    "Mountain": "sasquatch chasm valley boulder crevasse granite summit".split(" "),
    "Desert": "sandy oasis mirage dune cactus parched burning".split(" "),
    "Underground": "cavern stalagmite bedrock sunken buried spelunking igneous bottomless".split(" "),
    "generic": "quiet raucous minty caramel existential cowboy cheeky enigmatic lucid".split(" "),
}
