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


package engine.net;

import engine.exception.FactoryBuildException;
import engine.gameManager.ChatManager;
import engine.net.client.ClientConnection;
import engine.net.client.Protocol;
import engine.objects.PlayerCharacter;
import engine.server.MBServerStatics;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;

public class NetMsgFactory {

    // NetMsg Opcode to Constructor List
    private static final HashMap<Integer, Constructor> netMsgDefinitions = new HashMap<>();
    private static final int FLOOD_CONTROL_TRIP_SETPOINT = 1000;
    // Standardize the error strings
    private static String ALL_GOOD_JUST_NOT_ENOUGH_BYTES = "Not enough Bytes";
    private static String DESERIALIZATION_FAILURE = "Deserialization Failure";
    private static String UNIMPLEMENTED_OPCODE = "Unimplemented Opcode";
    private static String UNKNOWN_OPCODE = "Unknown Opcode";
    private final ArrayList<AbstractNetMsg> msgOutbox;
    private final AbstractConnection owner;
    protected ByteBuffer internalBuffer;
    private boolean enableFloodControl;
    private boolean bypassFloodControl; // temp bypass
    private boolean floodControlTripped;
    private int badOpcodeCount;
    private int lastMsgPosition = 0;

    public NetMsgFactory(AbstractConnection origin, boolean enableFloodControl) {
        this.internalBuffer = Network.byteBufferPool.getBuffer(18); //256k buffer

        this.bypassFloodControl = false;
        this.msgOutbox = new ArrayList<>();
        this.enableFloodControl = enableFloodControl;
        this.floodControlTripped = false;
        this.owner = origin;
    }

    public NetMsgFactory(AbstractConnection origin) {
        this(origin, true);
    }

    public static String getByteArray(ByteBufferReader reader) {
        String ret = "";
        if (reader == null)
            return ret;

        ByteBuffer bb = reader.getBb();
        if (bb == null)
            return ret;

        int length = bb.limit(); // - bb.position();
        ByteBuffer temp = bb.duplicate();
        temp.position(bb.limit());
        temp.flip();
        for (int i = 0; i < length; i++) {
            ret += Integer.toString((temp.get() & 0xff) + 0x100, 16).substring(1).toUpperCase();
        }
        return ret;
    }

    @SuppressWarnings("unchecked")
    private static AbstractNetMsg getNewInstanceOf(int opcode,
                                                   AbstractConnection origin, ByteBufferReader reader) {
        try {

            Protocol protocolMsg = Protocol.getByOpcode(opcode);

            if (protocolMsg == Protocol.NONE) {

                String errorString = DateTime.now().toString() + origin.lastProtocol.name();

                int errorCode = errorString.hashCode();


                if (origin instanceof ClientConnection) {
                    PlayerCharacter player = ((ClientConnection) origin).getPlayerCharacter();
                    if (player != null) {
//						if (MBServerStatics.worldServerName.equals("Grief"))
                        Logger.error("Invalid protocol msg for player " + player.getFirstName() + " : " + opcode + " lastopcode: " + origin.lastProtocol.name() + " Error Code : " + errorCode);
                    } else
                        Logger.error("Invalid protocol msg  : " + opcode + " lastopcode: " + origin.lastProtocol.name() + " Error Code : " + errorCode);

                }

                return null;
            }
            origin.lastProtocol = protocolMsg;

            if (protocolMsg.constructor == null) {
                return null;
            }


            Constructor<AbstractNetMsg> constructor = protocolMsg.constructor;

            if (constructor == null)
                return null;

            Object[] myArgs = new Object[2];
            myArgs[0] = origin;
            myArgs[1] = reader;

            Object object = constructor.newInstance(myArgs);

            if (object instanceof engine.net.AbstractNetMsg)
                return (AbstractNetMsg) object;

        } catch (IllegalAccessException | IllegalArgumentException | InstantiationException |
                 ExceptionInInitializerError e) {
            Logger.error(e);

        } catch (InvocationTargetException e) {
            if (e.getCause() != null
                    && e.getCause().getClass() == BufferUnderflowException.class)
                throw new BufferUnderflowException();
            Logger.error(e);
        }
        return null;
    }

    public final void addData(byte[] ba) {
        // Dont use prefab BB's here, since sizeof(ba) is unknown.
        ByteBuffer bb = ByteBuffer.wrap(ba);
        bb.position(bb.capacity());
        this.addData(bb);
    }

