from functools import partial
import math

from core.helper import callEvent, enum, getRandomPath, sendMessage
from core.system.tween import Tween
from core.talent import chanceToGainTalentPoints

from siege import game
from siege.component import Stat, Stats
from siege.graphic import Color, PixelCheck, TextureCache
from siege.log import Log
from siege.io import DataStream
from siege.network import Message
from siege.particle import PhysicsParticleEmitter
from siege.util import RangeColor, RangeInt, RangeFloat, RangeVector, Random, Rect, seconds, Vector
from siege.world import getNearbyEnemies, World
from siege.world.realm import Layer


DamageType = enum("Physical", "Fire", "Earth", "Wind", "Water")
AttackType = enum("Divine", "Melee", "Magic", "Ranged")


class AttackData(object):
    def __init__(self, power, attackType, damageType, knockback=0, interrupts=True):
        self.power = power
        self.attackType = attackType
        self.damageType = damageType
        self.knockback = knockback
        self.interrupts = interrupts


def createCombatStats(**kwargs):
    stats = Stats()
    stats.stats.append(Stat('HP', 'HP', kwargs.get('HP', 100), 9999, True, True))
    stats.stats.append(Stat('SP', 'SP', kwargs.get('SP', 100), 9999, True, True))
    stats.stats.append(Stat('ATK', 'Attack', kwargs.get('ATK', 30), 999, False, True))
    stats.stats.append(Stat('DEF', 'Defense', kwargs.get('DEF', 30), 999, False, True))
    stats.stats.append(Stat('INT', 'Intelligence', kwargs.get('INT', 30), 999, False, True))
    stats.stats.append(Stat('MIND', 'Mind', kwargs.get('MIND', 30), 999, False, True))
    stats.stats.append(Stat('AGI', 'Agility', kwargs.get('AGI', 30), 999, False, True))
    stats.stats.append(Stat('LUCK', 'Luck', kwargs.get('LUCK', 30), 999, False, True))
    stats.stats.append(Stat('ACC', 'Accuracy', kwargs.get('ACC', 0), 999, False, False))
    stats.stats.append(Stat('EVA', 'Evasion', kwargs.get('EVA', 0), 999, False, False))
    stats.stats.append(Stat('SPD', 'Attack Speed', kwargs.get('SPD', 0), 999, False, False))
    stats.stats.append(Stat('MOVE', 'Movement Speed', kwargs.get('MOVE', 0), 999, False, False))
    stats.stats.append(Stat('fire', 'Fire Resistance', kwargs.get('fire', 100), 9999, False, False))
    stats.stats.append(Stat('water', 'Water Resistance', kwargs.get('water', 100), 9999, False, False))
    stats.stats.append(Stat('earth', 'Earth Resistance', kwargs.get('earth', 100), 9999, False, False))
    stats.stats.append(Stat('wind', 'Wind Resistance', kwargs.get('wind', 100), 9999, False, False))
    stats.stats.append(Stat('HPRegen', 'HP Regeneration', kwargs.get('HPRegen', 0), 9999, False, False))
    stats.stats.append(Stat('SPRegen', 'SP Regeneration', kwargs.get('SPRegen', 0), 9999, False, False))
    stats.stats.append(Stat('knockback', 'Knockback Power', kwargs.get('knockback', 0), 999, False, False))
    stats.stats.append(Stat('knockbackRes', 'Knockback Resistance', kwargs.get('knockbackRes', 0), 999, False, False))
    return stats


def checkCollision(attacker, defender):
    if attacker.has('modularRender'):
        if defender.has('modularRender'):
            return PixelCheck.collision(attacker.render.getSprites('attack'), defender.render.getSprites('body'), attacker.loopWidth)
        else:
            return PixelCheck.collision(defender.render, attacker.render.getSprites('attack'), attacker.loopWidth)
    else:
        if defender.has('modularRender'):
            return PixelCheck.collision(attacker.render, defender.render.getSprites('body'), attacker.loopWidth)
        else:
            return PixelCheck.collision(attacker.render, defender.render, attacker.loopWidth)


def getHitEnemies(attacker, team=None):
    if team is None:
        team = attacker.combat.team
    hitEnemies = []
    enemies = getNearbyEnemies(team, attacker.realm.layers, attacker.getRender().getRect(), onlyAlive=True)
    for enemy in enemies:
        combat = enemy.combat
        if not combat.isInvincible.get() and not combat.isHitStunned.get() and checkCollision(attacker, enemy):
            hitEnemies.append(enemy)
    return hitEnemies


