from functools import partial
import math

from core.combat import AttackData, AttackType, applyDamage, calculateDamage, damageEnemies, getDefense, getHitEnemies
from core.system.input import InputTypes

from siege import game, InventoryItem
from siege.component import CombatTeam, ToolbarSlot
from siege.graphic import Color, PixelCheck, TextureCache
from siege.io import DataStream
from siege.log import Log
from siege.network import Message, NetworkManager
from siege.particle import ParticleEmitter, PhysicsParticleEmitter
from siege.sfml import Transform
from siege.util import Random, RangeColor, RangeInt, RangeVector, Rect, seconds, Sizes, Vector
from siege.world import getNearbyEnemies, World, toRadians
from siege.world.realm import Layer


def useStatusPotion(status, level, duration, args):
    entity = args["player"].entity
    entity.effects.add(status, level, duration)
    args["decrement"] = 1


def usePotion(stat, amount, args):
    entity = args["player"].entity
    amount = entity.stats.get(stat).adjust(amount)
    if amount:
        #Only use potion if it had an affect
        game.combat.showCombatNumber(entity, amount, False)
        args["decrement"] = 1
    else:
        args["cancel"] = True


def destroyHitFragileTiles(entity):
    realm = entity.realm
    ground = realm.layers[Layer.Ground]
    area = entity.render.getRect()
    tiles = ground.overlaps(area, False, False)
    for tile in tiles:
        if ground.getTile(tile) == 0:
            continue
        component = ground.getTileComponent(tile)
        if component.isFragile and realm.layers.canChangeTile(Layer.Ground, tile, 0):
            # Use either the attack sprites or the RenderComponent
            sprites = entity.render.getSprites('attack') if entity.has('modularRender') else entity.render
            tileArea = Rect(tile.getWorldPosition(), Vector(Sizes.TILE, Sizes.TILE))
            if not PixelCheck.contains(sprites, tileArea, entity.loopWidth):
                continue
            game.events["hit_tile"].invoke(entity, realm, component, Layer.Ground, tile, True)
            realm.layers.setTile(Layer.Ground, tile, 0)


def updateThrustSprites(player, itemEntity, flipX, angle, physics, frameTime):
    render = player.entity.render
    render.flipX(flipX)
    flipped = -1 if flipX else 1
    armPivot = player.entity.physics.getCenter() - Vector(4 * flipped, 6)
    transform = Transform()
    transform.rotate(angle, armPivot.toSfml())
    for name in ["front_arm", "item"]:
        for sprite in render.getSprites(name):
            sprite.modular.states.transform = transform
    return not player.entity.animation.isPlaying(itemEntity.item.useAnimation, "arm_front")


def useDaggerWeapon(player, itemEntity, position, delay, index, slot):
    useMeleeWeapon(player, itemEntity, position, delay, index, slot)
    center = player.entity.physics.getCenter()
    loopWidth = player.entity.loopWidth
    direction = center.getDirectionX(position.x, loopWidth)
    flipX = direction == -1
    armPivot = center - Vector(4 * direction, 6)
    angle = armPivot.getAngle(position, loopWidth)
    if flipX:
        angle += 180
    player.entity.physics.onTimeStep.listen(partial(updateThrustSprites, player, itemEntity, flipX, angle))


def createArrow(player, power, contentEntity, position, angle, moveSpeed=Vector(10, 12)):
    arrow = game.entity.create(contentEntity)
    arrow.projectile.attackData.power += power
    arrow.render.setOriginToCenter()
    arrow.setInitialPosition(position)
    arrow.projectile.setOwner(player.entity)
    arrow.projectile.lifetime.reset(seconds(10))
    radians = toRadians(angle)
    speed = Vector(math.cos(radians) * moveSpeed.x, math.sin(radians) * moveSpeed.y)
    arrow.physics.setVelocity(speed)
    arrow.physics.onTimeStep.listen(arrow.projectile.rotateToDirection)
    if NetworkManager.isHost():
        arrow.physics.onTimeStep.listen(arrow.projectile.disappearOnHit)
    World.get().move(arrow, player.entity.realm, Layer.Active)
    return arrow