    public final void addData(ByteBuffer newData) {
        synchronized (this.internalBuffer) {

            int newCapacity = this.internalBuffer.position() + newData.position();

            if (newCapacity >= this.internalBuffer.capacity()) {
                //Resize!!!!
                Logger.warn(
                        "Bytebuffer is being be Resized.");

                //Get a newer, bigger BB
                ByteBuffer newBB = Network.byteBufferPool.getBufferToFit((int) (newCapacity * 1.5));

                //Copy old data in
                this.internalBuffer.flip();
                newBB.put(this.internalBuffer);

                //Get a handle on old BB
                ByteBuffer oldBB = this.internalBuffer;

                //install new BB
                this.internalBuffer = newBB;

                //Return old BB
                Network.byteBufferPool.putBuffer(oldBB);
            }

            synchronized (newData) {
                // Copy over the data.
                newData.flip();

                try {
                    this.internalBuffer.put(newData);
                } catch (Exception e) {
                    Logger.error(e.toString());
                    // TODO figure out how to handle this error.
                }
            }
        }
    }

    public void parseBuffer() {
        // Check flood control first
        if (this.floodControlTripped)
            // this.conn.disconnect();
            return;

        // MBServer.jobMan.submitJob(new ParseBufferJob(this));
        this._parseBuffer();
    }

    /**
     * This function makes a copy of the current internal byte buffer and loads
     * the copy into a ByteBufferReader. It is copied so that the Factory can
     * continue to accumulate data on the internal buffer from the
     * socketChannels. The ByteBufferReader is then used in an attempt to build
     * an AbstractNetMsg subclass based on protocolMsg. If a message is successfully
     * built, the bytes used are removed from the Factory's internal byte
     * buffer.
     *
     * @return
     * @throws Exception
     */
    protected void _parseBuffer() {
        synchronized (this.internalBuffer) {
            while (this.internalBuffer.position() > 0) {
                // Check flood control first
                if (this.floodControlTripped)
                    break;

                ByteBufferReader reader = null;

                // Check to see if the minimum amount of data is here:
                if (this.internalBuffer.position() < 4)
                    // nothing wrong, just not enough info yet.
                    break;

                // copy internal buffer into a reader
                reader = new ByteBufferReader(this.internalBuffer, false);

                // Reset the limit to the capacity
                this.internalBuffer.limit(this.internalBuffer.capacity());

                try {
                    AbstractNetMsg msg = this.tryBuild(owner, reader);

                    // error, null messages are being returned on unhandled
                    // opcodes
                    // for some reason
                    if (msg == null)
                        throw new FactoryBuildException(UNIMPLEMENTED_OPCODE);


                    if (owner.getClass().equals(ClientConnection.class)) {
                        ClientConnection client = (ClientConnection) owner;
                        client.setLastOpcode(msg.getProtocolMsg().opcode);
                    }


                    //					Logger.debug("Adding a " + msg.getSimpleClassName()
                    //							+ " to the outbox.");
                    this.addMsgToOutBox(msg);

                    this.dropLeadingBytesFromBuffer(reader.position());
                    this.bypassFloodControl = false;

                } catch (FactoryBuildException e) {
                    String error = e.getMessage();
                    int readerPos = reader.position();

                    if (error.equals(ALL_GOOD_JUST_NOT_ENOUGH_BYTES)) {
                        break;
                    }
                    // no worries, just break.

                    else if (error.equals(DESERIALIZATION_FAILURE)) {
                        // Lop readerPos bytes off the buffer.
                        this.dropLeadingBytesFromBuffer(readerPos);

                        // Lets bypass flood control for now.
                        this.bypassFloodControl = true;
                        continue;

                    } else if (error.equals(UNIMPLEMENTED_OPCODE)) {

                        if (owner.lastProtocol != null && owner.lastProtocol.constructor == null) {
                            this.dropLeadingBytesFromBuffer(readerPos);
                            this.bypassFloodControl = true;
                            continue;
                        }

                        // Lop readerPos bytes off the buffer.
                        if (reader.position() >= 4)
                            reader.position(reader.position() - 4);
                        int newPosition = Protocol.FindNextValidOpcode(reader);
                        this.dropLeadingBytesFromBuffer(newPosition);
                        // Lets bypass flood control for now.
                        this.bypassFloodControl = true;

                        continue;

                    } else if (error.equals(UNKNOWN_OPCODE)) {

                        if (owner.lastProtocol != null && owner.lastProtocol.constructor == null) {
                            this.dropLeadingBytesFromBuffer(readerPos);
                            this.bypassFloodControl = true;
                            continue;
                        }
                        // We don't know what this is or how long, so dump the
                        // first
                        // byte and try again
                        if (reader.position() >= 4)
                            reader.position(reader.position() - 4);
                        int newPosition = Protocol.FindNextValidOpcode(reader);
                        this.dropLeadingBytesFromBuffer(newPosition);
                        // Lets bypass flood control for now.
                        this.bypassFloodControl = true;

                        continue;
                    }
                } catch (Exception e) {
                    // TODO FIX THIS!!!!
//					Logger.error( e);

                }// end catch

            } // end while loop
        }
    }// end fn