def damageEnemies(attackId, attacker, enemies, data):
    hit = []
    attackerCombat = attacker.combat
    for target in enemies:
        if not attackerCombat.hasAlreadyHit(attackId, target):
            attackerCombat.hit(attackId, target)
            if data.attackType is AttackType.Magic or accuracyCheck(attacker, target, data.attackType):
                game.events["combat_hit"].invoke(attacker, target, data)
                damage = dealDamage(attacker, target, data)
                if damage:
                    hit.append(target)
    return hit


def flashAlpha(entity):
    render = entity.render

    def getAlpha():
        return render.getColor().a

    def setAlpha(value):
        color = render.getColor()
        color.a = int(value)
        render.setColor(color)

    def getModularAlpha():
        return render.alpha

    def setModularAlpha(value):
        render.alpha = int(value)

    if entity.has('modularRender'):
        getter = getModularAlpha
        setter = setModularAlpha
    else:
        getter = getAlpha
        setter = setAlpha
    game.tween.add(render, target=127, duration=125, tweening=Tween.Linear, get=getter, set=setter)
    game.tween.add(render, target=255, duration=125, delay=125, tweening=Tween.Linear, get=getter, set=setter)
    game.tween.add(render, target=127, duration=125, delay=250, tweening=Tween.Linear, get=getter, set=setter)
    game.tween.add(render, target=255, duration=125, delay=375, tweening=Tween.Linear, get=getter, set=setter)


def continueKnockback(combat, velocity, physics, timestep):
    combat.knockback.update(int(timestep))
    if combat.knockback.remaining() > 150:
        physics.setVelocity(velocity)
    else:
        physics.setFriction(Vector(0.2, 0))
    expired = combat.knockback.expired()
    if expired:
        physics.setFriction(Vector())
        physics.setVelocity(Vector())
    return expired


def knockbackTarget(target, origin, knockback):
    power = max(0, knockback - target.stats.knockbackRes.get())
    if power == 0 or not target.has('physics'):
        return
    direction = -1 if origin.x > target.getPosition().x else 1
    physics = target.physics
    velocity = physics.getVelocity()
    velocity.x = (0.5 + power * 0.5) * direction
    velocity.y = -1 - power * 0.5
    physics.setVelocity(velocity)
    combat = target.combat
    combat.knockback.reset(250)
    target.animation.play("fall")
    physics.onTimeStep.listen(partial(continueKnockback, combat, velocity))


def hitStun(target, duration):
    target.combat.isHitStunned.set(True)

    def hitStunExpire():
        target.combat.isHitStunned.set(False)
    game.timer.add(duration, hitStunExpire, isPrecise=True)


def accuracyCheck(attacker, target, attackType):
    if attackType is not AttackType.Magic:
        accuracy = attacker.stats.ACC.get()
        evasion = target.stats.EVA.get()
        results = callEvent("accuracy_check", None, attacker, target, accuracy=accuracy, evasion=evasion)
        accuracy, evasion = results.accuracy, results.evasion
        if evasion > accuracy:
            chance = min(evasion - accuracy, 50)
            return chance >= Random.get(100)
    return True


def getTalentFromAttackType(talents, attackType):
    if attackType is AttackType.Magic:
        return talents.arcane
    elif attackType is AttackType.Divine:
        return talents.divine
    else:
        return talents.arms


def getStrength(entity, attackType):
    #Get appropriate strength based on AttackType
    entityStats = entity.stats
    if attackType is AttackType.Magic:
        return entityStats.INT.get() * 3
    if attackType is AttackType.Divine:
        return entityStats.MIND.get() * 3
    elif attackType is AttackType.Ranged:
        return entityStats.ATK.get() + entityStats.AGI.get() * 2
    else:
        return entityStats.ATK.get() * 3


def getDefense(entity, attackType):
    #Get approriate defense based on AttackType
    if attackType is AttackType.Magic:
        return entity.stats.INT.get() * 3
    elif attackType is AttackType.Divine:
        return entity.stats.MIND.get() * 3
    else:
        return entity.stats.DEF.get() * 3


def factorDamageType(entity, damageType, damage):
    if damageType is DamageType.Fire:
        return damage - damage * ((entity.stats.fire.get() - 100) / 100.0)
    elif damageType is DamageType.Earth:
        return damage - damage * ((entity.stats.earth.get() - 100) / 100.0)
    elif damageType is DamageType.Wind:
        return damage - damage * ((entity.stats.wind.get() - 100) / 100.0)
    elif damageType is DamageType.Water:
        return damage - damage * ((entity.stats.water.get() - 100) / 100.0)
    else:
        return damage


