from functools import partial
import math

from core.combat import handlePlayerDeath
from core.helper import sendMessage
from core.system.travel import TravelCrystal
from core.ui.chat import ChatUi
from core.ui.craft import CraftUi
from core.ui.equipment import EquipmentUi
from core.ui.info import InfoUi
from core.ui.inventory import InventoryUi, TreasureUi
from core.ui.option import OptionUi
from core.ui.research import ResearchUi
from core.ui.talent import TalentUi
from core.ui.toolbar import ToolbarUi
from core.ui.tooltip import TooltipUi
from core.ui.travel import TravelUi

import siege
from siege import game
from siege.graphic import Color, DrawableText
from siege.io import Keyboard
from siege.log import Log
from siege.network import Message, MessageHandler, NetworkManager
from siege.sfml import sfEvent
from siege.util import seconds, Sizes, Timer, Vector
from siege.world import World
from siege.world.realm import Layer, LavaCell


class GameplayUi(object):
    #Responsible for handling gameplay UI
    _instance = None

    @staticmethod
    def get():
        return GameplayUi._instance

    def __init__(self):
        pass

    def initialize(self, world, player):
        GameplayUi._instance = self
        self.player = player

        self.tooltip = TooltipUi(player)
        self.craft = CraftUi(player, self.tooltip)
        self.equipment = EquipmentUi(player, self.tooltip)
        self.talent = TalentUi(player, self.tooltip)
        self.info = InfoUi(player)
        self.chat = ChatUi(player)
        self.option = OptionUi()
        self.travel = TravelUi(player)
        self.research = ResearchUi(player, self.tooltip)
        self.toolbar = ToolbarUi(player, self.tooltip)
        self.inventory = InventoryUi(player, self.tooltip)
        game.research.updateMaterials()
        # self.reporter = None
        #Track other inventories that are opened (not the players)
        chest = game.content.get('small_silver_chest').getEntity()
        self.treasure = TreasureUi(self.player, chest, 0, self.tooltip)
        game.events['world_saved'].listen(self.handleWorldSaved)

        # Load dialog positions
        self.dialogs = [
            self.craft,
            self.equipment,
            self.talent,
            self.info,
            self.chat,
            self.option,
            self.travel,
            self.research,
            self.toolbar,
            self.inventory,
            self.treasure
        ]
        settings = game.settings
        for dialog in self.dialogs:
            if dialog is self.toolbar:
                continue
            position = settings.getDialogPosition(dialog.view.name)
            if position is not None:
                dialog.view.setPosition(position)
        windowSize = game.render.getWindowSize()
        self.onResize(windowSize.x, windowSize.y)

        self.alphaTimer = Timer()

        #Register network callbacks
        client = game.network.getClient()
        client.register(Message.INVENTORY_INTERACT, MessageHandler.create(self.handleInventoryInteraction))
        client.register(Message.RESEARCH_INTERACT, MessageHandler.create(self.handleResearchInteraction))
        client.register(Message.NPC_INTERACT, MessageHandler.create(self.handleNpcInteraction))
        client.register(Message.TRAVEL_SHOW_UI, MessageHandler.create(self.handleShowTravelUi))
        client.register(Message.TRAVEL, MessageHandler.create(self.handleTravel))

        if not NetworkManager.isHost():
            self.player.entity.combat.isAlive.onChange.listen(self.handleClientDeath)

        World.get().time.minutes.onChange.listen(self.minuteChanged)
        self.minuteChanged(0, (World.get().time.minutes.get() / 15) * 15)
        game.render.onResize.listen(self.onResize)
        #Hightlight interactable stuff
        self.helpText = DrawableText('', "mods/core/ui/fonts/Bitter-Regular.ttf", 14)
        self.highlightShader = game.render.getShader("mods/core/shader/highlight.frag")
        self.flashTime = 0

        # Show Tutorial
        if settings.showTutorial:
            self.option.onTutorialClick(None, None)
            self.option.show()
            settings.showTutorial = False

    def update(self, frameTime):
        self.info.update(frameTime)
        #Check to see if any of the inventory owners are too far or do not exist anymore
        if self.treasure:
            position = self.player.entity.getPosition()
            loopWidth = World.get().getRealm().size.width
            if self.treasure.isDestroyed or position.getDistance(self.treasure.owner.getPosition(), loopWidth) > 300:
                self.treasure.destroy()
        if self.research.isVisible():
            self.research.update(frameTime)
        if self.travel.isVisible():
            self.travel.update(frameTime)

        #Update tooltip (position)
        self.toolbar.update(frameTime)
        self.tooltip.update(frameTime)
        self.chat.update(frameTime)

        self.alphaTimer.update(frameTime)
        if not self.alphaTimer.expired():
            self.player.entity.render.alpha = int((1 - abs(math.sin(math.pi * self.alphaTimer.remaining() / 200))) * 128 + 128)
        #Update background colors
        world = World.get()
        realm = world.getRealm()
        backgroundColor = world.time.backgroundColor
        realm.layers[Layer.BG1].setColor(backgroundColor)
        realm.layers[Layer.BG2].setColor(backgroundColor)
        realm.layers[Layer.BG3].setColor(backgroundColor)
        # Highlight moused over interactable item (if any)
        world = World.get()
        mousePosition = world.getMousePosition()
        entity = world.getRealm().getInteracted(self.player, mousePosition)
        if entity:
            mousePosition.x += 18
            mousePosition.x = abs(mousePosition.x)
            mousePosition.y = abs(mousePosition.y)
            self.helpText.setPosition(mousePosition)
            self.helpText.setString("Interact")
            self.flashTime = (self.flashTime + frameTime) % seconds(1)
            brightness = 1.25 + 0.25 * math.cos((float(self.flashTime) / seconds(1)) * 2 * math.pi)
            self.highlightShader.setParameter("brightness", brightness)
            entity.render.states.shader = self.highlightShader
        else:
            self.flashTime = 0
        self.helpText.isVisible = bool(entity)

    def toggleResearch(self):
        if self.research.isVisible():
            self.research.hide()
        else:
            station = game.research.findNearbyResearchStation(self.player)
            if station:
                self.research.showWhileNearby(station)

    def handleClientDeath(self, entity, previous, current):
        if not current and entity.animation.has("death"):
            entity.animation.play("death", forceRestart=True)
            entity.physics.setVelocity(Vector(0, 0))
            world = World.get()
            world.requestRegion(world.realm, world.playerData[self.player.uid].homePoint, partial(handlePlayerDeath, entity.animation, entity.physics, 0))
            entity.physics.onTimeStep.listen(partial(handlePlayerDeath, entity.animation))

    def handleEvent(self, event):
        if event.type == sfEvent.EventType.KeyPressed:
            controls = game.settings.controls
            if (event.key.code >= Keyboard.Num1 and event.key.code <= Keyboard.Num9) or event.key.code == Keyboard.Num0:
                # Set selected toolbar to keypress
                value = 9 if event.key.code == Keyboard.Num0 else int(event.key.code) - int(Keyboard.Num1)
                self.player.entity.toolbar.selected = value
            elif event.key.code in controls['craft']:
                # Toggle visibilty of craft dialog
                self.craft.toggleVisibility()
            elif event.key.code in controls['equipment']:
                # Toggle visibilty of equipment dialog
                self.equipment.toggleVisibility()
            elif event.key.code in controls['inventory']:
                self.inventory.toggleVisibility()
            elif event.key.code == Keyboard.F2:
                self.info.toggleVisibility()
            elif not siege.isRelease and event.key.code == Keyboard.F3:
                game.debugRender.enabled = not game.debugRender.enabled
            elif not siege.isRelease and event.key.code == Keyboard.F4:
                self.player.entity.realm.lighting.toggleLighting()
            elif event.key.code in controls['research']:
                if game.research.isResearchAvailable(self.player):
                    self.toggleResearch()
                else:
                    sendMessage("general", "System", "ResearchUnavailble", self.player.networkId)
            elif event.key.code in controls['talent']:
                self.talent.toggleVisibility()
            elif event.key.code in controls['options']:
                # Always unfocus toolbar and chat to ensure other dialogs get unfocused and hidden before options is shown
                game.gui.unfocusView(self.toolbar.view)
                game.gui.unfocusView(self.chat.view)
                # Either hide the currently focused dialog or show the options dialog
                focused = game.gui.getFocused()
                if focused.name != "toolbar" and focused.name != "chat":
                    game.gui.unfocusView(focused)
                    focused.hide()
                else:
                    self.option.show()
            elif event.key.code == Keyboard.H or (event.key.code == Keyboard.Return and not self.chat.view.isVisible()):
                self.chat.toggleVisibility()
            elif not siege.isRelease and event.key.code == Keyboard.O:
                mousePos = World.get().getMousePosition()
                x = int(mousePos.x / Sizes.SUBTILE)
                y = int(mousePos.y / Sizes.SUBTILE)
                World.get().getRealm().automata.setCell(x, y, LavaCell(8))
            elif event.key.code == Keyboard.LShift or event.key.code == Keyboard.RShift:
                self.player.flipPlacedItems = not self.player.flipPlacedItems
        game.input.handleEvent(event)

    def handleInventoryInteraction(self, client, packet):
        ownerId = packet.readUint32()
        bagIndex = packet.readUint32()
        isOpening = packet.readBool()
        owner = game.entity.get(ownerId)
        #Validate packet details
        if not owner or not owner.has('inventory') or not owner.inventory.exists(bagIndex):
            #Something from the packet doesn't check out - bail
            Log.warn("Encountered problem with inventory interaction.")
            return
        #Check to see if this inventory already exists
        if self.treasure.owner is None or self.treasure.owner.id != ownerId or self.treasure.bag.index != bagIndex:
            self.treasure.handleHide()
            self.treasure.repopulate(owner, bagIndex)
        #If this inventory was already open then it will hide it
        #When the player goes out of range it will automatically be cleaned up
        if isOpening:
            self.treasure.sendHideInteraction = True
            self.treasure.show()
        else:
            #Don't send the hide interaction so it doesn't loop back
            self.treasure.sendHideInteraction = False
            self.treasure.hide()

    def handleResearchInteraction(self, client, packet):
        self.research.showWhileNearby(self.player.entity)

    def handleNpcInteraction(self, client, packet):
        entity = game.entity.get(packet.readUint32())
        if not entity or not entity.has('npc'):
            Log.warn("Invalid NPC referenced from server. Ignoring it.")
            return
        entity.npc.core.showDialog(self, self.player, packet)

    def minuteChanged(self, previous, current):
        if current % 15 == 0:
            hour = World.get().time.hours.get()
            nextHour = 0 if hour == 23 else hour + 1
            currentValue = [100, 100, 100, 80, 60, 40, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 40, 60, 80, 100, 100, 100][hour]
            nextValue = [100, 100, 100, 80, 60, 40, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 40, 60, 80, 100, 100, 100][nextHour]
            value = int(currentValue + (nextValue - currentValue) * (float(current) / World.get().time.MINUTES_IN_HOUR))
            World.get().getRealm().lighting.setAmbient(value, Color(255, 255, 255))

    def handleShowTravelUi(self, client, packet):
        current = TravelCrystal()
        attuned = packet.readUint32()
        current.unpack(packet)
        amount = packet.readUint32()
        crystals = []
        for i in range(amount):
            crystal = TravelCrystal()
            crystal.unpack(packet)
            crystals.append(crystal)
        self.travel.updateCrystals(attuned, current, crystals)
        self.travel.showWhileNearby(game.entity.get(current.entityId))

    def handleTravel(self, client, packet):
        game.audio.play("mods/core/audio/sfx/angelic_chime.ogg")

    def handleWorldSaved(self, world, asynchronous):
        # Save all dialog positions to settings
        settings = game.settings
        for dialog in self.dialogs:
            settings.setDialogPosition(dialog.view.name, dialog.view.getPosition())
        settings.save()

    def render(self, target):
        if self.helpText.isVisible:
            self.helpText.renderOutlined(target, Color(49, 49, 49, 255))

    def onResize(self, width, height):
        for dialog in self.dialogs:
            position = dialog.view.getPosition()
            if position.x > width - 100 or position.y > height - 100:
                dialog.view.setPosition(dialog.defaultPosition)