    public AbstractNetMsg tryBuild(AbstractConnection origin,
                                   ByteBufferReader reader) throws FactoryBuildException {
        try {

            // Get the protocolMsg
            int opcode = reader.getInt();
            // String ocHex = StringUtils.toHexString(protocolMsg);

            if (MBServerStatics.PRINT_INCOMING_OPCODES)
                try {
                    Logger.info("Incoming protocolMsg: "
                            + Protocol.getByOpcode(opcode).name() + " " + Integer.toHexString(opcode) + ", size: " + reader.getBb().limit() + "; " + getByteArray(reader));
                } catch (Exception e) {
                    Logger.error(e);
                }

            return NetMsgFactory.getNewInstanceOf(opcode, origin, reader);

        } catch (BufferUnderflowException e) {
            // This is okay. it indicates that we recognized the protocolMsg, but
            // there isn't enough information in
            // the reader to complete the NetMsg deserialization
            throw new FactoryBuildException(ALL_GOOD_JUST_NOT_ENOUGH_BYTES);

        }
    }

    private void incrBadOpcodeCount() {
        // keeping this a nested if for Troubleshooting/clarity
        if (this.enableFloodControl == true)
            if (this.bypassFloodControl == false) {
                ++this.badOpcodeCount;


                if (this.badOpcodeCount >= FLOOD_CONTROL_TRIP_SETPOINT) {
                    if (this.owner != null) {
                        if (this.owner instanceof ClientConnection) {
                            ClientConnection client = (ClientConnection) this.owner;
                            if (client.getPlayerCharacter() != null) {
                                ChatManager.chatSystemError(client.getPlayerCharacter(), "TRIPPED Flood Control! PLEASE RELOG!");
                                Logger.info(client.getPlayerCharacter().getName() + " Tripped Flood Control!" + this.badOpcodeCount);
                            }

                        }
                    }
                    this.floodControlTripped = true;
                } else {
                    if (this.owner != null) {
                        if (this.owner instanceof ClientConnection) {
                            ClientConnection client = (ClientConnection) this.owner;
                            if (client.getPlayerCharacter() != null) {
                                ChatManager.chatSystemError(client.getPlayerCharacter(), "Client sending bad messages. bad message Count " + this.badOpcodeCount);
                                Logger.info(client.getPlayerCharacter().getName() + " has been caught sending bad opcodes. Bad Opcode Count " + this.badOpcodeCount);
                            }

                        }


                    }
                }

            }
    }

    protected final void dropLeadingBytesFromBuffer(int numberOfBytes) {
        this.internalBuffer.limit(this.internalBuffer.position());
        this.internalBuffer.position(numberOfBytes);
        this.internalBuffer.compact(); // Compact
    }

    protected boolean addMsgToOutBox(AbstractNetMsg msg) {
        synchronized (this.msgOutbox) {
            return msgOutbox.add(msg);
        }
    }

    public AbstractNetMsg getMsg() {
        synchronized (this.msgOutbox) {
            if (this.msgOutbox.isEmpty())
                return null;
            return msgOutbox.remove(0);
        }
    }

    public boolean hasMsg() {
        synchronized (this.msgOutbox) {
            return !msgOutbox.isEmpty();
        }
    }

    public boolean hasData() {
        synchronized (this.internalBuffer) {
            return (this.internalBuffer.position() != 0);
        }
    }

    public ByteBuffer getInternalBuffer() {
        return internalBuffer;
    }

}