from core.helper import enum, getRandomPath

from siege import game
from siege.component import Direction
from siege.io import GameInput, Input, KeyInput, Mouse, MouseInput
from siege.network import Message, MessageHandler, NetworkManager, NetworkId
from siege.sfml import sfEvent
from siege.util import seconds, Sizes, Timer, Vector
from siege.world import World
from siege.world.realm import Layer

InputTypes = enum("Up", "Down", "Left", "Right", "Jump", "MouseL", "MouseR")
INPUT_TYPE_COUNT = 7


class InputState(object):
    def __init__(self):
        #Inputs contains a list of bools for InputTypes isPressed state
        #We can get away with using a list since this is only over the network
        self.inputs = []
        self.position = Vector()

    def read(self, stream):
        self.inputs = []
        for x in xrange(INPUT_TYPE_COUNT):
            elapsed = stream.readUint32()
            isPressed = stream.readBool()
            self.inputs.append((elapsed, isPressed))
        self.position = stream.readVector()

    def write(self, stream):
        for elapsed, isPressed in self.inputs:
            stream.writeUint32(elapsed)
            stream.writeBool(isPressed)
        stream.writeVector(self.position)

    def copy(self):
        state = InputState()
        state.inputs = [(elapsed, isPressed) for elapsed, isPressed in self.inputs]
        state.position = Vector(self.position)
        return state


class ClientInput(object):
    def __init__(self, client, entity):
        self.uid = client
        self.inputs = Input()
        self.actions = []

        for x in xrange(INPUT_TYPE_COUNT):
            action = self.inputs.createAction()
            action.inputs.append(GameInput())
            self.actions.append(action)
        self.mousePosition = Vector()
        #Components for ease of access
        self.animation = entity.animation
        self.physics = entity.physics
        self.state = entity.playerState
        self.stats = entity.stats
        self.render = entity.render
        self.effects = entity.effects
        self.combat = entity.combat if entity.has("combat") else None
        self.runTime = 0
        self.airTime = 0
        self.jumpTime = InputSystem.JUMP_TIME
        self.dashing = Timer()
        self.walkSoundTime = 0
        self.direction = 0
        self.attackDirection = 0
        self.dashDirection = 0
        self.isTouchingGround = True

    @property
    def entity(self):
        return self.render.getParent()

    def playFootstepSound(self, frameTime):
        self.walkSoundTime += frameTime
        if self.walkSoundTime > 400:
            # Determine the correct sound based off of the tile the player is running on
            position = self.entity.getPosition()
            position.y += Sizes.SUBTILE
            ground = self.entity.realm.layers[Layer.Ground]
            tilePosition = ground.getTilePosition(position)
            if not ground.isEmpty(tilePosition):
                tile = ground.getTileComponent(tilePosition)
                groups = tile.getGroups()
                soundType = 'soil[1-5]'
                if 'grass' in groups:
                    soundType = 'grass[1-4]'
                elif 'wood' in groups:
                    soundType = 'wood[1-4]'
                # elif 'stone' in groups:
                #     soundType = 'stone[1-4]'
                game.audio.playAt(getRandomPath("mods/core/audio/sfx/footsteps/{}.ogg".format(soundType)), self.physics.getCenter())
            self.walkSoundTime = 0


