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

package engine.wpakpowers;

import engine.InterestManagement.WorldGrid;
import engine.gameManager.ChatManager;
import engine.gameManager.DbManager;
import engine.gameManager.DispatchManager;
import engine.gameManager.SessionManager;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.FinishRecycleTimeJob;
import engine.jobs.WpakUsePowerJob;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.ModifyHealthMsg;
import engine.net.client.msg.PerformActionMsg;
import engine.net.client.msg.RecyclePowerMsg;
import engine.net.client.msg.UpdateStateMsg;
import engine.objects.*;
import engine.server.MBServerStatics;
import engine.util.Hasher;
import engine.wpak.EffectsParser;
import engine.wpak.PowerActionParser;
import engine.wpak.PowersParser;
import engine.wpak.data.Effect;
import engine.wpak.data.*;
import org.pmw.tinylog.Logger;

import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;

import static engine.math.FastMath.sqr;

public class WpakPowerManager {
    public static HashMap<String, Effect> _effectsLookup = new HashMap<>();
    public static HashMap<Integer, PowerAction> _powerActionLookup = new HashMap<>();
    public static HashMap<Integer, Power> _powersLookup = new HashMap<>();

    private static JobScheduler js;

    public static void init() {
        EffectsParser.parseWpakFile();
        PowersParser.parseWpakFile();
        PowerActionParser.parseWpakFile();
    }

