from core.helper import callEvent, sendMessage
from core.system.client import PlayerReserveChanges
from core.talent import calculateTpPoints, gainTalentPoints

from siege import game, InventoryItem, ItemList, ToolItem
from siege.component import CraftResult, ToolbarSlot
from siege.log import Log
from siege.network import Message, MessageHandler, NetworkManager
from siege.util import Random
from siege.subsystem import InventorySet
from siege.world import World
from siege.world.realm import Layer


class ServerHandler(object):
    INVENTORY_LAYERS = [Layer.Back, Layer.Item, Layer.Active]

    @staticmethod
    def registration(world):
        if NetworkManager.isHost():
            game.network.getServer().register(Message.ITEM_CRAFT, MessageHandler.create(ServerHandler.handleItemCraft))
            game.network.getServer().register(Message.ITEM_RESEARCH, MessageHandler.create(ServerHandler.handleResearch))
            game.network.getServer().register(Message.ITEM_ACTIVE_TOOLBAR, MessageHandler.create(ServerHandler.handleActiveToolbar))
            game.network.getServer().register(Message.PLAYER_RESERVE, MessageHandler.create(ServerHandler.handleReserveRequest))

    @staticmethod
    def handleItemCraft(client, packet):
        player = World.get().getPlayer(client)
        # Read item content id and get content
        content = game.content.get(packet.readUint32())
        count = packet.readUint32()
        craftSystem = game.craft

        if content.id not in craftSystem.getComponents() or not player.entity.research.hasRecipe(content.id):
            #Invalid content id or player does not own this recipe
            Log.warn("Invalid content id or player does not own this recipe")
            return

        craft = content.entity.craft
        inventory = player.entity.inventory

        #Make a deep copy of the material list
        uniqueCount = 0
        materials = ItemList()
        reserves = player.entity.reserves
        for material in craft.materials:
            entity = material.content.entity
            if entity.item.isUnique:
                if 'craft' not in reserves:
                    Log.warn("Player does not have a craft reserve even though crafted item has unique materials.")
                    return
                craftReserve = reserves['craft']
                if uniqueCount >= craftReserve.size():
                    Log.warn("Player does not have enough items in craft reserve.")
                    return
                item = craftReserve.get(uniqueCount)
                if item.quantity < material.quantity:
                    Log.warn("Craft reserve does not have enough quantity of a material.")
                    return
                materials.append(item)
                uniqueCount += 1
            else:
                materials.append(InventoryItem(entity, material.quantity))

        callEvent("craft_start", player, craft, count, materials)

        # Verify that the player has the required materials (or that he can get them from nearby chests)
        providers = InventorySet()
        for material in materials:
            neededQuantity = material.quantity * count
            if uniqueCount > 0 and material.entity.item.isUnique:
                continue
            availableQuantity = inventory.getContentQuantity(material.content)
            if availableQuantity > 0:
                providers.add(inventory)
            metRequirement = neededQuantity <= availableQuantity
            if not metRequirement:
                potentialProviders = craftSystem.getAvailableInventories(player.entity.realm.layers, ServerHandler.INVENTORY_LAYERS, player.entity)
                for potential in potentialProviders:
                    provided = potential.getContentQuantity(material.content)
                    if provided > 0:
                        providers.add(potential)
                        availableQuantity += provided
                        metRequirement = neededQuantity <= availableQuantity
                        if metRequirement:
                            break
            if not metRequirement:
                #Not enough materials available to craft this item
                return

        #Remove all of the materials needed for this craft from the providers' inventories
        for material in materials:
            if uniqueCount > 0 and material.entity.item.isUnique:
                continue
            remaining = material.quantity * count
            for provider in providers:
                remaining = provider.remove(material.content, remaining)
                if remaining == 0:
                    break

        #TODO: Right now only unique items come from the craft inventory which means all items in it have 1 quantity and cannot have a remainder
        #      However, in the future this will not be the case and it could be possible to have extra
        if uniqueCount > 0:
            reserves.resize("craft", uniqueCount)

        #Determine the quality level of the craft
        talentLevel = player.entity.talents.craft.level.get()
        diff = talentLevel - craft.level
        stats = player.entity.stats
        quality = max(0, talentLevel * 2 + diff * 4 + stats.craft.get())
        results = callEvent("craft_quality", player, craft, quality=quality, maxQuality=0)
        quality, maxQuality = results.quality, results.maxQuality
        quality = int(min(quality, maxQuality))
        result = craft.results[0]
        if diff >= 0 and len(craft.results) > 1:
            for i in xrange(1, len(craft.results)):
                current = craft.results[i]
                if current.quality > quality:
                    break
                chance = diff + 1 + ((quality - current.quality) / 5.0) + (stats.LUCK.get() / 15.0)
                results = callEvent("craft_chance", player, craft, quality, current, chance=chance, maxChance=75.0)
                chance, maxChance = results.chance, results.maxChance
                chance = int(min(chance, maxChance) * 100)
                random = callEvent("craft_random", player, craft, quality, current, chance, random=Random.get(10000)).random
                if chance < random:
                    break
                result = current

        #Make a copy here so that it could be changed without affecting the original craft component
        result = CraftResult(result)

        def finishCraft(entity, quantity):
            item = InventoryItem(entity, quantity)
            craft.onCraft.invoke(player, craft, materials, count, result, quality, item)
            remainder = inventory.add(item.entity, item.quantity)
            callEvent("craft_finish", player, craft, count, result, item, remainder)

        #Add the resulting crafted items to the player's inventory
        for item in result.items:
            entity = item.content.entity
            amount = item.quantity * count
            if entity.has('item') and entity.item.isUnique:
                for x in xrange(count):
                    if entity.has('equipment'):
                        entity = game.equipment.create(item.content, max(0, 255))
                    else:
                        entity = game.entity.create(item.content)
                    finishCraft(entity, 1)
            else:
                finishCraft(entity, amount)
            sendMessage('general', 'System', "CraftSuccess", player.networkId, recipe=entity.name, amount=amount)

    @staticmethod
    def handleResearch(client, packet):
        category = packet.readString()
        player = World.get().getPlayer(client)
        # Validate packet to current game state
        reserves = player.entity.reserves
        if 'research' not in reserves:
            Log.warn("Research requested but player does not have research reserve.")
            return
        item = reserves['research'].get(0)
        if not item:
            Log.warn("Research requested but research reserve is empty.")
            return
        if not game.research.isResearchAvailable(player):
            Log.warn("Research requested but research is not available to the player.")
            return
        research = player.entity.research
        recipes = game.research.analyze(player.entity, category, item.content.id)
        if not recipes:
            Log.warn("Research requested but no recipes available for this category ({})".format(category))
            return

        # Pick a few recipes based off of the level difference for the player
        craftLevel = player.entity.talents.craft.level.get()
        count = 0
        for recipe, level in recipes:
            levelDiff = level - craftLevel
            threshold = craftLevel + 4 - count * 2
            if threshold < levelDiff:
                break
            count += 1
        count = max(1, min(count, min(len(recipes), 3)))

        talentPoints = 0
        for i in xrange(count):
            recipe = recipes[i][0]
            research.researchRecipe(recipe, item.content.id)
            message = "ResearchRecipeComplete" if research.hasRecipe(recipe) else "ResearchRecipe"
            recipeEntity = game.content.get(recipe).entity
            sendMessage("general", "System", message, player.networkId, recipe=recipeEntity.name)
            talentPoints += calculateTpPoints(player, player.entity.talents.craft, recipeEntity.craft.level, 30, recipeEntity.craft.experience)
        #Gain TP
        gainTalentPoints(player, player.entity.talents.craft, talentPoints)
        # Consume the material
        if item.quantity > 1:
            item.quantity -= 1
        else:
            item = InventoryItem()
        reserves['research'].set(0, item)

    @staticmethod
    def handleActiveToolbar(client, packet):
        player = World.get().getPlayer(client)
        previousSlot = ToolbarSlot(packet.readUint8())
        previousIndex = packet.readUint16()
        slot = ToolbarSlot(packet.readUint8())
        selected = packet.readUint16()
        hasPrevious = packet.readBool()
        if hasPrevious:
            previousItem = ToolItem()
            previousItem.unpack(packet, game.content, game.entity)
        else:
            previousItem = player.entity.toolbar.get(previousIndex, previousSlot)
        #TODO: Validate selected and slot

        primaryItem = player.entity.toolbar.get(selected, ToolbarSlot.LEFT)
        secondaryItem = player.entity.toolbar.get(selected, ToolbarSlot.RIGHT)
        activeItem = primaryItem if slot == ToolbarSlot.LEFT else secondaryItem
        player.entity.playerState.activeItem = activeItem
        if activeItem.entity and activeItem.entity.has('item'):
            item = activeItem.entity.item
            Log.info("{} changed active item to {}".format(player.entity.name, activeItem.entity.name))
            player.entity.render.changeSprite(item.hold)

        gear = player.entity.gear
        ServerHandler.attemptEquip(gear, primaryItem, "active1")
        if not secondaryItem.isItem() or not primaryItem.isItem() or secondaryItem.entity.id != primaryItem.entity.id:
            ServerHandler.attemptEquip(gear, secondaryItem, "active2")
        else:
            gear.unequip("active2")

        # Deactivate the previous item
        if previousItem and previousItem.isItem() and previousItem.entity and previousItem.entity.has('event'):
            if previousItem.entity.event.has('deactivate_item'):
                previousItem.entity.event['deactivate_item'].invoke(player)
        # Activate the current item
        if activeItem and activeItem.isItem() and activeItem.quantity and activeItem.entity and activeItem.entity.has('event'):
            if activeItem.entity.event.has('activate_item'):
                activeItem.entity.event['activate_item'].invoke(player)

    @staticmethod
    def attemptEquip(gearComponent, item, gearSlot):
        if item.isItem() and gearComponent.canEquip(item):
            if item.entity.has('equipment') and item.entity.equipment.getSlot() == "active" and gearComponent.canEquip(item, gearSlot):
                gearComponent.equip(item, gearSlot)
        else:
            gearComponent.unequip(gearSlot)

    @staticmethod
    def handleReserveRequest(client, packet):
        player = World.get().getPlayer(client)
        changeType = packet.readUint8()
        name = packet.readString()
        if changeType == PlayerReserveChanges.Resize:
            size = packet.readUint8()
            player.entity.reserves.resize(name, size)
        elif changeType == PlayerReserveChanges.Restore:
            player.entity.reserves.restore(name, player.entity.realm.dropped)
