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


package engine.gameManager;

import engine.InterestManagement.WorldGrid;
import engine.math.Vector3fImmutable;
import engine.mbEnums.DispatchChannel;
import engine.mbEnums.GameObjectType;
import engine.net.AbstractNetMsg;
import engine.net.Dispatch;
import engine.net.MessageDispatcher;
import engine.net.client.ClientConnection;
import engine.objects.AbstractWorldObject;
import engine.objects.Item;
import engine.objects.PlayerCharacter;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.HashSet;

import static engine.net.MessageDispatcher.dispatchCount;
import static engine.net.MessageDispatcher.maxRecipients;

// MB Dev Notes:
// Dispatch Manager is the main interface to the threaded async message
// delivery system used by Magicbane.
//
// A Dispatch is .borrowed() and used to wrap an outgoing protocol message.
// A distribution list is created and passed to the MessageDispatcher for
// delivery.

public enum DispatchManager {

    DISPATCH_MANAGER;

    public static void startMessagePump() {

        Thread messageDispatcher;
        messageDispatcher = new Thread(new MessageDispatcher());

        messageDispatcher.setName("MessageDispatcher");
        messageDispatcher.start();
    }


    public static void sendToAllInRange(AbstractWorldObject obj,
                                        AbstractNetMsg msg) {

        if (obj == null)
            return;

        if (obj.getObjectType() == GameObjectType.PlayerCharacter || obj.getObjectType() == GameObjectType.Mob || obj.getObjectType() == GameObjectType.NPC || obj.getObjectType() == GameObjectType.Corpse)
            dispatchMsgToInterestArea(obj, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
        else
            dispatchMsgToInterestArea(obj, msg, DispatchChannel.PRIMARY, MBServerStatics.STRUCTURE_LOAD_RANGE, false, false);

    }

    // Dispatches a message to a playercharacter's interest area
    // Method includes handling of exclusion rules for visibility, self, etc.

    public static void dispatchMsgToInterestArea(AbstractWorldObject sourceObject, AbstractNetMsg msg, DispatchChannel dispatchChannel, int interestRange, boolean sendToSelf, boolean useIgnore) {

        Dispatch messageDispatch;
        HashSet<AbstractWorldObject> gridList;
        PlayerCharacter gridPlayer;
        AbstractWorldObject dispatchSource;
        PlayerCharacter sourcePlayer = null;
        long recipientCount = 0;

        if (sourceObject == null)
            return;

        // If the source of the message is a structure, item or player
        // setup our method variables accordingly.

        switch (sourceObject.getObjectType()) {
            case Item:
                dispatchSource = (AbstractWorldObject) ((Item) sourceObject).getOwner();
                break;
            case PlayerCharacter:
                dispatchSource = sourceObject;
                sourcePlayer = (PlayerCharacter) sourceObject;
                if (sourcePlayer.getClientConnection() != null && sendToSelf) {
                    Dispatch dispatch = Dispatch.borrow(sourcePlayer, msg);
                    DispatchManager.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
                }


                break;
            default:
                dispatchSource = sourceObject;
        }

        gridList = WorldGrid.getObjectsInRangePartial(dispatchSource.getLoc(), interestRange, MBServerStatics.MASK_PLAYER);

        for (AbstractWorldObject gridObject : gridList) {

            gridPlayer = (PlayerCharacter) gridObject;

            // Apply filter options if source of dispatch is a player

            if ((dispatchSource.getObjectType() == GameObjectType.PlayerCharacter) &&
                    (sourcePlayer != null)) {

                if (gridPlayer.getObjectUUID() == sourcePlayer.getObjectUUID())
                    continue;

                if ((useIgnore == true) && (gridPlayer.isIgnoringPlayer(sourcePlayer) == true))
                    continue;

                if (gridPlayer.canSee(sourcePlayer) == false)
                    continue;
            }

            messageDispatch = Dispatch.borrow(gridPlayer, msg);
            MessageDispatcher.send(messageDispatch, dispatchChannel);
            recipientCount++;
        }

        // Update metrics

        if (recipientCount > maxRecipients[dispatchChannel.getChannelID()])
            maxRecipients[dispatchChannel.getChannelID()] = recipientCount;

        dispatchCount[dispatchChannel.getChannelID()].increment();
    }

    public static void dispatchMsgToInterestArea(Vector3fImmutable targetLoc, AbstractWorldObject sourceObject, AbstractNetMsg msg, DispatchChannel dispatchChannel, int interestRange, boolean sendToSelf, boolean useIgnore) {

        Dispatch messageDispatch;
        HashSet<AbstractWorldObject> gridList;
        PlayerCharacter gridPlayer;
        AbstractWorldObject dispatchSource;
        PlayerCharacter sourcePlayer = null;
        long recipientCount = 0;

        if (sourceObject == null)
            return;

        // If the source of the message is a structure, item or player
        // setup our method variables accordingly.

        switch (sourceObject.getObjectType()) {
            case Item:
                dispatchSource = (AbstractWorldObject) ((Item) sourceObject).getOwner();
                break;
            case PlayerCharacter:
                dispatchSource = sourceObject;
                sourcePlayer = (PlayerCharacter) sourceObject;

                if (sourcePlayer.getClientConnection() != null && sendToSelf) {
                    Dispatch dispatch = Dispatch.borrow(sourcePlayer, msg);
                    DispatchManager.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
                }


                break;
            default:
                dispatchSource = sourceObject;
        }

        gridList = WorldGrid.getObjectsInRangePartial(targetLoc, interestRange, MBServerStatics.MASK_PLAYER);

        for (AbstractWorldObject gridObject : gridList) {

            gridPlayer = (PlayerCharacter) gridObject;

            // Apply filter options if source of dispatch is a player

            if ((dispatchSource.getObjectType() == GameObjectType.PlayerCharacter) &&
                    (sourcePlayer != null)) {

                if (gridPlayer.getObjectUUID() == sourcePlayer.getObjectUUID())
                    continue;

                if ((useIgnore == true) && (gridPlayer.isIgnoringPlayer(sourcePlayer) == true))
                    continue;

                if (gridPlayer.canSee(sourcePlayer) == false)
                    continue;
            }

            messageDispatch = Dispatch.borrow(gridPlayer, msg);
            MessageDispatcher.send(messageDispatch, dispatchChannel);
            recipientCount++;
        }

        // Update metrics

        if (recipientCount > maxRecipients[dispatchChannel.getChannelID()])
            maxRecipients[dispatchChannel.getChannelID()] = recipientCount;

        dispatchCount[dispatchChannel.getChannelID()].increment();
    }

    // Sends a message to all players in the game

    public static void dispatchMsgToAll(AbstractNetMsg msg) {

        Dispatch messageDispatch;
        long recipientCount = 0;

        // Send message to nobody?  No thanks!

        if (SessionManager.getAllActivePlayerCharacters().isEmpty())
            return;

        // Messages to all we will default to the secondary dispatch
        // delivery channel.  They are generally large, or inconsequential.

        for (PlayerCharacter player : SessionManager.getAllActivePlayerCharacters()) {
            messageDispatch = Dispatch.borrow(player, msg);
            MessageDispatcher.send(messageDispatch, DispatchChannel.SECONDARY);
            recipientCount++;
        }

        // Update metrics

        if (recipientCount > maxRecipients[DispatchChannel.SECONDARY.getChannelID()])
            maxRecipients[DispatchChannel.SECONDARY.getChannelID()] = recipientCount;

        dispatchCount[DispatchChannel.SECONDARY.getChannelID()].increment();

    }

    public static void dispatchMsgToAll(PlayerCharacter source, AbstractNetMsg msg, boolean ignore) {

        Dispatch messageDispatch;
        long recipientCount = 0;

        // Send message to nobody?  No thanks!

        if (SessionManager.getAllActivePlayerCharacters().isEmpty())
            return;

        // Messages to all we will default to the secondary dispatch
        // delivery channel.  They are generally large, or inconsequential.

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

            if (ignore && player.isIgnoringPlayer(source))
                continue;

            messageDispatch = Dispatch.borrow(player, msg);
            MessageDispatcher.send(messageDispatch, DispatchChannel.SECONDARY);
            recipientCount++;
        }

        // Update metrics

        if (recipientCount > maxRecipients[DispatchChannel.SECONDARY.getChannelID()])
            maxRecipients[DispatchChannel.SECONDARY.getChannelID()] = recipientCount;

        dispatchCount[DispatchChannel.SECONDARY.getChannelID()].increment();

    }

