package engine.gameManager;

import engine.Enum;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.AttackJob;
import engine.jobs.DeferredPowerJob;
import engine.net.DispatchMessage;
import engine.net.client.ClientConnection;
import engine.net.client.msg.TargetedActionMsg;
import engine.net.client.msg.UpdateStateMsg;
import engine.objects.*;
import engine.powers.DamageShield;
import engine.powers.effectmodifiers.AbstractEffectModifier;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

public class FinalCombatManager {
    public static void combatCycle(AbstractCharacter attacker, AbstractWorldObject target) {
        //early exit checks
        if(attacker == null || target == null || !attacker.isAlive() || !target.isAlive())
            return;

        switch(target.getObjectType()){
            case Building:
                if(((Building)target).isVulnerable() == false)
                    return;
                break;
            case PlayerCharacter:
            case Mob:
                PlayerBonuses bonuses = ((AbstractCharacter)target).getBonuses();
                if(bonuses != null && bonuses.getBool(Enum.ModType.ImmuneToAttack, Enum.SourceType.NONE))
                    return;
                break;
            case NPC:
                return;
        }

        Item mainWeapon = attacker.getCharItemManager().getEquipped().get(Enum.EquipSlotType.RHELD);
        Item offWeapon = attacker.getCharItemManager().getEquipped().get(Enum.EquipSlotType.LHELD);
        if(mainWeapon == null && offWeapon == null){
            //no weapons equipped, punch with both fists
            processAttack(attacker,target,Enum.EquipSlotType.RHELD);
            processAttack(attacker,target,Enum.EquipSlotType.LHELD);
        }else if(mainWeapon == null && offWeapon != null && offWeapon.template.item_skill_required.containsKey("Block")){
            //no weapon equipped with a shield, punch with one hand
            processAttack(attacker,target,Enum.EquipSlotType.RHELD);
        }else if(mainWeapon != null && offWeapon != null && offWeapon.template.item_skill_required.containsKey("Block")){
            //one weapon equipped with a shield, swing with one hand
            processAttack(attacker,target,Enum.EquipSlotType.RHELD);
        }else if(mainWeapon != null && offWeapon != null && offWeapon.template.item_skill_required.containsKey("Block") == false){
            //two weapons equipped, swing both hands
            processAttack(attacker,target,Enum.EquipSlotType.RHELD);
            processAttack(attacker,target,Enum.EquipSlotType.LHELD);
        } else if(mainWeapon == null && offWeapon != null && offWeapon.template.item_skill_required.containsKey("Block") == false){
            //swing left hand only
            processAttack(attacker,target,Enum.EquipSlotType.LHELD);
        }
    }