def calculateDamage(strength, defense, level, power):
    # Ensure defense is at least 1
    defense = max(1, defense)
    # scale down attack power so we can work with tastier basic numbers
    power /= 3.0
    # take a ratio between the attacker's strength and the defender's defense
    attackDefenseRatio = math.sqrt(strength) / math.sqrt(defense)
    # try to steer the ratio closer to 1
    attackDefenseRatio = pow(attackDefenseRatio, 2)
    # do bonus damage based on the attacker's level
    bonusDamage = (power / 20.0) * (level - 1)
    return attackDefenseRatio * (power + bonusDamage)


def dealDamage(attacker, target, data):
    player = World.get().getPlayer(attacker) if attacker.has("playerState") else None
    callEvent('deal_damage_start', player, attacker, target, data)
    targetCombat = target.combat
    attackerLevel = attacker.combat.level.get()
    targetLevel = targetCombat.level.get()
    attackerStats = attacker.stats
    strength = getStrength(attacker, data.attackType)
    defense = getDefense(target, data.attackType)
    damage = calculateDamage(strength, defense, attackerLevel, data.power)
    damage = factorDamageType(target, data.damageType, damage)

    critRate = (attackerStats.LUCK.get() + attackerLevel - targetLevel) / 4
    isCritical = critRate >= (Random.get(65535) * 95 / 65535 + 5)
    if isCritical:
        damage *= 2

    result = callEvent('deal_damage', player, attacker, target, data, isCritical, damage=damage)
    damage = result.damage
    if damage == 0:
        return 0

    origin = attacker.getPosition()
    knockback = attackerStats.knockback.get() + data.knockback
    damage = applyDamage(target, damage, origin, knockback, "DeathCombat", isCritical, messageDict={'attacker': attacker.name})
    sendMessage('battle', 'System', "AttackDamage", attacker=attacker.name, target=target.name, amount=damage)
    targetCombat.onHit.invoke(attacker, target, data, damage)
    attacker.combat.timeSinceCombat = 0
    if player:
        talent = getTalentFromAttackType(attacker.talents, data.attackType)
        rate = 20 * (3 if target.stats.HP.get() == 0 else 1)
        amount = 15
        chanceToGainTalentPoints(player, talent, targetCombat.level.get(), rate, amount)
    return damage


def applyDamage(target, damage, origin, knockback, deathMessage, isCritical=False, messageDict={}):
    targetStats = target.stats
    result = callEvent('apply_damage', None, target, origin, damage=damage, isCritical=isCritical, knockback=knockback)
    damage = int(targetStats.HP.adjust(int(-result.damage)))
    Log.trace("Damage dealt: {}".format(damage))
    game.combat.showCombatNumber(target, -damage, result.isCritical)
    flashAlpha(target)
    if result.knockback:
        knockbackTarget(target, origin, result.knockback)
    if targetStats.HP.get() == 0:
        sound = target.combat.onDeathSound
        killEntity(target, deathMessage, messageDict)
    else:
        sound = target.combat.onDamageSound
    if sound:
        game.audio.playAt(getRandomPath(sound), target.getPosition(), broadcast=True)
    target.combat.timeSinceCombat = 0
    return damage


def participateInCombat(combatants):
    for combatant in combatants:
        combatant.combat.timeSinceCombat = 0


def killEntity(entity, message, messageDict={}):
    awardExperienceToEnemies(entity)
    entity.stats.HP.set(0)
    if entity.animation.has("death"):
        entity.animation.play("death", forceRestart=True)
        if entity.has('physics'):
            entity.physics.setVelocity(Vector(0, 0))
            if entity.has('playerState'):
                world = World.get()
                player = world.getPlayer(entity)
                world.requestRegion(entity.realm, world.playerData[player.uid].homePoint, partial(handlePlayerDeath, entity.animation, entity.physics, 0))
                entity.physics.onTimeStep.listen(partial(handlePlayerDeath, entity.animation))
                sendMessage("general", "System", message, None, target=entity.name, **messageDict)
    entity.combat.setAlive(False)


def handlePlayerDeath(animation, physics, timestep):
    if not animation.wasPlaying("death") and animation.getFrameTime() < seconds(2):
        return
    entity = physics.getParent()
    world = World.get()
    player = world.getPlayer(entity)
    placeInOpenSpace(entity, World.get().playerData[player.uid].homePoint)
    hp = entity.stats.HP
    hp.set(hp.getMax())
    entity.descent.reset()
    entity.breath.restoreBreath()
    entity.combat.setAlive(True)
    physics.wake()
    if player.uid != world.getPlayer().uid:
        stream = DataStream(Message.PLAYER_POSITION)
        stream.writeVector(entity.getPosition())
        game.network.getServer().send(player.networkId, stream)
        stream = DataStream(Message.PLAYER_INPUT)
        stream.writeBool(True)
        game.network.getServer().send(player.networkId, stream)
    return True


