from core.system.client import ClientAssist
from core.ui import boolStr, DialogUi

from siege import game, Locale, GrabbedItemLocation
from siege.io import DataStream
from siege.log import Log
from siege.network import Message
from siege.util import PixelRect, StringSet, Vector, Uint32Set
from siege.world import World
from siege.world.realm import Layer


class CraftUi(DialogUi):
    #How far the player has to move to update available services and inventories
    DELTA_THRESHOLD = 16
    SERVICE_LAYERS = [Layer.Back, Layer.Item, Layer.Active]
    INVENTORY_LAYERS = [Layer.Back, Layer.Item, Layer.Active]

    def __init__(self, player, tooltip):
        self.player = player
        windowSize = game.render.getWindowSize()
        width = 600
        height = 450
        x = 375
        y = (windowSize.y / 2) - (height / 2)
        DialogUi.__init__(self, 'craft', PixelRect(x, y, width, height))
        #Register UI callbacks
        self.view.register('populate', self.onPopulate)
        self.view.register('craft', self.onCraft)
        self.view.register('selectItem', self.handleSelectItem)
        self.view.register('clickedItem', self.handleClickedItem)
        self.view.register('splitItem', self.handleSplitItem)
        self.view.register('showTooltip', tooltip.onShow)
        self.view.onHide.listen(self.hide)
        self.load('html/craft.html')

        #List of available surface services
        self.services = game.craft.getAvailableServices(World.get().getRealm().layers, self.SERVICE_LAYERS, self.player.entity)
        #List of available inventories
        self.inventories = [self.player.entity.inventory]
        #List of created items in javascript
        self.createdItems = Uint32Set()
        self.createdServices = StringSet()
        #Player Movement delta used for determining when to update available services and inventories
        self.movementDelta = Vector()
        self.currentUniqueCount = 0

        #Setup listeners
        self.inventories[0].onChange.listen(self.handleInventoryChange)
        self.player.entity.research.onRecipeChange.listen(self.handleRecipeChange)
        self.player.entity.render.onMove.listen(self.handlePlayerMove)
        self.player.onGrabbedChange.listen(self.handleGrabbedChange)
        self.player.entity.reserves.observe('craft', self.handleReserveChange)
        self.player.entity.event["realm_change"].listen(self.handleRealmChange)
        self.handleRealmChange(self.player, None, self.player.entity.realm)

    def hide(self):
        super(CraftUi, self).hide()
        self.view.execute("App.materialsView.setSelected(null);")

    def onPopulate(self, view, args):
        self.createdItems.clear()
        self.createdServices.clear()
        categories = {}
        commands = []
        #Go through all craftables and create a App.Item object for them
        craftComponents = game.craft.getComponents()
        for craftComponent in craftComponents:
            itemId = craftComponent.key()
            component = craftComponent.data()
            itemContent = component.getParent().content
            itemEntity = itemContent.entity
            #Add the item to the component and subcategory mapping
            if component.category in categories:
                category = categories[component.category]
                if component.subcategory in category:
                    category[component.subcategory].append(itemContent.id)
                else:
                    category[component.subcategory] = [itemContent.id]
            else:
                categories[component.category] = {component.subcategory: [itemContent.id]}
            #Create the item in javascript
            commands.append("var item = App.Item.create({{id: {}, name: '{}'}});".format(itemId, Locale.getEscaped(itemEntity.name)))
            commands.append("item.icon = 'asset://crea/{}';".format(itemEntity.render.getIconPath()))
            if itemEntity.has('item'):
                commands.append("item.set('isUnique', {});".format(boolStr(itemEntity.item.isUnique)))
            serviceRequired = component.serviceRequired
            if serviceRequired:
                if serviceRequired not in self.createdServices:
                    commands.append("App.services.pushObject(App.Service.create({{key: '{}', name: '{}'}}));".format(serviceRequired, Locale.getEscaped(serviceRequired)))
                    self.createdServices.add(serviceRequired)
                commands.append("item.service = App.services.findProperty('key', '{}');".format(serviceRequired))
            #if component.output != 1:
            #    commands.append("item.output = %d;" % component.output)
            #TODO: Figure out what to do with the output...
            commands.append("item.output = {};".format(1))
            commands.append("App.items[{}] = item;".format(itemId))
            self.createdItems.add(itemId)

        #Now populate item materials
        recipes = self.player.entity.research.recipes
        for craftComponent in craftComponents:
            itemId = craftComponent.key()
            component = craftComponent.data()
            commands.append("var item = App.items[{}];".format(itemId))
            knownMaterials = recipes.get(itemId, [])
            for index, material in enumerate(component.materials):
                if material.content.id not in self.createdItems:
                    itemEntity = material.content.entity
                    commands.append("var material = App.Item.create({{id: {}, name: '{}'}});".format(material.content.id, Locale.getEscaped(itemEntity.name)))
                    commands.append("material.icon = 'asset://crea/{}';".format(itemEntity.render.getIconPath()))
                    if itemEntity.has('item'):
                        commands.append("material.set('isUnique', {});".format(boolStr(itemEntity.item.isUnique)))
                    commands.append("App.items[{}] = material;".format(material.content.id))
                    self.createdItems.add(material.content.id)
                known = str(material.content.id in knownMaterials).lower()
                material = "App.Material.create({{item: App.items[{}], quantity: {}, known: {}, index: {}}})".format(material.content.id, material.quantity, known, index)
                commands.append("item.materials.pushObject({});".format(material))

        #Populate the categories and subcategories now that we have them setup
        for category, subcategories in categories.iteritems():
            icon = "../images/category_icon_{}.png".format(category.lower())
            commands.append("var category = App.Category.create({{name: '{}', icon: '{}'}});".format(Locale.getEscaped(category), icon))
            for subcategory, items in subcategories.iteritems():
                commands.append("var subcategory = App.Subcategory.create({{name: '{}'}});".format(Locale.getEscaped(subcategory)))
                for item in items:
                    commands.append("subcategory.items.pushObject(App.items[{}]);".format(item))
                commands.append("category.subcategories.pushObject(subcategory);")
            commands.append("App.categories.pushObject(category);")
        if categories:
            commands.append("App.categories.content[0].isSelected = true;")

        #Update item quantities using all available inventories
        for inventory in self.inventories:
            self.updateInventory(commands, inventory, '+')

        #Populate the available services
        for service in self.createdServices:
            commands.append("App.services.findProperty('key', '{}').set('isAvailable', true);".format(service))
        commands.append("App.categoryView.set('selected', App.categories.content[0]);")
        self.view.execute(commands)

    def handleItemChange(self, previous, current):
        def addItemCommand(commands, item, sign):
            if item.content:
                line = "if ({0} in App.items) {{ var item = App.items[{0}];".format(item.content.id)
                line += "item.set('quantity', item.get('quantity') {sign} {quantity}); }}".format(quantity=item.quantity, sign=sign)
                commands.append(line)
        commands = []
        addItemCommand(commands, previous, '-')
        addItemCommand(commands, current, '+')
        self.view.execute(commands)

    def updateAvailableServices(self):
        #Checks to see if a nearby service has changed. If so then update availability.
        services = game.craft.getAvailableServices(World.get().getRealm().layers, self.SERVICE_LAYERS, self.player.entity)
        hasServiceChange = False
        if len(services) is not len(self.services):
            hasServiceChange = True
        else:
            for service in services:
                if service not in self.services:
                    hasServiceChange = True
                    break
        if hasServiceChange:
            self.services = services
            commands = []
            for service in self.createdServices:
                isAvailable = boolStr(service in self.services)
                commands.append("App.services.findProperty('key', '{}').set('isAvailable', {});".format(service, isAvailable))
            self.view.execute(commands)

    def updateInventory(self, commands, inventory, sign):
        for index in xrange(inventory.capacity):
            if inventory.exists(index):
                for item in inventory.getBag(index).items:
                    if item and item.content.id in self.createdItems:
                        commands.append("var item = App.items[{}];".format(item.content.id))
                        commands.append("item.set('quantity', item.get('quantity') {} {});".format(sign, item.quantity))

    def updateAvailableInventories(self):
        #Update available inventories
        commands = []
        inventories = game.craft.getAvailableInventories(World.get().getRealm().layers, self.INVENTORY_LAYERS, self.player.entity)
        for inventory in inventories:
            foundIndex = -1
            for index, existing in enumerate(self.inventories):
                if inventory.getParent().id == existing.getParent().id:
                    foundIndex = index
                    break
            if foundIndex != -1:
                del self.inventories[foundIndex]
            else:
                inventory.onChange.listen(self.handleInventoryChange)
                self.updateInventory(commands, inventory, '+')
        #Remove any remaining inventories that are no longer available
        for inventory in self.inventories:
            inventory.onChange.remove(self.handleInventoryChange)
            self.updateInventory(commands, inventory, '-')
        self.inventories = inventories
        self.view.execute(commands)

    def handleInventoryChange(self, component, bagIndex, index, previous, current):
        self.handleItemChange(previous, current)

    def handleGrabbedChange(self, player, previous, current):
        self.handleItemChange(previous.item, current.item)

    def handleReserveChange(self, ignored, index, previous, item):
        self.handleItemChange(previous, item)
        commands = []
        commands.append("var selected = App.materialsView.get('selected');")
        commands.append("if (selected == null || {} >= selected.materials.content.length) {{ return; }}".format(index))
        commands.append("var item = selected.materials.content[{}];".format(index))
        commands.append("item.set('held', {});".format(item.quantity))
        if item:
            commands.append("item.set('id', {});".format(item.entity.id))
        else:
            commands.append("item.set('id', 0);")
        self.view.execute(commands)

    def handleRecipeChange(self, recipe, material):
        self.view.execute("App.items[{}].learnMaterial({});".format(recipe, material))

    def handleCreatePlacement(self, entity):
        if self.player.entity.getPosition().getDistance(entity.getPosition(), entity.loopWidth) <= self.player.entity.stats.reach.get():
            if entity.has('surface'):
                self.updateAvailableServices()
            if entity.has('inventory'):
                self.updateAvailableInventories()

    def handleDestroyPlacement(self, entity, dropped):
        #Treat this the same as when an entity is created (it just updates the available services and inventories)
        self.handleCreatePlacement(entity)

    def handlePlayerMove(self, component, previous, current):
        self.movementDelta.x += current.x - previous.x
        self.movementDelta.y += current.y - previous.y
        if abs(self.movementDelta.x) > self.DELTA_THRESHOLD or abs(self.movementDelta.y) > self.DELTA_THRESHOLD:
            self.movementDelta.x = 0
            self.movementDelta.y = 0
            self.updateAvailableInventories()
            self.updateAvailableServices()

    def handleRealmChange(self, player, previousRealm, realm):
        if previousRealm:
            realm.placement.onCreate.remove(self.handleCreatePlacement)
            realm.placement.onDestroy.remove(self.handleDestroyPlacement)
        realm.placement.onCreate.listen(self.handleCreatePlacement)
        realm.placement.onDestroy.listen(self.handleDestroyPlacement)
        self.updateAvailableInventories()
        self.updateAvailableServices()

    def handleSelectItem(self, view, args):
        if self.currentUniqueCount:
            ClientAssist.requestReserveRestoration('craft')
        contentId = args[1].toInteger()
        uniqueCount = 0
        if contentId != 0:
            self.selected = game.content.get(contentId).entity
            for material in self.selected.craft.materials:
                if material.content.entity.item.isUnique:
                    uniqueCount += 1
            if uniqueCount:
                ClientAssist.requestReserveCreation('craft', uniqueCount)
        self.currentUniqueCount = uniqueCount

    def handleClickedItem(self, view, args):
        def isValidInteraction(player, item, index):
            if player.grabbed:
                return self.selected.craft.materials[index].content == player.grabbed.item.content
            return item
        self.handleItemInteraction(args[1].toInteger(), isValidInteraction, Message.ITEM_SWAP_WITH_GRAB)

    def handleSplitItem(self, view, args):
        isValidInteraction = lambda player, item, index: item and (not player.grabbed or item.content == player.grabbed.item.content)
        self.handleItemInteraction(args[1].toInteger(), isValidInteraction, Message.ITEM_SPLIT)

    def handleItemInteraction(self, index, isValidInteraction, message):
        reserves = self.player.entity.reserves
        if 'craft' not in reserves:
            Log.warn('Craft reserve does not exist.')
            return
        item = reserves['craft'].get(index)
        if isValidInteraction(self.player, item, index):
            game.audio.play("mods/core/audio/sfx/ui/grab1.ogg")
            packet = DataStream(message)
            packet.writeUint8(GrabbedItemLocation.RESERVE)
            packet.writeUint32(self.player.entity.id)
            packet.writeString('craft')
            packet.writeUint8(index)
            game.network.getClient().send(packet)

    def onCraft(self, view, args):
        #Called from the UI
        #Request the server to perform the crafting
        contentId = args[1].toInteger()
        quantity = args[2].toInteger()
        request = DataStream(Message.ITEM_CRAFT)
        request.writeUint32(contentId)
        request.writeUint32(quantity)
        game.network.getClient().send(request)
