package electrosphere.net.parser.net.raw;

import electrosphere.net.parser.net.message.MessagePool;
import electrosphere.net.parser.net.message.NetworkMessage;
import io.github.studiorailgun.CircularByteBuffer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * The main message parser. This is used to serialize/deserialize messages to/from the provided streams.
 */
public class NetworkParser {

    /**
     * The size of the read buffer
     */
    static final int READ_BLOCK_SIZE = 16 * 1024 * 1024;

    /**
     * The size of the circular buffer
     */
    static final int CIRCULAR_BUFFER_SIZE = 64 * 1024 * 1024;
    
    /**
     * The input stream for the parser
     */
    InputStream incomingStream;

    /**
     * The output stream for the parser
     */
    OutputStream outgoingStream;
    
    /**
     * The queue of incoming messages that have been parsed
     */
    CopyOnWriteArrayList<NetworkMessage> incomingMessageQueue = new CopyOnWriteArrayList<NetworkMessage>();

    /**
     * The queue of outgoing messages that have yet to be sent
     */
    CopyOnWriteArrayList<NetworkMessage> outgoingMessageQueue = new CopyOnWriteArrayList<NetworkMessage>();

    /**
     * Message object pool
     */
    MessagePool pool = new MessagePool();
    
    /**
     * The byte buffer for storing incoming bytes
     */
    CircularByteBuffer incomingByteBuffer = new CircularByteBuffer(CIRCULAR_BUFFER_SIZE);

    /**
     * The block array used to read blocks of bytes in
     */
    byte[] readBuffer = new byte[READ_BLOCK_SIZE];

    /**
     * The outgoing byte buffer
     */
    CopyOnWriteArrayList<Byte> outgoingByteQueue = new CopyOnWriteArrayList<Byte>();

    /**
     * The number of bytes read
     */
    long totalBytesRead = 0;

    /**
     * If set to true, the parser will automatically release messages on send.
     * Otherwise, will not release when the message is sent.
     */
    boolean releaseOnSend = true;
    
    
    /**
     * Constructor
     * @param incomingStream The stream of incoming bytes
     * @param outgoingStream The stream of outgoing bytes
     */
    public NetworkParser(InputStream incomingStream, OutputStream outgoingStream){
        this.incomingStream = incomingStream;
        this.outgoingStream = outgoingStream;
    }

    /**
     * Reads messages from the input stream
     */
    public void readMessagesIn() throws IOException {
        //read in bytes
        int bytesRead = 0;
        while(incomingStream.available() > 0){
            // nextValue = incomingStream.read();
            bytesRead = incomingStream.read(readBuffer, 0, READ_BLOCK_SIZE);
            if(bytesRead > 0){
                incomingByteBuffer.add(readBuffer, bytesRead);
            }
            totalBytesRead = totalBytesRead + bytesRead;
        }
        //parse byte queue for messages
        //for each message, append to clientIncomingMessageQueue
        NetworkMessage newMessage;
        while((newMessage = NetworkMessage.parseBytestreamForMessage(incomingByteBuffer,this.pool))!=null){
            incomingMessageQueue.add(newMessage);
        }
    }
    
    /**
     * Pushes messages out across the output stream
     * @throws IOException Thrown if a message fails to serialize or the output stream fails to write
     */
    public void pushMessagesOut() throws IOException {
        for(NetworkMessage message : outgoingMessageQueue){
            outgoingMessageQueue.remove(message);
            outgoingStream.write(message.getRawBytes());
            if(this.releaseOnSend){
                this.pool.release(message);
            }
        }
    }
    
    /**
     * Checks if there is a fully parsed incoming message in the queue
     * @return true if there is message in the queue, false otherwise
     */
    public boolean hasIncomingMessaage(){
        return incomingMessageQueue.size() > 0;
    }
    
    /**
     * Pops a fully parsed incoming message from the queue
     * @return The message
     */
    public NetworkMessage popIncomingMessage(){
        return incomingMessageQueue.remove(0);
    }
    
    /**
     * Adds a message to the outgoing queue
     * @param message The message
     */
    public void addOutgoingMessage(NetworkMessage message){
        outgoingMessageQueue.add(message);
    }

    /**
     * Copies the current contents of the incoming messages queue to a provided list
     * @param messages The list to copy the incoming messages to
     */
    public void copyIncomingMessages(List<NetworkMessage> messages){
        messages.addAll(incomingMessageQueue);
    }

    /**
     * Copies the current contents of the outgoing messages queue to a provided list
     * @param messages The list to copy the outgoing messages to
     */
    public void copyOutgoingMessages(List<NetworkMessage> messages){
        messages.addAll(outgoingMessageQueue);
    }

    /**
     * Gets the total number of bytes read by this connection
     * @return The total number of bytes
     */
    public long getNumberOfBytesRead(){
        return totalBytesRead;
    }

    /**
     * Releases a network message object back into the pool
     * @param message The message
     */
    public void release(NetworkMessage message){
        this.pool.release(message);
    }

    /**
     * Gets the message pool
     * @return The message pool
     */
    public MessagePool getMessagePool(){
        return this.pool;
    }

    /**
     * If set to true, the parser will automatically release messages on send.
     * Otherwise, will not release when the message is sent.
     * @param releaseOnSend true to release messages on send, false otherwise
     */
    public void setReleaseOnSend(boolean releaseOnSend){
        this.releaseOnSend = releaseOnSend;
    }

}