class InputSystem(object):
    NAME = "input"
    RUN_SPEED = 2.0
    AIR_SPEED = 3.0
    DASH_SPEED = 6
    DASH_TIME = 300
    DASH_FATIGUE_DURATION = seconds(3)
    JUMP_TIME = 270
    JUMP_SPEED = -4.0
    frontArmPriorities = ["axe", "bow_front", "bow_hold_front", "casting", "hold", "swing", "sword", "thrust"]
    backArmPriorities = ["bow_back", "bow_hold_back"]
    # Currently these are all front_arm animations
    attackAnimations = ["axe", "sword", "thrust"]

    def getName(self):
        return self.NAME

    def __init__(self):
        #Actual hardware inputs
        self.state = None
        self.clients = {}
        self.client = None
        self.realInputs = Input()
        self.realActions = []
        for x in xrange(INPUT_TYPE_COUNT):
            self.realActions.append(self.realInputs.createAction())
        controls = game.settings.controls
        for key in controls['left']:
            self.realActions[InputTypes.Left].inputs.append(KeyInput(key))
        for key in controls['down']:
            self.realActions[InputTypes.Down].inputs.append(KeyInput(key))
        for key in controls['right']:
            self.realActions[InputTypes.Right].inputs.append(KeyInput(key))
        for key in controls['jump']:
            self.realActions[InputTypes.Jump].inputs.append(KeyInput(key))
        self.realActions[InputTypes.MouseL].inputs.append(MouseInput(Mouse.Button.Left))
        self.realActions[InputTypes.MouseR].inputs.append(MouseInput(Mouse.Button.Right))

    def reset(self):
        self.clients = {}
        self.client = None

    def update(self, frameTime):
        pass

    def updateReal(self, frameTime):
        mouseRState = self.state.inputs[InputTypes.MouseR][1] if self.state is not None else False
        self.state = InputState()
        self.state.inputs = [[action.elapsed, False] for action in self.realActions]
        self.state.inputs[InputTypes.MouseR][1] = mouseRState
        self.realInputs.update(frameTime)

    def handleEvent(self, event):
        if event.mouseButton.button == Mouse.Right:
            self.state.inputs[InputTypes.MouseR][1] = event.type == sfEvent.EventType.MouseButtonPressed

    def getState(self):
        if game.hasFocus and game.hasInputFocus:
            for index, action in enumerate(self.realActions):
                if index is not InputTypes.MouseR:
                    self.state.inputs[index][1] = action.isPressed
        self.state.position = World.get().getMousePosition()
        action = self.realActions[InputTypes.Jump]
        return self.state

    def registerClient(self, client, clientId):
        clientInput = ClientInput(client, World.get().getPlayer(client).entity)
        clientInput.actions[InputTypes.Down].onChange.listen(self.handleDownAction)
        clientInput.actions[InputTypes.Jump].onChange.listen(self.handleJumpAction)
        clientInput.actions[InputTypes.Left].onChange.listen(self.handleDirectionAction)
        clientInput.actions[InputTypes.Right].onChange.listen(self.handleDirectionAction)
        self.clients[clientId] = clientInput
        game.events['client_input_ready'].invoke(clientInput)

    def getClientInput(self, player):
        return self.clients[NetworkId.toUint32(player.networkId)]

    def isPlayersInput(self, player, clientInput):
        return NetworkId.toUint32(player.networkId) == NetworkId.toUint32(clientInput.uid)

    def handle(self, state, client, inputState, frameTime):
        #Set these here before the action update so any handlers will have access to the correct components
        clientId = NetworkId.toUint32(client)
        if clientId not in self.clients:
            self.registerClient(client, clientId)
        self.client = self.clients[clientId]
        if not self.client.combat.isAlive.get():
            return

        if inputState:
            #Set the actions to the correct state based off of the inputState
            for inputType, value in enumerate(inputState.inputs):
                action = self.client.actions[inputType]
                action.elapsed = value[0]
                action.inputs[0].isPressed = value[1]
            action = self.client.actions[InputTypes.Jump]
            self.client.inputs.update(0)
        else:
            self.client.inputs.update(frameTime)
        self.client.mousePosition = inputState.position

        velocity = self.client.physics.getVelocity()
        wasTouchingGround = self.client.isTouchingGround
        self.client.isTouchingGround = self.client.physics.isTouching(Direction.BOTTOM)
        canMove = self.canMove()
        #Reset the player's vertical velocity to 0 if on the ground
        entity = self.client.entity
        if entity.realm.physics.isOnSlope(entity):
            velocity.y = 6
        elif (self.client.isTouchingGround or wasTouchingGround) and self.client.physics.jumpTimer.expired():
            velocity.y = 0
        if not self.client.isTouchingGround:
            self.client.physics.wake()

        if self.client.isTouchingGround and not wasTouchingGround and self.client.airTime > 150:
            volume = int(min(((self.client.airTime - 150) / 200.0) * 100, 100))
            game.audio.playAt("mods/core/audio/sfx/footsteps/land.ogg", self.client.physics.getCenter(), volume)
        if self.client.isTouchingGround:
            self.client.airTime = 0
        else:
            self.client.airTime += frameTime

        def isIdle():
            self.client.runTime = 0
            if self.client.isTouchingGround and self.client.physics.jumpTimer.expired():
                self.client.animation.play("idle")
                self.fallbackToAnimation("idle_arm_back", "arm_back", self.backArmPriorities)
                if not self.isPlayingPriority("arm_front", self.frontArmPriorities) and \
                        not self.client.animation.isPlaying("defend", "arm_front") and \
                        not self.client.animation.wasPlaying("defend", "arm_front"):
                    self.client.animation.play("idle_arm_front", "arm_front")
            velocity.x = 0
            self.client.physics.setVelocity(velocity)
        if self.client.state.activeItem.isItem() and self.client.state.activeItem.quantity > 0:
            item = self.client.state.activeItem.entity.item
            if item.hold:
                if self.client.animation.getAnimationName("arm_front").endswith("arm_front"):
                    self.client.animation.play("hold", "arm_front")
            elif self.client.animation.isPlaying("hold", "arm_front"):
                self.client.animation.play("idle_arm_front", "arm_front")
        elif self.client.animation.isPlaying("hold", "arm_front"):
            self.client.animation.play("idle_arm_front", "arm_front")

        #Jumping/Dashing Update Logic
        if not self.client.physics.jumpTimer.expired():
            self.client.physics.jumpTimer.update(frameTime)
            velocity = self.client.physics.getVelocity()
            velocity.y = self.JUMP_SPEED
            self.client.physics.setVelocity(velocity)
        elif not self.client.dashing.expired():
            self.client.dashing.update(frameTime)
            velocity = self.client.physics.getVelocity()
            velocity.x = self.DASH_SPEED * self.client.dashDirection
            self.client.physics.setVelocity(velocity)
            if self.client.dashing.expired():
                self.client.render.setTrail(0, 0)
                gravity = self.client.physics.getGravity()
                gravity.y = 0.45
                self.client.physics.setGravity(gravity)
            return

        #Do not allow for any input while being knockbacked
        if self.client.combat and not self.client.combat.knockback.expired():
            return

        if self.client.actions[InputTypes.Right].isPressed or self.client.actions[InputTypes.Left].isPressed:
            if not self.isAttacking() and canMove:
                self.updateDirection()
            moveSpeed = self.client.stats.MOVE.get()
            if not canMove:
                if self.client.isTouchingGround:
                    isIdle()
                else:
                    velocity.x = (self.AIR_SPEED + (self.AIR_SPEED * moveSpeed / 100.0)) * self.client.direction
            else:
                if self.client.isTouchingGround:
                    #The player is moving on the ground
                    if self.client.physics.jumpTimer.expired():
                        self.client.animation.play("run")
                        self.fallbackToAnimation("run_arm_back", "arm_back", self.backArmPriorities)
                        self.fallbackToAnimation("run_arm_front", "arm_front", self.frontArmPriorities)
                        self.client.playFootstepSound(frameTime)
                    self.client.runTime += frameTime
                    runSpeed = self.RUN_SPEED + (self.RUN_SPEED * moveSpeed / 100.0)
                    velocity.x = (runSpeed + min(min(self.client.runTime, 200) / 200.0, runSpeed)) * self.client.direction
                else:
                    velocity.x = (self.AIR_SPEED + (self.AIR_SPEED * moveSpeed / 100.0)) * self.client.direction
            self.client.physics.setVelocity(velocity)
        else:
            isIdle()
        if not self.client.isTouchingGround and not wasTouchingGround:
            #The player is in the air!
            if velocity.y > 0:
                #The playering is moving downwards
                self.client.animation.play("fall")
                self.fallbackToAnimation("fall_arm_back", "arm_back", self.backArmPriorities)
                self.fallbackToAnimation("fall_arm_front", "arm_front", self.frontArmPriorities)

    def updateDirection(self):
        if self.client.actions[InputTypes.Right].isPressed:
            self.client.direction = 1
            self.client.render.flipX(False)
        elif self.client.actions[InputTypes.Left].isPressed:
            self.client.direction = -1
            self.client.render.flipX(True)

    def handleDirectionAction(self, actionState):
        if actionState.justPressed and actionState.elapsed <= 350 and self.canMove():
            # Double pressed direction key - try to dash
            if not self.client.effects.has("Fatigue"):
                self.client.dashDirection = (-1 if self.client.actions[InputTypes.Left].isPressed else 1)
                self.client.render.setTrail(4, 6)
                self.client.dashing.reset(self.DASH_TIME)
                self.client.physics.setVelocity(Vector(self.DASH_SPEED * self.client.dashDirection, 0))
                gravity = self.client.physics.getGravity()
                gravity.y = 0
                self.client.physics.setGravity(gravity)
                animation = "dashf" if self.client.dashDirection == self.client.direction else "dashb"
                self.client.animation.play(animation)
                self.fallbackToAnimation("jump_arm_back", "arm_back", self.backArmPriorities)
                self.fallbackToAnimation("jump_arm_front", "arm_front", self.frontArmPriorities)
                self.client.effects.add("Fatigue", 1, self.DASH_FATIGUE_DURATION)
                self.client.physics.jumpTimer.reset()

    def handleDownAction(self, actionState):
        if actionState.justPressed and actionState.elapsed <= 350:
            #Pressed double down. Check if we can fall through whatever the player is standing on
            self.client.physics.wake()
            if not self.client.physics.isTouching(Direction.BOTTOM):
                #Not standing on anything!
                return
            realm = self.client.entity.realm
            touching = realm.physics.getTouching(self.client.physics, Direction.BOTTOM)
            for physics in touching:
                if not (physics.getPassthrough() & Direction.TOP):
                    #Couldn't passthrough one of the bodies the character is standing on - Cancel passthrough
                    self.client.physics.clearPassingThrough()
                    break
                self.client.physics.addPassingThrough(physics)

    def handleJumpAction(self, actionState):
        if actionState.justPressed and self.client.dashing.expired() and self.canMove():
            if self.client.physics.isTouching(Direction.BOTTOM) and not self.client.actions[InputTypes.Down].isPressed:
                #Do a normal jump
                self.client.physics.jumpTimer.reset(self.client.jumpTime)
                velocity = self.client.physics.getVelocity()
                velocity.y = self.JUMP_SPEED
                self.client.physics.setVelocity(velocity)
                self.client.animation.play("jump")
                self.fallbackToAnimation("jump_arm_back", "arm_back", self.backArmPriorities)
                self.fallbackToAnimation("jump_arm_front", "arm_front", self.frontArmPriorities)
                game.audio.playAt("mods/core/audio/sfx/footsteps/jump.ogg", self.client.physics.getCenter())
        elif actionState.justReleased:
            self.client.physics.jumpTimer.reset()

    def handlePlayerInput(self, client, stream):
        inputState = InputState()
        state = stream.readUint32()
        inputState.read(stream)
        position = stream.readVector()
        entity = World.get().getPlayer(client).entity
        if entity.combat.isAlive.get():
            entity.setPosition(position)
        game.state.setInputState(client, state, inputState)

    def handlePlayerPosition(self, client, stream):
        entity = World.get().getPlayer().entity
        entity.physics.wake()
        entity.setPosition(stream.readVector())

    def isPlayingPriority(self, group, priorities):
        return self.client.animation.isPlayingGroup(group) and self.client.animation.getAnimationName(group) in priorities

    def fallbackToAnimation(self, animation, group, priorities):
        '''Plays an animation if none of the priority animations are playing.'''
        if not self.isPlayingPriority(group, priorities):
            self.client.animation.play(animation, group)

    def canMove(self):
        return self.client.state.canMove.get() and not self.isAttacking()

    def isAttacking(self):
        return self.client.animation.isPlayingGroup("arm_front") and self.client.animation.getAnimationName("arm_front") in self.attackAnimations

    @staticmethod
    def register(world):
        inputSystem = InputSystem()
        game.registerSystem(InputSystem.NAME, inputSystem)
        if NetworkManager.isHost():
            game.network.getServer().register(Message.PLAYER_INPUT, MessageHandler.create(inputSystem.handlePlayerInput))
        else:
            game.network.getClient().register(Message.PLAYER_POSITION, MessageHandler.create(inputSystem.handlePlayerPosition))

    @staticmethod
    def unregister(world):
        game.unregisterSystem(InputSystem.NAME)
        if NetworkManager.isHost():
            game.network.getServer().unregister(Message.PLAYER_INPUT)
        else:
            game.network.getClient().unregister(Message.PLAYER_POSITION)