def transformBow(player, render, center, position):
    loopWidth = player.entity.loopWidth
    direction = center.getDirectionX(position.x, loopWidth)
    flipX = direction == -1
    armPivot = center - Vector(4 * direction, 6)
    angle = armPivot.getAngle(position, loopWidth)
    if flipX:
        angle += 180
    render.flipX(flipX)
    armPivot = center - Vector(4 * direction, 6)
    transform = Transform()
    transform.rotate(angle, armPivot.toSfml())
    for name in ["front_arm", "back_arm", "item", "arrow"]:
        for sprite in render.getSprites(name):
            sprite.modular.states.transform = transform
    return flipX, angle


def updateBowSprites(player, itemEntity, ammo, onFinish, mouseButton, physics, frameTime):
    render = player.entity.render
    center = player.entity.physics.getCenter()
    clientInput = game.input.getClientInput(player)
    isMouseReleased = not clientInput.actions[mouseButton].isPressed
    position = clientInput.mousePosition
    flipX, angle = transformBow(player, render, center, position)
    animation = player.entity.animation
    isFinished = animation.isPlaying("bow_hold_front", "arm_front")
    if isMouseReleased:
        animation.clearQueue("arm_front")
        animation.clearQueue("arm_back")
        animation.pause("arm_front")
        animation.pause("arm_back")
        arrow = None
        if isFinished:
            if NetworkManager.isHost():
                if flipX:
                    angle -= 180
                arrow = createArrow(player, itemEntity.weapon.power, ammo.entity.ammo.projectileContent, center, angle)
                if not ammo.entity.item.isUnique:
                    player.entity.gear.consume("ammo")
        else:
            player.useTimer.reset()
        if onFinish:
            onFinish(player, itemEntity, ammo, arrow, angle)
        return True
    elif isFinished:
        player.useTimer += frameTime
    return False


def useBowWeapon(onFinish=None):
    def useBowWeaponInner(player, itemEntity, position, delay, index, slot):
        ammo = player.entity.gear.ammo.item
        if ammo and ammo.entity.has("ammo"):
            #TODO: Need an additional check if this ammo is compatible with the bow
            animation = player.entity.animation
            animation.play("bow_front", "arm_front")
            animation.play("bow_back", "arm_back")
            animation.scale(delay, "arm_front")
            animation.scale(delay, "arm_back")
            animation.queue("bow_hold_front", "arm_front")
            animation.queue("bow_hold_back", "arm_back")
            mouseButton = InputTypes.MouseL if slot == ToolbarSlot.LEFT else InputTypes.MouseR
            player.entity.physics.onTimeStep.listen(partial(updateBowSprites, player, itemEntity, ammo, onFinish, mouseButton))
        elif onFinish:
            onFinish(player, itemEntity, ammo, None, 0)
    return useBowWeaponInner


def checkMeleeCollision(player, itemEntity, attackId, playerPhysics, frameTime):
    #print "Checking Melee Collision with {}".format(itemEntity.name)
    if not player.entity.animation.isPlaying(itemEntity.item.useAnimation, "arm_front"):
        player.entity.combat.finishAttack(attackId)
        return True
    enemies = getHitEnemies(player.entity)
    if enemies:
        weapon = itemEntity.weapon
        attackData = AttackData(weapon.power, weapon.attackType, weapon.damageType)
        if NetworkManager.isHost():
            damageEnemies(attackId, player.entity, enemies, attackData)
    destroyHitFragileTiles(player.entity)


def useMeleeWeapon(player, itemEntity, position, delay, index, slot):
    attackId = player.entity.combat.startAttack()
    player.entity.physics.onTimeStep.listen(partial(checkMeleeCollision, player, itemEntity, attackId))


