// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com


package engine.powers.effectmodifiers;

import engine.Enum.DamageType;
import engine.Enum.GameObjectType;
import engine.Enum.ModType;
import engine.Enum.SourceType;
import engine.gameManager.ChatManager;
import engine.jobs.AbstractEffectJob;
import engine.jobs.DamageOverTimeJob;
import engine.net.AbstractNetMsg;
import engine.net.DispatchMessage;
import engine.net.client.msg.ModifyHealthKillMsg;
import engine.net.client.msg.ModifyHealthMsg;
import engine.objects.*;
import org.pmw.tinylog.Logger;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.ThreadLocalRandom;

public class HealthEffectModifier extends AbstractEffectModifier {

    private DamageType damageType;

    public HealthEffectModifier(ResultSet rs) throws SQLException {
        super(rs);
        String damageTypeDB = rs.getString("type");
        try {
            this.damageType = DamageType.valueOf(damageTypeDB);
        } catch (IllegalArgumentException e) {
            Logger.error("DamageType could not be loaded from database. " + "UUID = " + this.UUID
                    + " value received = '" + damageTypeDB + '\'', e);
        }
    }

    public static float getMinDamage(float baseMin, float intelligence, float spirit, float focus) {
        float min = baseMin * (((float) Math.pow(intelligence, 0.75f) * 0.0311f) + (0.02f * (int) focus) + ((float) Math.pow(spirit, 0.75f) * 0.0416f));
        return (float) ((int) (min + 0.5f)); //round to nearest whole number
    }

    public static float getMaxDamage(float baseMax, float intelligence, float spirit, float focus) {
        float max = baseMax * (((float) Math.pow(intelligence, 0.75f) * 0.0785f) + (0.015f * (int) focus) + ((float) Math.pow(spirit, 0.75f) * 0.0157f));
        return (float) ((int) (max + 0.5f)); //round to nearest whole number
    }