def handleMonsterDeath(entity, loot):
    realm = entity.realm
    position = entity.getPosition()
    # Handle loot drops
    droppedLoot = (item for item in loot if item.dropped())
    droppedPosition = Vector(position)
    droppedPosition.move(-12, -12)
    for item in droppedLoot:
        entity.realm.dropped.create(item.retrieve(), droppedPosition, Vector(0, -6))
    # Create Particle Emitter
    emitter = PhysicsParticleEmitter(realm.uid, realm.size.width, position, 1, 0)
    emitter.amount = Random.get(20, 30)
    emitter.setTexture(TextureCache.get("monster/death_particle.png"))
    emitter.velocityStart = RangeVector(Vector(-2.5, -2.0), Vector(2.5, -1.0))
    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.5, -0.5))
    emitter.rotationChange = RangeFloat(0, 45)
    emitter.particleLife = RangeInt(seconds(3), seconds(4))
    emitter.scaleStart = RangeVector(Vector(2, 2), Vector(2, 2))
    emitter.gravity = Vector(0.0, 0.04)
    emitter.friction = Vector(0.08, 0.0)
    emitter.restitution = Vector(0.1, 0.2)
    game.particles.add(emitter)
    entity.destroy()


def handleSpawnerDeath(entity, wasAlive, isAlive):
    if wasAlive and not isAlive:
        handleMonsterDeath(entity, entity.spawner.loot)


def placeInOpenSpace(entity, position):
    #Look around for a valid position
    ground = entity.realm.layers[Layer.Ground]
    if ground.isSpaceAvailable(entity.physics.getWorldBody()):
        entity.setPosition(position)
        return
    else:
        #TODO: Find a suitable place for the entity
        entity.setPosition(position)


def awardExperienceToEnemies(target):
    #THOUGHTS: First determine all nearby combatants gaining experience.
    #          base is amount of experience the monster gives
    #          Set party level equal to highest level combatant
    #          level is the level of the combatant gaining experience
    #          share is a modifier based on number of party members
    # base * (mLevel / pLevel) * (level / pLevel) * share
    #
    # One thing I don't like about this is that the amount of experience you're gaining is the same with lv1 vs lv1 and lv10 vs lv10.
    # This would be ok if the amount of experience required to level increased slightly with each level.
    # Inversely we could have the base decrease based off of mLevel... (base / 2.0) + (base / 2.0 * (maxLevel - mLevel - 1))
    targetCombat = target.combat
    if targetCombat.experienceYield == 0:
        return
    position = target.getPosition()
    radius = 1000
    enemies = getNearbyEnemies(target.combat.team, target.realm.layers, Rect(position.x - radius, position.y - radius, radius * 2, radius * 2), onlyAlive=True)
    if not enemies:
        return
    gLevel = 0
    for enemy in enemies:
        level = enemy.combat.level.get()
        if level > gLevel:
            gLevel = float(level)
    share = 1.0 / len(enemies)  # Give slight bonus for more members - do not split so evening
    base = float(targetCombat.experienceYield)
    base *= share * (targetCombat.level.get() / gLevel)
    for enemy in enemies:
        combat = enemy.combat
        level = combat.level.get()
        value = int(base * level / gLevel)
        if value > 0:
            combat.adjustExperience(value)
            if enemy.has('playerState'):
                player = World.get().getPlayer(enemy)
                sendMessage('battle', 'System', "CombatGainXp", player.networkId, target=target.name, amount=value)
                if combat.level.get() > level:
                    for _ in xrange(combat.level.get() - level):
                        sendMessage("general", "System", "CharacterLevelUp", player.networkId)


def addStatsOnLevel(stats, entity, previous, current):
    levelsGained = current - previous
    if levelsGained <= 0:
        return
    component = entity.stats
    for statName, value in stats.iteritems():
        stat = component.get(statName)
        if stat.hasMax():
            stat.adjustMax(value * levelsGained)
            stat.adjust(stat.getMax())
        else:
            stat.adjust(value * levelsGained)


def addLevelUpStats(monsterTuning, entity, previous, current):
    levelsGained = current - previous
    if levelsGained <= 0:
        return
    component = entity.stats
    for level in xrange(previous, current):
        stats = None
        for levelStats in monsterTuning.LEVEL_STATS:
            if levelStats.level > level:
                break
            stats = levelStats
        if stats is None:
            return
        for statName, value in stats.iteritems():
            if statName == 'level':
                continue
            stat = component.get(statName)
            if stat.hasMax():
                stat.adjustMax(value)
                stat.adjust(stat.getMax())
            else:
                stat.adjust(value)