def explode(realm, explosive, radius):
    def createSmoke(position):
        #Create explosion particle effects
        emitter = ParticleEmitter(realm.uid, realm.size.width, position, 1, 0)
        emitter.amount = Random.get(1, 2)
        emitter.particlePosition = RangeVector(Vector(-Sizes.TILE / 2, -Sizes.TILE), Vector(-Sizes.TILE / 2, 0))
        emitter.setTexture(TextureCache.get("weapon/throwing/smoke.png"))
        emitter.velocityStart = RangeVector(Vector(-0.1, -0.5), Vector(0.1, -0.2))
        emitter.colorStart = RangeColor(Color(255, 255, 255), Color(255, 255, 255))
        emitter.colorEnd = RangeColor(Color(255, 255, 255, 0), Color(255, 255, 255, 0))
        emitter.scaleChange = RangeVector(Vector(-1, -1), Vector(-0.3, -0.3))
        emitter.particleLife = RangeInt(seconds(2), seconds(3))
        game.particles.add(emitter)
        return emitter

    def createShrapnel(position, color, amount):
        emitter = PhysicsParticleEmitter(realm.uid, realm.size.width, position, 1, 0)
        emitter.amount = amount
        emitter.velocityStart = RangeVector(Vector(-4, -4.0), Vector(4, -5.0))
        emitter.colorStart = RangeColor(color, color)
        color.a = 0
        emitter.colorEnd = RangeColor(color, color)
        emitter.particleLife = RangeInt(seconds(2), seconds(3))
        emitter.scaleStart = RangeVector(Vector(2, 2), Vector(2, 2))
        emitter.gravity = Vector(0.0, 0.35)
        emitter.friction = Vector(0.1, 0.0)
        emitter.restitution = Vector(0.2, 0.4)
        game.particles.add(emitter)

    tiles = ["soil", "stone"]

    ground = realm.layers[Layer.Ground]
    center = explosive.physics.getCenter()
    area = Rect(center.x - radius, center.y - radius, radius * 2, radius * 2)
    overlapTiles = ground.overlaps(area, False, False)
    for tile in overlapTiles:
        tileCenter = tile.getWorldCenter()
        if ground.getTile(tile) == 0 or radius < center.getDistance(tileCenter, realm.size.width):
            continue
        component = ground.getTileComponent(tile)
        tileDrop = component.getDropped()
        if not tileDrop or not component.isCompatible(tiles):
            continue
        realm.layers.setTile(Layer.Ground, tile, 0, True)
        #TODO: Set velocity based on angle to blast center
        realm.dropped.create(InventoryItem(game.content.get(tileDrop), 1), tileCenter, Vector(Random.get(-2.0, 2.0), -6))
        createSmoke(tileCenter)

    enemies = getNearbyEnemies(CombatTeam.NONE, realm.layers, area, onlyAlive=True)
    projectile = explosive.projectile
    for enemy in enemies:
        if enemy.combat.isInvincible.get():
            continue
        enemyCenter = enemy.physics.getCenter() if enemy.has('physics') else enemy.render.getCenter()
        distance = center.getDistance(enemyCenter, realm.size.width)
        if (radius + radius / 5) >= distance:
            #TODO: Move these numbers into projectile content file
            strength = 55
            defense = getDefense(enemy, AttackType.Magic)
            level = 10
            power = projectile.attackData.power
            damage = calculateDamage(strength, defense, level, power)
            applyDamage(enemy, damage, center, projectile.attackData.knockback, "DeathExplosion")
    createShrapnel(center, Color(235, 105, 107), Random.get(40, 50))
    createShrapnel(center, Color(108, 108, 108), Random.get(40, 50))
    smokeEmitter = createSmoke(center)
    smokeEmitter.amount = Random.get(3, 4)
    game.audio.playAt("mods/core/audio/sfx/weapons/bombblast2.ogg", center, broadcast=True)
    explosive.destroy()