    @Override
    protected void _applyEffectModifier(AbstractCharacter source, AbstractWorldObject awo, int trains, AbstractEffectJob effect) {
        if (awo == null) {
            Logger.error("_applyEffectModifier(): NULL AWO passed in.");
            return;
        }

        if (effect == null) {
            Logger.error("_applyEffectModifier(): NULL AbstractEffectJob passed in.");
            return;
        }

        float modAmount = 0f;

        // Modify health by percent
        if (this.percentMod != 0f) {

            //high level mobs/players should not be %damaged/healed.
            if (awo.getHealthMax() > 25000f && (this.percentMod < 0f || this.percentMod > 5f))
                return;

            float mod = 1f;
            if (this.useRampAdd)
                mod = (this.percentMod + (this.ramp * trains)) / 100;
            else
                mod = (this.percentMod * (1 + (this.ramp * trains))) / 100;
            modAmount = mod * awo.getHealthMax();
            if (AbstractWorldObject.IsAbstractCharacter(awo)) {
                if (((AbstractCharacter) awo).isSit())
                    modAmount *= 2.5f;
            }

            //debug for spell damage and atr
            if (source.getDebug(16) && source.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                PlayerCharacter pc = (PlayerCharacter) source;
                String smsg = "Percent Damage: " + mod * 100 + '%';
                ChatManager.chatSystemInfo(pc, smsg);
            }
        }

        // Modify health by min/max amount
        else if (this.minMod != 0f || this.maxMod != 0f) {
            float min = this.minMod;
            float max = this.maxMod;
            if (this.ramp > 0f) {
                float mod = this.ramp * trains;
                if (this.useRampAdd) {
                    min += mod;
                    max += mod;
                } else {
                    min *= (1 + mod);
                    max *= (1 + mod);
                }
            }
            if (source.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                PlayerCharacter pc = (PlayerCharacter) source;

                float focus;
                CharacterSkill skill = pc.getSkills().get(effect.getPower().getSkillName());
                if (skill == null)
                    focus = CharacterSkill.getQuickMastery(pc, effect.getPower().getSkillName());
                else
                    focus = skill.getModifiedAmount();
                //TODO clean up old formulas once new one is verified
                //				min *= (0.5 + 0.0075 * pc.getStatIntCurrent() + 0.011 * pc.getStatSpiCurrent() + 0.0196 * focus);
                //				max *= (0.62 + 0.0192 * pc.getStatIntCurrent() + 0.00415 * pc.getStatSpiCurrent() + 0.015 * focus);
                float intt = (pc.getStatIntCurrent() >= 1) ? (float) pc.getStatIntCurrent() : 1f;
                float spi = (pc.getStatSpiCurrent() >= 1) ? (float) pc.getStatSpiCurrent() : 1f;
                //				min *= (intt * 0.0045 + 0.055 * (float)Math.sqrt(intt - 0.5) + spi * 0.006 + 0.07 * (float)Math.sqrt(spi - 0.5) + 0.02 * (int)focus);
                //				max *= (intt * 0.0117 + 0.13 * (float)Math.sqrt(intt - 0.5) + spi * 0.0024 + (float)Math.sqrt(spi - 0.5) * 0.021 + 0.015 * (int)focus);
                min = HealthEffectModifier.getMinDamage(min, intt, spi, focus);
                max = HealthEffectModifier.getMaxDamage(max, intt, spi, focus);

                //debug for spell damage and atr
                if (pc.getDebug(16)) {
                    String smsg = "Damage: " + (int) Math.abs(min) + " - " + (int) Math.abs(max);
                    ChatManager.chatSystemInfo(pc, smsg);
                }
            } else if (source.getObjectType() == GameObjectType.Mob) {
                Mob pc = (Mob) source;

                float focus;
                CharacterSkill skill = pc.getSkills().get(effect.getPower().getSkillName());
                if (skill == null)
                    focus = CharacterSkill.getQuickMastery(pc, effect.getPower().getSkillName());
                else
                    focus = skill.getModifiedAmount();
                //TODO clean up old formulas once new one is verified
                //				min *= (0.5 + 0.0075 * pc.getStatIntCurrent() + 0.011 * pc.getStatSpiCurrent() + 0.0196 * focus);
                //				max *= (0.62 + 0.0192 * pc.getStatIntCurrent() + 0.00415 * pc.getStatSpiCurrent() + 0.015 * focus);
                float intt = (pc.getStatIntCurrent() >= 1) ? (float) pc.getStatIntCurrent() : 1f;

                if (pc.isPlayerGuard())
                    intt = 200;
                float spi = (pc.getStatSpiCurrent() >= 1) ? (float) pc.getStatSpiCurrent() : 1f;

                if (pc.isPlayerGuard())
                    spi = 200;
                //				min *= (intt * 0.0045 + 0.055 * (float)Math.sqrt(intt - 0.5) + spi * 0.006 + 0.07 * (float)Math.sqrt(spi - 0.5) + 0.02 * (int)focus);
                //				max *= (intt * 0.0117 + 0.13 * (float)Math.sqrt(intt - 0.5) + spi * 0.0024 + (float)Math.sqrt(spi - 0.5) * 0.021 + 0.015 * (int)focus);
                min = HealthEffectModifier.getMinDamage(min, intt, spi, focus);
                max = HealthEffectModifier.getMaxDamage(max, intt, spi, focus);

                //debug for spell damage and atr
                //				if (pc.getDebug(16)) {
                //					String smsg = "Damage: " + (int)Math.abs(min) + " - " + (int)Math.abs(max);
                //					ChatManager.chatSystemInfo(pc, smsg);
                //				}
            }
            modAmount = calculateDamage(source, min, max, awo, trains);
            PlayerBonuses bonus = source.getBonuses();

            // Apply any power effect modifiers (such as stances)
            if (bonus != null)
                modAmount *= (1 + (bonus.getFloatPercentAll(ModType.PowerDamageModifier, SourceType.None)));
        }
        if (modAmount == 0f)
            return;
        if (AbstractWorldObject.IsAbstractCharacter(awo)) {
            AbstractCharacter ac = (AbstractCharacter) awo;

            if (!ac.isAlive())
                return;

            if(source.getObjectType().equals(GameObjectType.PlayerCharacter)){
                modAmount *= ((PlayerCharacter)source).ZergMultiplier;
            }

            int powerID = 0, effectID = 0;
            String powerName = "";
            if (effect.getPower() != null) {
                powerID = effect.getPower().getToken();
                powerName = effect.getPower().getName();
            } else {
                Logger.error("Power has returned null! Damage will fail to register! (" + (ac.getCurrentHitpoints() > 0 ? "Alive)" : "Dead)"));
            }

            if (effect.getEffect() != null) {
                effectID = effect.getEffect().getToken();
            } else {
                Logger.error("Effect has returned null! Damage will fail to register! (" + (ac.getCurrentHitpoints() > 0 ? "Alive)" : "Dead)"));
            }

            //see if target is immune to heals
            if (modAmount > 0f) {
                boolean skipImmune = false;
                // first tick of HoT going thru SM was removed in a later patch
				/*if (effect.getAction().getPowerAction() instanceof DirectDamagePowerAction) {
					ArrayList<ActionsBase> actions = effect.getPower().getActions();
					for (ActionsBase ab : actions) {
						AbstractPowerAction apa = ab.getPowerAction();
						if (apa instanceof DamageOverTimePowerAction)
							skipImmune = true;
					}
				}*/

                PlayerBonuses bonus = ac.getBonuses();
                if (!skipImmune && bonus.getFloat(ModType.BlackMantle, SourceType.Heal) >= trains) {
                    ModifyHealthMsg mhm = new ModifyHealthMsg(source, ac, 0f, 0f, 0f, powerID, powerName, trains, effectID);
                    mhm.setUnknown03(5); //set target is immune
                    DispatchMessage.sendToAllInRange(ac, mhm);
                    return;
                }
            }
            float mod = 0;

            //Modify health

            mod = ac.modifyHealth(modAmount, source, false);

            float cur = awo.getCurrentHitpoints();
            float maxAmount = awo.getHealthMax() - cur;

            AbstractNetMsg mhm = null;
            if (modAmount < 0 && cur < 0 && mod != 0)
                mhm = new ModifyHealthKillMsg(source, ac, modAmount, 0f, 0f, powerID, powerName, trains, effectID);
            else
                mhm = new ModifyHealthMsg(source, ac, modAmount, 0f, 0f, powerID, powerName, trains, effectID);

            if (effect instanceof DamageOverTimeJob) {
                if (mhm instanceof ModifyHealthMsg)
                    ((ModifyHealthMsg) mhm).setOmitFromChat(1);
                else if (mhm instanceof ModifyHealthKillMsg)
                    ((ModifyHealthKillMsg) mhm).setUnknown02(1);
            }

            //send the damage

            DispatchMessage.sendToAllInRange(ac, mhm);

            //			//send corpse if this kills a mob
            //			//TODO fix the someone misses blurb.
            //			if(awo instanceof Mob && awo.getHealth() <= 0) {
            //				CombatMessageMsg cmm = new CombatMessageMsg(null, 0, awo, 15);
            //				try {
            //					DispatchMessage.sendToAllInRange(ac, cmm);
            //				} catch (MsgSendException e) {
            //					Logger.error("MobCorpseSendError", e);
            //				}
            //			}
        } else if (awo.getObjectType().equals(GameObjectType.Building)) {

            Building b = (Building) awo;

            if (modAmount < 0 && (!b.isVulnerable()))
                return; //can't damage invul building

            int powerID = 0, effectID = 0;
            String powerName = "";
            if (effect.getPower() != null) {
                powerID = effect.getPower().getToken();
                powerName = effect.getPower().getName();
            } else
                Logger.error("Power has returned null! Damage will fail to register! (" + (b.getRank() == -1 ? "Standing)" : "Destroyed)"));

            if (effect.getEffect() != null) {
                effectID = effect.getEffect().getToken();
            } else
                Logger.error("Effect has returned null! Damage will fail to register! (" + (b.getRank() == -1 ? "Standing)" : "Destroyed)"));

            float mod = b.modifyHealth(modAmount, source);
            ModifyHealthMsg mhm = new ModifyHealthMsg(source, b, modAmount, 0f, 0f, powerID, powerName, trains, effectID);

            if (effect instanceof DamageOverTimeJob)
                mhm.setOmitFromChat(1);

            //send the damage

            DispatchMessage.sendToAllInRange(b, mhm);

        }
    }

