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

package engine.InterestManagement;

import engine.Enum;
import engine.Enum.DispatchChannel;
import engine.Enum.GameObjectType;
import engine.gameManager.GroupManager;
import engine.gameManager.SessionManager;
import engine.job.JobScheduler;
import engine.jobs.RefreshGroupJob;
import engine.net.AbstractNetMsg;
import engine.net.Dispatch;
import engine.net.DispatchMessage;
import engine.net.client.ClientConnection;
import engine.net.client.msg.*;
import engine.objects.*;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.ArrayList;
import java.util.HashSet;

import static engine.math.FastMath.sqr;

public enum InterestManager implements Runnable {

    INTERESTMANAGER;

    private static long lastTime;

    InterestManager() {
        Logger.info(" Interest Management thread is running.");
    }

    public static void forceLoad(AbstractWorldObject awo) {

        AbstractNetMsg msg = null;
        LoadStructureMsg lsm;
        LoadCharacterMsg lcm;
        NPC npc;
        Corpse corpse;
        HashSet<AbstractWorldObject> toUpdate;

        switch (awo.getObjectType()) {
            case Building:
                lsm = new LoadStructureMsg();
                lsm.addObject((Building) awo);
                msg = lsm;
                break;
            case Corpse:
                corpse = (Corpse) awo;
                lcm = new LoadCharacterMsg(corpse, false);
                msg = lcm;
                break;
            case NPC:
                npc = (NPC) awo;
                lcm = new LoadCharacterMsg(npc, false);
                msg = lcm;
                break;
            default:
                return;
        }

        toUpdate = WorldGrid.getObjectsInRangePartial(awo.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE, MBServerStatics.MASK_PLAYER);

        boolean send;

        for (AbstractWorldObject tar : toUpdate) {
            PlayerCharacter player = (PlayerCharacter) tar;
            HashSet<AbstractWorldObject> loadedStaticObjects = player.getLoadedStaticObjects();
            send = false;

            if (!loadedStaticObjects.contains(awo)) {
                loadedStaticObjects.add(awo);
                send = true;
            }

            if (send) {

                Dispatch dispatch = Dispatch.borrow(player, msg);
                DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
            }
        }
    }

    public static void HandleSpecialUnload(Building building, ClientConnection origin) {

        if (Regions.FurnitureRegionMap.get(building.getObjectUUID()) == null)
            return;

        Regions buildingRegion = Regions.FurnitureRegionMap.get(building.getObjectUUID());

        if (!buildingRegion.isOutside())
            return;

        MoveToPointMsg moveMsg = new MoveToPointMsg(building);

        if (origin != null)
            origin.sendMsg(moveMsg);
    }

    public static void reloadCharacter(AbstractCharacter absChar) {

        UnloadObjectsMsg uom = new UnloadObjectsMsg();
        uom.addObject(absChar);
        LoadCharacterMsg lcm = new LoadCharacterMsg(absChar, false);

        HashSet<AbstractWorldObject> toSend = WorldGrid.getObjectsInRangePartial(absChar.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE,
                MBServerStatics.MASK_PLAYER);

        PlayerCharacter pc = null;

        if (absChar.getObjectType().equals(GameObjectType.PlayerCharacter))
            pc = (PlayerCharacter) absChar;

        for (AbstractWorldObject awo : toSend) {

            PlayerCharacter pcc = (PlayerCharacter) awo;

            if (pcc == null)
                continue;

            ClientConnection cc = SessionManager.getClientConnection(pcc);

            if (cc == null)
                continue;

            if (pcc.getObjectUUID() == absChar.getObjectUUID())
                continue;

            else {
                if (pc != null)
                    if (pcc.getSeeInvis() < pc.getHidden())
                        continue;

                if (!cc.sendMsg(uom)) {
                    String classType = uom.getClass().getSimpleName();
                    Logger.error("Failed to send message ");
                }

                if (!cc.sendMsg(lcm)) {
                    String classType = lcm.getClass().getSimpleName();
                    Logger.error("Failed to send message");
                }
            }
        }
    }

    @Override
    public void run() {
        beginLoadJob();
    }