def rotateWithVelocity(physics, timestep):
    velocity = physics.getVelocity()
    render = physics.getParent().render
    rotation = (velocity.x / 5.0) * 10 + render.getRotation()
    render.setRotation(rotation)


def throwItem(contentPath, lifetime, args, isSticky=False):
    player = args['player']
    center = player.entity.physics.getCenter()
    speed = 7
    args['decrement'] = 1
    content = game.content.get(contentPath)
    entity = launchProjectile(player, content, center, args['position'], speed)
    game.timer.add(lifetime, entity.destroy, isPrecise=True)
    if isSticky:
        entity.physics.onTimeStep.listen(entity.projectile.attachOnHit)


def useExplosive(contentPath, fuseTime, radius, args, isSticky=False):
    player = args['player']
    center = player.entity.physics.getCenter()
    speed = 7
    args['decrement'] = 1
    content = game.content.get(contentPath)
    entity = spawnExplosive(player, content, center, args['position'], speed, radius, fuseTime)
    if isSticky:
        entity.physics.onTimeStep.listen(entity.projectile.attachOnHit)


def spawnExplosive(player, content, center, target, speed, radius, fuseTime):
    entity = launchProjectile(player, content, center, target, speed)
    game.timer.add(fuseTime, partial(explode, player.entity.realm, entity, radius), isPrecise=True)
    return entity


def launchProjectile(player, content, center, target, speed, rotate=True):
    realm = player.entity.realm
    direction = center.getDirectionX(target.x, realm.size.width)
    armPivot = center - Vector(4 * direction, 6)
    entity = game.entity.create(content)
    entity.render.setOriginToCenter()
    entity.setInitialPosition(armPivot)
    entity.physics.getWorldBody()
    if entity.has('projectile'):
        entity.projectile.setOwner(player.entity)
    angle = armPivot.getAngle(target, realm.size.width)
    radians = toRadians(angle)
    speed = Vector(math.cos(radians) * speed, math.sin(radians) * speed - 2)
    entity.physics.setVelocity(speed)
    if rotate:
        entity.physics.onTimeStep.listen(rotateWithVelocity)
    World.get().move(entity, realm, Layer.Active)
    return entity


def interactInventory(player, entity, position):
    if not NetworkManager.isHost():
        return
    #Toggle if this player is using this chest
    users = entity.data.data['users']
    isOpening = player.entity.id not in users
    if isOpening:
        #Player was not already using the chest - add to user list
        users.append(player.entity.id)
        if not entity.animation.isPlaying('open'):
            entity.animation.play('open')
    else:
        #Player already has this chest open so close it and remove player from users
        users.remove(player.entity.id)
        #If no more users then close the chest
        if len(users) == 0:
            entity.animation.play('closed')
    #Send packet to player
    packet = DataStream(Message.INVENTORY_INTERACT)
    packet.writeUint32(entity.id)
    #Only allow the use of the first bag in the chest's inventory
    packet.writeUint32(0)
    packet.writeBool(isOpening)
    game.network.getServer().send(player.networkId, packet)


def upgradeEquipment(player, craft, materials, count, result, quality, item):
    if not item.entity.has('equipment'):
        Log.warn("Crafted item ({}) unexpectedly does not have an equipment component.".format(item.entity.name))
        return

    #Take the previous equipment quality and average it with the new
    qualities = [quality]
    for material in materials:
        if material.entity.item.isUnique and material.entity.has('equipment'):
            qualities.append(material.entity.equipment.quality)
    quality = sum(qualities) / len(qualities)

    #Recreate item with the averaged quality so attributes reflect it properly
    item.entity = game.equipment.create(item.entity.content, min(max(0, quality), 255))

    #Go through the materials and adds any unique items with an equipment attribute to the new entity's equipment
    equipment = item.entity.equipment
    for material in materials:
        if material.entity.item.isUnique and material.entity.has('equipment'):
            for attribute in material.entity.equipment.getAttributes():
                equipment.addAttribute(attribute, ignoreMax=True)