    public static void processAttack(AbstractCharacter attacker, AbstractWorldObject target, Enum.EquipSlotType slot){
        //check if character can even attack yet
        if(attacker.getTimestamps().containsKey("Attack" + slot.name()))
            if(System.currentTimeMillis() < attacker.getTimestamps().get("Attack" + slot.name()))
                return;

        //check if character is in range to attack target
        PlayerBonuses bonus = attacker.getBonuses();
        float rangeMod = 1.0f;
        float attackRange = MBServerStatics.NO_WEAPON_RANGE;
        Item weapon = attacker.getCharItemManager().getEquipped(slot);
        if (weapon != null) {
            if (bonus != null)
                rangeMod += bonus.getFloatPercentAll(Enum.ModType.WeaponRange, Enum.SourceType.NONE);

            attackRange = weapon.template.item_weapon_max_range * rangeMod;
        }
        if(attacker.getObjectType().equals(Enum.GameObjectType.Mob))
            if(((Mob)attacker).isSiege())
                attackRange = 300;

        float distanceSquared = attacker.loc.distanceSquared(target.loc);
        if(distanceSquared > attackRange * attackRange)
            return;

        //take stamina away from attacker
        if (weapon == null)
            attacker.modifyStamina(-0.5f, attacker, true);
        else {
            float stam = weapon.template.item_wt / 3f;
            stam = (stam < 1) ? 1 : stam;
            attacker.modifyStamina(-(stam), attacker, true);
        }

        //cancel things that are cancelled by an attack
        attacker.cancelOnAttackSwing();

        //declare relevant variables
        int min = attacker.minDamageHandOne;
        int max = attacker.maxDamageHandOne;
        int atr = attacker.atrHandOne;

        //get the proper stats based on which slot is attacking
        if(slot == Enum.EquipSlotType.LHELD){
            min = attacker.minDamageHandTwo;
            max = attacker.maxDamageHandTwo;
            atr = attacker.atrHandTwo;
        }
        int def = 0;
        if(AbstractCharacter.IsAbstractCharacter(target))
            def = ((AbstractCharacter)target).defenseRating;

        //calculate hit chance based off ATR and DEF
        int hitChance;
        float dif = atr / def;
        if (dif <= 0.8f)
            hitChance = 4;
        else
            hitChance = ((int) (450 * (dif - 0.8f)) + 4);
        if (target.getObjectType() == Enum.GameObjectType.Building)
            hitChance = 100;

        int passiveAnim = getSwingAnimation(attacker.getCharItemManager().getEquipped().get(slot).template, null, true);

        if(ThreadLocalRandom.current().nextInt(100) > hitChance) {
            TargetedActionMsg msg = new TargetedActionMsg(attacker, target, 0f, passiveAnim);

            if (target.getObjectType() == Enum.GameObjectType.PlayerCharacter)
                DispatchMessage.dispatchMsgToInterestArea(target, msg, Enum.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
            else
                DispatchMessage.sendToAllInRange(attacker, msg);

            return;
        }

        //calculate passive chances only if target is AbstractCharacter
        if(AbstractCharacter.IsAbstractCharacter(target)){
            int hitRoll = ThreadLocalRandom.current().nextInt(100);
            float dodgeChance = ((AbstractCharacter) target).getPassiveChance("Dodge", attacker.getLevel(), true);
            float blockChance = ((AbstractCharacter) target).getPassiveChance("Block", attacker.getLevel(), true);
            float parryChance = ((AbstractCharacter) target).getPassiveChance("Parry", attacker.getLevel(), true);
            if(hitRoll > dodgeChance || hitRoll > parryChance || hitRoll > blockChance){
                TargetedActionMsg msg = new TargetedActionMsg(attacker, passiveAnim, target, MBServerStatics.COMBAT_SEND_BLOCK);

                if (target.getObjectType() == Enum.GameObjectType.PlayerCharacter)
                    DispatchMessage.dispatchMsgToInterestArea(target, msg, Enum.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
                else
                    DispatchMessage.sendToAllInRange(attacker, msg);

                return;
            }
        }

        //calculate the base damage
        int damage = ThreadLocalRandom.current().nextInt(min,max + 1);
        if(damage == 0)
            return;

        //get the damage type
        Enum.SourceType damageType;
        if(attacker.getCharItemManager().getEquipped().get(slot) == null) {
            damageType = Enum.SourceType.CRUSHING;
            if(attacker.getObjectType().equals(Enum.GameObjectType.Mob))
                if(((Mob)attacker).isSiege())
                    damageType = Enum.SourceType.SIEGE;
        }else {
            damageType = (Enum.SourceType) attacker.getCharItemManager().getEquipped().get(slot).template.item_weapon_damage.keySet().toArray()[0];
        }

        //get resists
        Resists resists;
        if(AbstractCharacter.IsAbstractCharacter(target) == false){
            //this is a building
            resists = ((Building) target).getResists();
        }else{
            //this is a character
            resists = ((AbstractCharacter) target).getResists();
        }

        if(AbstractCharacter.IsAbstractCharacter(target)) {
            AbstractCharacter absTarget = (AbstractCharacter) target;
            //check damage shields
            PlayerBonuses bonuses = absTarget.getBonuses();

            if (bonuses != null) {

                ConcurrentHashMap<AbstractEffectModifier, DamageShield> damageShields = bonuses.getDamageShields();
                float total = 0;

                for (DamageShield ds : damageShields.values()) {

                    //get amount to damage back
                    float amount;
                    if (ds.usePercent())
                        amount = damage * ds.getAmount() / 100;
                    else
                        amount = ds.getAmount();

                    //get resisted damage for damagetype
                    if (resists != null)
                        amount = resists.getResistedDamage(absTarget, attacker, ds.getDamageType(), amount, 0);
                    total += amount;
                }

                if (total > 0) {
                    //apply Damage back
                    attacker.modifyHealth(-total, absTarget, true);
                    TargetedActionMsg cmm = new TargetedActionMsg(attacker, attacker, total, 0);
                    DispatchMessage.sendToAllInRange(target, cmm);
                }
            }
            if (resists != null) {
                //check for damage type immunities
                if (resists.immuneTo(damageType))
                    return;
                //calculate resisted damage including fortitude
                damage = (int) resists.getResistedDamage(attacker,(AbstractCharacter)target,damageType,damage,0);
            }
        }

        //remove damage from target health
        if(damage > 0){
            if(AbstractCharacter.IsAbstractCharacter(target)){
                ((AbstractCharacter)target).modifyHealth(-damage, attacker, true);
            }else{
                ((Building)target).setCurrentHitPoints(target.getCurrentHitpoints() - damage);
            }
            TargetedActionMsg cmm = new TargetedActionMsg(attacker,target, (float) damage,0);
            DispatchMessage.sendToAllInRange(target, cmm);
        }

        //calculate next allowed attack and update the timestamp
        long delay = 20 * 100;
        if (weapon != null){
            int wepSpeed = (int) (weapon.template.item_weapon_wepspeed);

            if (weapon.getBonusPercent(Enum.ModType.WeaponSpeed, Enum.SourceType.NONE) != 0f) //add weapon speed bonus
                wepSpeed *= (1 + weapon.getBonus(Enum.ModType.WeaponSpeed, Enum.SourceType.NONE));

            if (attacker.getBonuses() != null && attacker.getBonuses().getFloatPercentAll(Enum.ModType.AttackDelay, Enum.SourceType.NONE) != 0f) //add effects speed bonus
                wepSpeed *= (1 + attacker.getBonuses().getFloatPercentAll(Enum.ModType.AttackDelay, Enum.SourceType.NONE));

            if (wepSpeed < 10)
                wepSpeed = 10; //Old was 10, but it can be reached lower with legit buffs,effects.

            delay = wepSpeed * 100;
        }
        attacker.getTimestamps().put("Attack" + slot.name(),System.currentTimeMillis() + delay);

        //handle auto attack job creation
        ConcurrentHashMap<String, JobContainer> timers = attacker.getTimers();

        if (timers != null) {
            AttackJob aj = new AttackJob(attacker, slot.ordinal(), true);
            JobContainer job;
            job = JobScheduler.getInstance().scheduleJob(aj, (delay + 1)); // offset 1 millisecond so no overlap issue
            timers.put("Attack" + slot, job);
        } else {
            Logger.error("Unable to find Timers for Character " + attacker.getObjectUUID());
        }
    }

    public static void toggleCombat(boolean toggle, ClientConnection origin) {

        PlayerCharacter pc = SessionManager.getPlayerCharacter(origin);
        if (pc == null)
            return;

        pc.setCombat(toggle);
        if (!toggle) // toggle is move it to false so clear combat target
            pc.setCombatTarget(null); //clear last combat target

        UpdateStateMsg rwss = new UpdateStateMsg();
        rwss.setPlayer(pc);
        DispatchMessage.dispatchMsgToInterestArea(pc, rwss, Enum.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
    }

    public static void toggleSit(boolean toggle, ClientConnection origin) {

        PlayerCharacter pc = SessionManager.getPlayerCharacter(origin);
        if (pc == null)
            return;

        pc.setSit(toggle);
        UpdateStateMsg rwss = new UpdateStateMsg();
        rwss.setPlayer(pc);
        DispatchMessage.dispatchMsgToInterestArea(pc, rwss, Enum.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
    }

    //Called when character takes damage.
    public static void handleRetaliate(AbstractCharacter target, AbstractCharacter attacker) {

        if (attacker == null || target == null)
            return;

        if (attacker.equals(target))
            return;

        if (target.isMoving() && target.getObjectType().equals(Enum.GameObjectType.PlayerCharacter))
            return;

        if (!target.isAlive() || !attacker.isAlive())
            return;

        boolean isCombat = target.isCombat();

        //If target in combat and has no target, then attack back
        if(isCombat && target.combatTarget == null)
            target.setCombatTarget(attacker);
    }

    public static int getSwingAnimation(ItemTemplate wb, DeferredPowerJob dpj, boolean mainHand) {
        int token = 0;

        if (dpj != null)
            token = (dpj.getPower() != null) ? dpj.getPower().getToken() : 0;

        if (token == 563721004) //kick animation
            return 79;

        if (wb == null)
            return 75;

        ItemTemplate template = wb;

        if (mainHand) {
            if (template.weapon_attack_anim_right.size() > 0) {

                int animation;

                int random = ThreadLocalRandom.current().nextInt(template.weapon_attack_anim_right.size());

                try {
                    animation = template.weapon_attack_anim_right.get(random)[0];
                    return animation;
                } catch (Exception e) {
                    Logger.error(e.getMessage());
                    return template.weapon_attack_anim_right.get(0)[0];
                }

            } else if (template.weapon_attack_anim_left.size() > 0) {

                int animation;
                int random = ThreadLocalRandom.current().nextInt(template.weapon_attack_anim_left.size());

                try {
                    animation = template.weapon_attack_anim_left.get(random)[0];
                    return animation;
                } catch (Exception e) {
                    Logger.error(e.getMessage());
                    return template.weapon_attack_anim_right.get(0)[0];
                }
            }
        } else {
            if (template.weapon_attack_anim_left.size() > 0) {
                int animation;
                int random = ThreadLocalRandom.current().nextInt(template.weapon_attack_anim_left.size());

                try {
                    animation = template.weapon_attack_anim_left.get(random)[0];
                    return animation;
                } catch (Exception e) {
                    Logger.error(e.getMessage());
                    return template.weapon_attack_anim_right.get(0)[0];

                }
            } else if (template.weapon_attack_anim_left.size() > 0) {

                int animation;
                int random = ThreadLocalRandom.current().nextInt(template.weapon_attack_anim_left.size());

                try {
                    animation = template.weapon_attack_anim_left.get(random)[0];
                    return animation;
                } catch (Exception e) {
                    Logger.error(e.getMessage());
                    return template.weapon_attack_anim_right.get(0)[0];

                }

            }
        }

        String required = template.item_skill_used;
        String mastery = wb.item_skill_mastery_used;

        if (required.equals("Unarmed Combat"))
            return 75;
        else if (required.equals("Sword")) {

            if (ItemTemplate.isTwoHanded(template))
                return 105;
            else
                return 98;

        } else if (required.equals("Staff") || required.equals("Pole Arm")) {
            return 85;
        } else if (required.equals("Spear")) {
            return 92;
        } else if (required.equals("Hammer") || required.equals("Axe")) {
            if (ItemTemplate.isTwoHanded(template)) {
                return 105;
            } else if (mastery.equals("Throwing")) {
                return 115;
            } else {
                return 100;
            }
        } else if (required.equals("Dagger")) {
            if (mastery.equals("Throwing")) {
                return 117;
            } else {
                return 81;
            }
        } else if (required.equals("Crossbow")) {
            return 110;
        } else if (required.equals("Bow")) {
            return 109;
        } else if (ItemTemplate.isTwoHanded(template)) {
            return 105;
        } else {
            return 100;
        }
    }
}