    private void beginLoadJob() {

        InterestManager.lastTime = System.currentTimeMillis();

        while (true) {
            try {
                updateAllPlayers();
            } catch (Exception e) {
                Logger.error("InterestManager.BeginLoadJob:updateAllPlayers", e);
            }
            try {
                Thread.sleep(advanceOneSecond());
            } catch (Exception e) {
                Logger.error("InterestManager.BeginLoadJob:advanceOneSecond", e);
            }
        }
    }

    private long advanceOneSecond() {

        long curTime = System.currentTimeMillis();
        long dur = 1000 + this.lastTime - curTime;

        if (dur < 0) {
            // Last update took more then one second, not good...
            Logger.warn("LoadJob took more then one second to complete.");
            this.lastTime = curTime + 100;
            return 100;
        }
        this.lastTime += 1000;
        return dur;
    }

    // Forces the loading of static objects (corpses and buildings).
    // Needed to override threshold limits on loading statics

    private void updateAllPlayers() {
        // get all players

        for (PlayerCharacter playerCharacter : SessionManager.getAllActivePlayerCharacters()) {

            if (playerCharacter == null)
                continue;

            ClientConnection origin = playerCharacter.getClientConnection();

            if (origin == null)
                continue;

            if (!playerCharacter.isEnteredWorld())
                continue;

            if (playerCharacter.getTeleportLock().readLock().tryLock()) {

                try {
                    updateStaticList(playerCharacter, origin);
                    updateMobileList(playerCharacter, origin);
                } catch (Exception e) {
                    Logger.error(e);
                } finally {
                    playerCharacter.getTeleportLock().readLock().unlock();
                }
            }
        }
    }

    private void updateStaticList(PlayerCharacter player, ClientConnection origin) {


        // Only update if we've moved far enough to warrant it

        float distanceSquared = player.getLoc().distanceSquared2D(player.getLastStaticLoc());

        if (distanceSquared > sqr(64))
            player.setLastStaticLoc(player.getLoc());
        else if (player.isDirtyLoad() == false)
            return;

        // Get Statics in range

        HashSet<AbstractWorldObject> toLoad = WorldGrid.getObjectsInRangePartial(player.getLoc(), MBServerStatics.STRUCTURE_LOAD_RANGE,
                MBServerStatics.MASK_STATIC);

        // get list of obects loaded that need removed

        HashSet<AbstractWorldObject> loadedStaticObjects = player.getLoadedStaticObjects();

        HashSet<AbstractWorldObject> toRemove = null;

        toRemove = new HashSet<>(loadedStaticObjects);

        toRemove.removeAll(toLoad);

        // unload static objects now out of range

        if (toRemove.size() > 0) {

            UnloadObjectsMsg uom = new UnloadObjectsMsg();

            for (AbstractWorldObject obj : toRemove) {

                if (obj.getObjectType().equals(GameObjectType.Building))
                    InterestManager.HandleSpecialUnload((Building) obj, origin);

                if (obj != null && !obj.equals(player))
                    uom.addObject(obj);
            }

            Dispatch dispatch = Dispatch.borrow(player, uom);
            DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
        }

        loadedStaticObjects.removeAll(toRemove);

        // remove any object to load that are already loaded

        toLoad.removeAll(loadedStaticObjects);

        LoadStructureMsg lsm = new LoadStructureMsg();
        LoadCharacterMsg lcm = null;
        ArrayList<LoadCharacterMsg> lcmList = new ArrayList<>();

        for (AbstractWorldObject awo : toLoad) {

            if (awo.getObjectType().equals(GameObjectType.Building))
                lsm.addObject((Building) awo);
            else if (awo.getObjectType().equals(GameObjectType.Corpse)) {
                Corpse corpse = (Corpse) awo;
                lcm = new LoadCharacterMsg(corpse, PlayerCharacter.hideNonAscii());

                Dispatch dispatch = Dispatch.borrow(player, lcm);
                DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);

            } else if (awo.getObjectType().equals(GameObjectType.NPC)) {
                NPC npc = (NPC) awo;
                lcm = new LoadCharacterMsg(npc, PlayerCharacter.hideNonAscii());

                lcmList.add(lcm);
            }
        }

        if (lsm.getStructureList().size() > 0) {
            Dispatch dispatch = Dispatch.borrow(player, lsm);
            DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
        }