    // Sends a message to an arbitrary distribution list

    public static void dispatchMsgDispatch(Dispatch messageDispatch, DispatchChannel dispatchChannel) {

        if (messageDispatch == null) {
            Logger.info("DISPATCH Null for DispatchMessage!");
            return;
        }

        // No need to serialize an empty list

        if (messageDispatch.player == null) {
            Logger.info("Player Null for Dispatch!");
            messageDispatch.release();
            return;
        }


        MessageDispatcher.send(messageDispatch, dispatchChannel);

        dispatchCount[dispatchChannel.getChannelID()].increment();

    }

    public static void serializeDispatch(Dispatch messageDispatch) {
        ClientConnection connection;

        if (messageDispatch.player == null) {
            Logger.info("Player null in serializeDispatch");
            messageDispatch.release();
            return;
        }

        connection = messageDispatch.player.getClientConnection();

        if ((connection == null) || (connection.isConnected() == false)) {
            messageDispatch.release();
            return;
        }

        if (messageDispatch.msg == null) {
            Logger.error("null message sent to " + messageDispatch.player.getName());
            messageDispatch.release();
            return;
        }

        if (!connection.sendMsg(messageDispatch.msg))
            Logger.error(messageDispatch.msg.getProtocolMsg() + " failed sending to " + messageDispatch.player.getName());

        messageDispatch.release();
    }


}