    public static void beginCast(final PerformActionMsg msg, ClientConnection origin, boolean sendCastToSelf) {

        if (executePower(msg, origin, sendCastToSelf)) {
            // Cast failed for some reason, reset timer

            RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(msg.getPowerUsedID());
            Dispatch dispatch = Dispatch.borrow(origin.getPlayerCharacter(), recyclePowerMsg);
            DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);

            // Send Fail to cast message
            PlayerCharacter pc = SessionManager.getPlayerCharacter(origin);

            if (pc != null) {
                sendPowerMsg(pc, 2, msg);
                if (pc.isCasting()) {
                    pc.update();
                }

                pc.setIsCasting(false);
            }
        }
    }

    private static boolean executePower(final PerformActionMsg msg, ClientConnection origin, boolean sendCastToSelf) {

        //check to see if the caster is valid
        PlayerCharacter playerCharacter = SessionManager.getPlayerCharacter(origin);
        if (playerCharacter == null)
            return false;

        //make sure player is still alive
        if (!playerCharacter.isAlive() && msg.getPowerUsedID() != 428589216) { //succor
            RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(msg.getPowerUsedID());
            Dispatch dispatch = Dispatch.borrow(playerCharacter, recyclePowerMsg);
            DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
            return false;
        }

        //make sure the recycle timer has actually elapsed
        if (playerCharacter.getRecycleTimers().containsKey(msg.getPowerUsedID())) {
            Logger.warn("usePowerA(): Cheat attempted? '" + msg.getPowerUsedID() + "' recycle timer not finished " + playerCharacter.getName());
            return false;
        }

        //lookup the power that was cast
        Power powerCast = _powersLookup.get(msg.getPowerUsedID());
        if (powerCast == null) {
            ChatManager.chatSayInfo(playerCharacter, "This power is not implemented yet.");
            return true;
        }

        if (playerCharacter.getLastPower() != null)
            return true;

        // get numTrains for power
        int trains = msg.getNumTrains();

        if (trains > powerCast.maxLevel) {
            trains = powerCast.maxLevel;
            msg.setNumTrains(trains);
        }

        //sanity check for amount of trains in spell cast
        if (playerCharacter.getPowers() != null && playerCharacter.getPowers().containsKey(msg.getPowerUsedID())) {
            CharacterPower cp = playerCharacter.getPowers().get(msg.getPowerUsedID());
            if (cp != null) {
                int tot = cp.getTotalTrains();
                if (tot == 0)
                    return false;
                if (trains != tot) {
                    trains = tot;
                    msg.setNumTrains(trains);
                }
            }
        }

        //get casting time
        int time = getRecycleTime(powerCast, trains);

        //combat mode sanity check
        if (playerCharacter.isCombat()) {
            if (!allowedInCombat(powerCast))
                return true;
        } else if (!allowedOutOfCombat(powerCast))
            return true;

        //stunned check
        PlayerBonuses bonus = playerCharacter.getBonuses();
        mbEnums.SourceType sourceType = mbEnums.SourceType.GetSourceType(powerCast.category);

        if (bonus != null && (bonus.getBool(mbEnums.ModType.Stunned, mbEnums.SourceType.None) || bonus.getBool(mbEnums.ModType.CannotCast, mbEnums.SourceType.None) || bonus.getBool(mbEnums.ModType.BlockedPowerType, sourceType)))
            return true;

        //sanity check for casting while moving
        Vector3fImmutable endLoc = playerCharacter.getEndLoc();

        if (!powerCast.canCastWhileMoving)
            if (playerCharacter.isMoving()) {
                float distanceLeftSquared = endLoc.distanceSquared2D(playerCharacter.getLoc());
                if (distanceLeftSquared > sqr(playerCharacter.getSpeed()))
                    return true;
            }

        //get the actual target form the message
        int type = msg.getTargetType();
        int UUID = msg.getTargetID();

        if (type == -1 || type == 0 || UUID == -1 || UUID == 0)
            return true;

        AbstractWorldObject target = (AbstractWorldObject) DbManager.getObject(mbEnums.GameObjectType.values()[type], UUID);

        //check to make sure power can be cast on building if target is a building
        if (target != null && target.getObjectType() == mbEnums.GameObjectType.Building && !powerCast.target_type.equals(mbEnums.PowerTargetType.BUILDING)) {
            sendPowerMsg(playerCharacter, 9, new PerformActionMsg(msg));
            return true;
        }

        //validate casting range
        if (playerCharacter.getLoc().distanceSquared2D(msg.getTargetLoc()) > (powerCast.range * powerCast.range))
            return true;

        //validate prereqs for power cast
        //equipment prereqs
        if (!powerCast.equipmentPreReq.isEmpty())
            for (EquipmentPreReq prereq : powerCast.equipmentPreReq) {
                String requiredSkill = prereq.skill;

                if (playerCharacter.charItemManager.equipped.get(prereq.slot) != null) {
                    Item equippedItem = playerCharacter.charItemManager.equipped.get(prereq.slot);
                    if (!equippedItem.template.item_skill_mastery_used.equals(requiredSkill) && !equippedItem.template.item_skill_used.equals(requiredSkill))
                        return true;
                } else {
                    return true;
                }
            }

        //effect prereqs
        if (!powerCast.effectPreReqs.isEmpty()) {
            for (Effect prereq : powerCast.effectPreReqs) {
                if (!playerCharacter.effects.contains(prereq.effect_id) && !playerCharacter.effects.contains(prereq.effect_name))
                    return true;
            }
        }

        float cost = getCost(powerCast, trains);
        if (bonus != null)
            cost *= (1 + bonus.getFloatPercentAll(mbEnums.ModType.PowerCost, mbEnums.SourceType.None));

        if (playerCharacter.getAltitude() > 0)
            cost *= 1.5f;

        if (cost > 0)
            if ((playerCharacter.getObjectTypeMask() & MBServerStatics.MASK_UNDEAD) != 0)
                if (playerCharacter.getHealth() <= cost)
                    return true;
                else {
                    playerCharacter.modifyHealth(-cost, playerCharacter, true);
                    ModifyHealthMsg mhm = new ModifyHealthMsg(playerCharacter, playerCharacter, -cost, 0f, 0f, 0, null, 9999, 0);
                    mhm.setOmitFromChat(1);
                    DispatchManager.dispatchMsgToInterestArea(playerCharacter, mhm, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
                }
            else if (powerCast.costType.name().equals("MANA"))
                if (playerCharacter.getMana() < cost)
                    return true;
                else
                    playerCharacter.modifyMana(-cost, playerCharacter, true);
            else if (powerCast.costType.name().equals("STAMINA"))
                if (playerCharacter.getStamina() < cost)
                    return true;
                else
                    playerCharacter.modifyStamina(-cost, playerCharacter, true);
            else if (playerCharacter.getHealth() <= cost)
                return true;
            else
                playerCharacter.modifyHealth(-cost, playerCharacter, true);

        if (time > 0) {
            FinishRecycleTimeJob frtj = new FinishRecycleTimeJob(playerCharacter, msg);
            playerCharacter.getRecycleTimers().put(msg.getPowerUsedID(), js.scheduleJob(frtj, time));
        } else {
            // else send recycle message to unlock power
            RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(msg.getPowerUsedID());
            Dispatch dispatch = Dispatch.borrow(playerCharacter, recyclePowerMsg);
            DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
        }

        int tr = msg.getNumTrains();
        DispatchManager.dispatchMsgToInterestArea(playerCharacter, msg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, sendCastToSelf, false);

        //Make new msg..
        PerformActionMsg copyMsg = new PerformActionMsg(msg);
        copyMsg.setNumTrains(tr);

        // make person casting stand up if spell (unless they're casting a chant which does not make them stand up)
        if (powerCast.isSpell() && !powerCast.isChant() && playerCharacter.isSit()) {
            playerCharacter.update();
            playerCharacter.setSit(false);
            UpdateStateMsg updateStateMsg = new UpdateStateMsg(playerCharacter);
            DispatchManager.dispatchMsgToInterestArea(playerCharacter, updateStateMsg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
        }

        // update cast (use skill) fail condition
        playerCharacter.cancelOnCast();

        // update castSpell (use spell) fail condition if spell
        if (powerCast.isSpell())
            playerCharacter.cancelOnSpell();

        // get cast time in ms.
        time = getCastTime(powerCast, trains);

        // set player is casting for regens

        if (time > 100) {
            playerCharacter.update();
            playerCharacter.setIsCasting(true);
        }

        playerCharacter.setLastMovementState(playerCharacter.getMovementState());

        // run timer job to end cast
        if (time < 1) // run immediately
            finishUsePower(copyMsg, playerCharacter, target);
        else {
            WpakUsePowerJob upj = new WpakUsePowerJob(playerCharacter, copyMsg, target);
            JobContainer jc = js.scheduleJob(upj, time);

            // make lastPower
            playerCharacter.setLastPower(jc);
        }


        return false;
    }

    public static void finishUsePower(PerformActionMsg msg, PlayerCharacter caster, AbstractWorldObject target) {

        Power powerUsed = _powersLookup.get(msg.getPowerUsedID());

        if (powerUsed == null) {
            Logger.error("Invalid power: " + msg.getPowerUsedID());
            return;
        }

        if (powerUsed.maxMobTargets > 1 || powerUsed.maxPlayerTargets > 1)
            AoeHandler(caster, target, powerUsed, msg.getNumTrains());
        else
            applyAllPowerEffects(caster, powerUsed, msg.getNumTrains(), target);
    }

    private static void AoeHandler(PlayerCharacter caster, AbstractWorldObject target, Power powerUsed, int rank) {

        HashSet<AbstractWorldObject> mobTargets = new HashSet<>();
        HashSet<AbstractWorldObject> pcTargets = new HashSet<>();
        int count = 1;

        if (powerUsed.maxMobTargets > 0)
            mobTargets = WorldGrid.getObjectsInRangePartial(target.loc, powerUsed.areaRange, MBServerStatics.MASK_MOB);

        if (powerUsed.maxPlayerTargets > 0)
            pcTargets = WorldGrid.getObjectsInRangePartial(target.loc, powerUsed.areaRange, MBServerStatics.MASK_PLAYER);

        for (AbstractWorldObject mob : mobTargets) {
            if (count < powerUsed.maxMobTargets + 1) {
                applyAllPowerEffects(caster, powerUsed, rank, mob);
                count++;
            } else {
                break;
            }
        }

        count = 1;
        for (AbstractWorldObject pc : pcTargets) {
            if (count < powerUsed.maxPlayerTargets + 1) {
                applyAllPowerEffects(caster, powerUsed, rank, pc);
                count++;
            } else {
                break;
            }
        }
    }

    public static void applyAllPowerEffects(AbstractCharacter caster, Power power, int rank, AbstractWorldObject target) {

        for (ActionEntry actionEntry : power.actionEntries) {

            PowerAction powerAction = _powerActionLookup.get(Hasher.SBStringHash(actionEntry.action_id));

            if (powerAction == null) {
                Logger.error("Null PowerAction for " + actionEntry.action_id);
                continue;
            }

            // Iterate effects for this powerAction and apply

            for (Effect effect : powerAction.effects) {

                // Create pojo to hold effect/modifiers stored in AWO

                AppliedEffect appliedEffect = new AppliedEffect();
                appliedEffect.effect = effect;
                appliedEffect.rank = rank;

                // Add calculated modifiers to pojo

                for (ModifierEntry modifierEntry : effect.mods) {
                    Object modifier = modifierEntry.type.behaviorType.apply(caster, target, power,
                            powerAction, effect, modifierEntry, rank);
                    appliedEffect.modifiers.put(modifierEntry.type, modifier);
                }

                // Add this power effect to the target
                // or overwrite the old value

                target._effects.put(effect, appliedEffect);
            }
            // target.updateBonuses() here?
        }
    }

    public static void sendPowerMsg(PlayerCharacter playerCharacter, int type, PerformActionMsg msg) {

        if (playerCharacter == null)
            return;

        msg.setUnknown05(type);

        switch (type) {
            case 3:
            case 4:
                msg.setUnknown04(2);
                DispatchManager.dispatchMsgToInterestArea(playerCharacter, msg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
                break;
            default:
                msg.setUnknown04(1);
                Dispatch dispatch = Dispatch.borrow(playerCharacter, msg);
                DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
        }
    }

    public static int getRecycleTime(Power power, int trains) { // returns cast time in ms
        if (power.curves.get("RECYCLETIME") != null)
            return (int) (((power.recycle_time + (power.curves.get("RECYCLETIME").getValue() * trains)) * 1000) + getCastTime(power, trains));
        else
            return (int) (((power.recycle_time * (1 + (power.curves.get("RECYCLETIME").getValue() * trains))) * 1000) + getCastTime(power, trains));
    }

    public static int getCastTime(Power power, int trains) { // returns cast time in ms
        if (power.curves.get("INITTIME") != null)
            return (int) ((power.init_time + (power.curves.get("INITTIME").getValue() * trains)) * 1000);
        else
            return (int) ((power.init_time * (1 + (power.curves.get("INITTIME").getValue() * trains))) * 1000);
    }

    public static float getCost(Power power, int trains) {
        if (power.curves.get("COSTAMT") != null)
            return power.cost + (power.curves.get("COSTAMT").getValue() * trains);
        else
            return power.cost * (1 + (power.curves.get("COSTAMT").getValue() * trains));

    }

    public static boolean allowedInCombat(Power power) {
        switch (power.castingMode.name()) {
            case "NONE":
            case "BOTH":
            case "COMBAT":
                return true;
        }
        return false;
    }

    public static boolean allowedOutOfCombat(Power power) {
        switch (power.castingMode.name()) {
            case "NONE":
            case "BOTH":
            case "NONCOMBAT":
                return true;
        }
        return false;
    }

    public static float applyCurveToModifier(PowerAction powerAction, ModifierEntry modifierEntry, int rank) {

        float scaledValue;

        // Method scales by either integer or float values driven by the curve type

        if (EnumSet.of(mbEnums.CompoundCurveType.DefaultFlat, mbEnums.CompoundCurveType.DefaultSlope,
                mbEnums.CompoundCurveType.DefaultSlopeDown).contains(powerAction.rampCurve))
            scaledValue = powerAction.rampCurve.getValue() + (modifierEntry.compoundCurveType.getValue() * rank);
        else
            scaledValue = powerAction.rampCurve.getValue() * (1 + (modifierEntry.compoundCurveType.getValue() * rank));

        scaledValue = scaledValue * 0.01f;

        return scaledValue;
    }
}