        for (LoadCharacterMsg lc : lcmList) {

            Dispatch dispatch = Dispatch.borrow(player, lc);
            DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
        }

        loadedStaticObjects.addAll(toLoad);
        player.setDirtyLoad(false);
    }

    private void updateMobileList(PlayerCharacter player, ClientConnection origin) {

        if (player == null)
            return;

        // Get list of players in range
        // TODO for now use a generic getALL list, later tie into Quad Tree
        HashSet<AbstractWorldObject> toLoad = WorldGrid.getObjectsInRangePartial(player.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE,
                MBServerStatics.MASK_MOBILE);

        HashSet<AbstractWorldObject> toRemove = new HashSet<>();

        HashSet<AbstractWorldObject> toLoadToPlayer = new HashSet<>();

        for (AbstractWorldObject loadedObject : toLoad) {

            switch (loadedObject.getObjectType()) {
                case PlayerCharacter:
                    PlayerCharacter loadedPlayer = (PlayerCharacter) loadedObject;

                    if (loadedPlayer.getObjectUUID() == player.getObjectUUID())
                        continue;

                    if (player.getSeeInvis() < loadedPlayer.getHidden())
                        continue;

                    if (loadedPlayer.safemodeInvis())
                        continue;

                    if (player.getLoadedObjects().contains(loadedPlayer))
                        continue;

                    if (!loadedPlayer.isInWorldGrid())
                        continue;

                    toLoadToPlayer.add(loadedPlayer);
                    break;
                //not playerCharacter, mobs,npcs and corpses cant be invis or safemode, just add normaly
                default:
                    if (player.getLoadedObjects().contains(loadedObject))
                        continue;

                    if (!loadedObject.isInWorldGrid())
                        continue;

                    toLoadToPlayer.add(loadedObject);
                    break;
            }
        }

        float unloadDistance = MBServerStatics.CHARACTER_LOAD_RANGE;
        for (AbstractWorldObject playerLoadedObject : player.getLoadedObjects()) {

            if (playerLoadedObject.getObjectType().equals(GameObjectType.PlayerCharacter)) {
                PlayerCharacter loadedPlayer = (PlayerCharacter) playerLoadedObject;
                if (player.getSeeInvis() < loadedPlayer.getHidden())
                    toRemove.add(playerLoadedObject);
                else if (loadedPlayer.safemodeInvis())
                    toRemove.add(playerLoadedObject);
            }

            if (!playerLoadedObject.isInWorldGrid())
                toRemove.add(playerLoadedObject);
            else if (playerLoadedObject.getLoc().distanceSquared2D(player.getLoc()) > unloadDistance * unloadDistance)
                toRemove.add(playerLoadedObject);

        }

        player.getLoadedObjects().addAll(toLoadToPlayer);
        player.getLoadedObjects().removeAll(toRemove);

        // get list of obects loaded to remove

        // unload objects now out of range

        if (toRemove.size() > 0) {

            UnloadObjectsMsg uom = new UnloadObjectsMsg();

            for (AbstractWorldObject obj : toRemove) {

                try {
                    if (obj != null)
                        if (obj.equals(player)) // don't unload self
                            continue;

                    uom.addObject(obj);

                    if (obj.getObjectType() == GameObjectType.Mob)
                        ((Mob) obj).playerAgroMap.remove(player.getObjectUUID());
                } catch (Exception e) {
                    Logger.error("UnloadCharacter", obj.getObjectUUID() + " " + e.getMessage());
                }
            }

            if (!uom.getObjectList().isEmpty()) {
                Dispatch dispatch = Dispatch.borrow(player, uom);
                DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
            }
        }

        LoadCharacterMsg lcm = null;
        ArrayList<AbstractWorldObject> players = new ArrayList<>();
        ArrayList<AbstractWorldObject> addToList = new ArrayList<>();

        for (AbstractWorldObject awo : toLoadToPlayer) {


            try {
                if (awo.equals(player))  // dont load yourself
                    continue;

                if ((awo.getObjectTypeMask() & MBServerStatics.MASK_PLAYER) != 0) {

                    // object to load is a player

                    PlayerCharacter awopc = (PlayerCharacter) awo;

                    // dont load if invis

                    if (player.getSeeInvis() < awopc.getHidden())
                        continue;

                    lcm = new LoadCharacterMsg(awopc, PlayerCharacter.hideNonAscii());
                    players.add(awo);

                    // check if in a group with the person being loaded
                    // and if so set updateGroup flag

                    if (GroupManager.getGroup(player) != null
                            && GroupManager.getGroup(player) == GroupManager.getGroup(awopc))

                        // submit a job as for some reason the client needs a delay
                        // with group updates
                        // as it wont update if we do RefreshGroup directly after
                        // sending the lcm below

                        JobScheduler.getInstance().scheduleJob(new RefreshGroupJob(player, awopc), MBServerStatics.LOAD_OBJECT_DELAY);

                } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_MOB) != 0) {
                    Mob awonpc = (Mob) awo;

                    if (!awonpc.isAlive() && (awonpc.isPet() || awonpc.isSiege() || awonpc.isNecroPet() || awonpc.isPlayerGuard()))
                        continue;

                    //removed, interest manager should still load mob corpses

                    if (awonpc.despawned == true)
                        continue;

                    awonpc.playerAgroMap.put(player.getObjectUUID(), false);
                    ((Mob) awonpc).setCombatTarget(null);
                    lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii());

                } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_NPC) != 0) {
                    NPC awonpc = (NPC) awo;
                    lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii());
                } else if ((awo.getObjectTypeMask() & MBServerStatics.MASK_PET) != 0) {
                    Mob awonpc = (Mob) awo;

                    if (!awonpc.isAlive())
                        continue;

                    awonpc.playerAgroMap.put(player.getObjectUUID(), false);

                    if ((awonpc.agentType.equals(Enum.AIAgentType.MOBILE)))
                        ((Mob) awonpc).setCombatTarget(null);

                    lcm = new LoadCharacterMsg(awonpc, PlayerCharacter.hideNonAscii());
                }

                addToList.add(awo);

                if (lcm != null) {
                    Dispatch dispatch = Dispatch.borrow(player, lcm);
                    DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
                }


            } catch (Exception e) {
                Logger.error(awo.getObjectUUID() + " " + e.getMessage());
            }
            //Delaying character loading to reduce bandwidth consumption
        }

        // send effects for all players being loaded
        // do it on a timer otherwise we may get failures as te client needs
        // time to process lcm
        //Added effects to LoadCharacter Serialization.
        //JobScheduler.getInstance().scheduleJob(new LoadEffectsJob(players, origin), MBServerStatics.LOAD_OBJECT_DELAY);
    }

    public synchronized void HandleLoadForEnterWorld(PlayerCharacter player) {

        if (player == null)
            return;

        for(PlayerCharacter pc : SessionManager.getAllActivePlayerCharacters()){
            if(pc.equals(player)){
                try{
                    WorldGrid.RemoveWorldObject(player);
                }catch(Exception e){

                }
            }
        }



        ClientConnection origin = player.getClientConnection();

        if (origin == null)
            return;

        // Update loaded upbjects lists

        player.isBoxed = PlayerCharacter.checkIfBoxed(player);
        player.setDirtyLoad(true);
        updateStaticList(player, origin);
        updateMobileList(player, origin);
    }

    public synchronized void HandleLoadForTeleport(PlayerCharacter playerCharacter) {

        if (playerCharacter == null)
            return;

        ClientConnection origin = playerCharacter.getClientConnection();

        if (origin == null)
            return;

        // Update loaded upbjects lists

        playerCharacter.setDirtyLoad(true);
        updateStaticList(playerCharacter, origin);
        updateMobileList(playerCharacter, origin);

    }

    public static void setObjectDirty(AbstractWorldObject abstractWorldObject) {

        HashSet<AbstractWorldObject> playerList = WorldGrid.getObjectsInRangePartial(abstractWorldObject.getLoc(), MBServerStatics.CHARACTER_LOAD_RANGE, MBServerStatics.MASK_PLAYER);

        if (playerList.isEmpty())
            return;

        for (AbstractWorldObject playerObject : playerList) {

            PlayerCharacter playerCharacter = (PlayerCharacter) playerObject;
            playerCharacter.setDirtyLoad(true);
        }
    }
}