    private float calculateDamage(AbstractCharacter source, float minDamage, float maxDamage, AbstractWorldObject awo, int trains) {

        // get range between min and max
        float range = maxDamage - minDamage;

        // Damage is calculated twice to average a more central point
        float damage = ThreadLocalRandom.current().nextFloat() * range;
        damage = (damage + (ThreadLocalRandom.current().nextFloat() * range)) / 2;

        // put it back between min and max
        damage += minDamage;

        Resists resists = null;
        // get resists
        if (AbstractWorldObject.IsAbstractCharacter(awo)) {
            AbstractCharacter ac = (AbstractCharacter) awo;
            resists = ac.getResists();
        } else if (awo.getObjectType().equals(GameObjectType.Building))
            resists = ((Building) awo).getResists();

        // calculate resists in if any
        if (resists != null) {
            if (AbstractWorldObject.IsAbstractCharacter(awo))
                damage = resists.getResistedDamage(source, (AbstractCharacter) awo, damageType, damage * -1, trains) * -1;
            else
                damage = resists.getResistedDamage(source, null, damageType, damage * -1, trains) * -1;
        }

        if (AbstractWorldObject.IsAbstractCharacter(awo)) {
            AbstractCharacter ac = (AbstractCharacter) awo;
            if (ac.isSit())
                damage *= 2.5f; // increase damage if sitting
        }

        return damage;
    }

    @Override
    public void applyBonus(AbstractCharacter ac, int trains) {

    }

    @Override
    public void applyBonus(Item item, int trains) {
    }

    @Override
    public void applyBonus(Building building, int trains) {
    }

}