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.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

public enum CombatManager {

    COMBAT_MANAGER;

    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.charItemManager.getEquipped().get(Enum.EquipSlotType.RHELD);
        Item offWeapon = attacker.charItemManager.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) {

        // heck 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.charItemManager.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) {
            float stam = weapon.template.item_wt / 3f;
            stam = (stam < 1) ? 1 : stam;
            attacker.modifyStamina(-(stam), attacker, true);
        } else
            attacker.modifyStamina(-0.5f, 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.charItemManager.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 (EnumSet.of(Enum.GameObjectType.PlayerCharacter, Enum.GameObjectType.NPC, Enum.GameObjectType.Mob).contains(target.getObjectType())) {
            Enum.PassiveType passiveType = Enum.PassiveType.None;
            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);

            // Passive chance clamped at 75

            dodgeChance = Math.max(0, Math.min(75, dodgeChance));
            blockChance = Math.max(0, Math.min(75, blockChance));
            parryChance = Math.max(0, Math.min(75, parryChance));

            if (hitRoll < dodgeChance)
                passiveType = Enum.PassiveType.Dodge;
            else if (hitRoll < blockChance)
                passiveType = Enum.PassiveType.Block;
            else if (hitRoll < parryChance)
                passiveType = Enum.PassiveType.Parry;


            if (passiveType.equals(Enum.PassiveType.None) == false) {
                TargetedActionMsg msg = new TargetedActionMsg(attacker, passiveAnim, target, passiveType.value);

                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.charItemManager.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.charItemManager.getEquipped().get(slot).template.item_weapon_damage.keySet().toArray()[0];
        }

        //get resists

        Resists resists;

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

        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 playerCharacter = SessionManager.getPlayerCharacter(origin);

        if (playerCharacter == null)
            return;

        playerCharacter.setCombat(toggle);

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

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

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

        PlayerCharacter playerCharacter = SessionManager.getPlayerCharacter(origin);

        if (playerCharacter == null)
            return;

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


    public static void handleRetaliate(AbstractCharacter target, AbstractCharacter attacker) {

        //Called when character takes damage.

        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;
        }
    }
}