package electrosphere.net.client;

import electrosphere.engine.Globals;
import electrosphere.engine.Main;
import electrosphere.logger.LoggerInterface;
import electrosphere.net.parser.net.message.NetworkMessage;
import electrosphere.net.parser.net.message.ServerMessage;
import electrosphere.net.parser.net.message.NetworkMessage.MessageType;
import electrosphere.net.parser.net.message.ServerMessage.ServerMessageType;
import electrosphere.net.parser.net.raw.NetworkParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

/**
 * Client networking thread
 */
public class ClientNetworking implements Runnable {
    
    /**
     * The server's address
     */
    String address;

    /**
     * The port on the server
     */
    int port;

    /**
     * Controls whether the connection is formed locally (with lists) or not (with sockets)
     */
    boolean local = false;
    
    /**
     * The socket for the connection
     */
    public Socket socket;


//    CryptoInputStream inputStream;
//    CryptoOutputStream outputStream;
    InputStream inputStream;
    OutputStream outputStream;
    boolean initialized;
    NetworkParser parser;
    
    /**
     * The client protocol
     */
    MessageProtocol messageProtocol = new MessageProtocol();

    /**
     * Maximum number of times to try connecting to a server
     */
    static final int MAX_CONNECTION_ATTEMPTS = 10;

    //set to true to also get ping and pong messages in debug logging
    boolean echoPings = false;

    //thresholds for when to send pings and to determine when we've disconnected
    static final long SEND_PING_THRESHOLD = 3000;
    static final long PING_DISCONNECT_THRESHOLD = 20000;
    //times for calculating ping-pong
    long lastPingTime = 0;
    long lastPongTime = 0;

    //debugging stuff
    String netMonitorHandle = null;
    
    /**
     * Creates a ClientNetworking object with a server address and port
     * @param address The address of the server
     * @param port The port to connect to on the server
     */
    public ClientNetworking(String address, int port){
        this.address = address;
        this.port = port;
    }

    /**
     * Creates a ClientNetworking object with a pair of streams
     * @param clientInputStream The input stream
     * @param clientOutputStream The output stream
     */
    public ClientNetworking(InputStream clientInputStream, OutputStream clientOutputStream){
        this.local = true;
        this.inputStream = clientInputStream;
        this.outputStream = clientOutputStream;
    }
    
    
    
    @Override
    public void run(){
        initialized = false;
            //        final SecretKeySpec key = new SecretKeySpec(("1234567890123456").getBytes(),"AES");
//        final Properties properties = new Properties();
//        final RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(4096, BigInteger.probablePrime(4000, new Random()));
//        try {
//            inputStream = new CryptoInputStream("AES/ECB/PKCS5Padding",properties,socket.getInputStream(),key,spec);
//        } catch (IOException ex) {
//            ex.printStackTrace();
//        }
//        try {
//            outputStream = new CryptoOutputStream("AES/ECB/PKCS5Padding",properties,socket.getOutputStream(),key,spec);
//        } catch (IOException ex) {
//            ex.printStackTrace();
//        }

        //Used to copy messages from network parser to NetMonitor
        // List<NetworkMessage> netMonitorCache = new LinkedList<NetworkMessage>();

        ///
        ///
        ///        SETUP
        ///
        ///


        if(!this.local) {
            //attempt connection
            int connectionAttempts = 0;
            boolean connected = false;
            while(!connected){
                try {
                    this.socket = new Socket(address,port);
                    connected = true;
                } catch (IOException ex) {
                    LoggerInterface.loggerNetworking.WARNING("Client failed to connect!");
                }
                if(!connected){
                    try {
                        TimeUnit.MILLISECONDS.sleep(50);
                    } catch (InterruptedException e) {}
                    connectionAttempts++;
                }
                if(connectionAttempts > MAX_CONNECTION_ATTEMPTS){
                    LoggerInterface.loggerNetworking.ERROR("Max client connection attempts!", new Exception());
                }
            }

            if(connected && Globals.netMonitor != null){
                this.netMonitorHandle = Globals.netMonitor.registerConnection();
            }

            //grab input/output streams
            try {
                inputStream = socket.getInputStream();
                outputStream = socket.getOutputStream();
            } catch (IOException ex) {
                LoggerInterface.loggerNetworking.ERROR("Error on client socket", ex);
            }
        }

        //create parser
        parser = new NetworkParser(inputStream,outputStream);
        


        ///
        ///
        ///      MAIN     LOOP
        ///
        ///

        //start parsing messages
        initialized = true;
        while(Main.isRunning()){
            //attempt poll incoming messages
            parser.readMessagesIn();
            //outgoing messages
            parser.pushMessagesOut();
            //parses messages asynchronously
            this.parseMessagesAsynchronously();

            //ping logic
            long currentTime = System.currentTimeMillis();
            //basically if we haven't sent a ping in a while, send one
            if(currentTime - lastPingTime > SEND_PING_THRESHOLD){
                queueOutgoingMessage(ServerMessage.constructPingMessage());
                lastPingTime = currentTime;
                if(lastPongTime == 0){
                    lastPongTime = lastPingTime;
                }
            }
            if(lastPingTime - lastPongTime > PING_DISCONNECT_THRESHOLD){
                //disconnected from the server
                LoggerInterface.loggerNetworking.WARNING("Disconnected from server");
                //close socket
                if(socket != null && socket.isConnected()){
                    try {
                        socket.close();
                    } catch (IOException e) {
                        LoggerInterface.loggerNetworking.ERROR("Error closing socket", e);
                    }
                }
                //TODO: kick us back to the main menu
                break;
            }
        }
        
    }



    /**
     * Parses messages asynchronously
     */
    public void parseMessagesAsynchronously(){
        if(initialized){
            while(parser.hasIncomingMessaage()){
                NetworkMessage message = parser.popIncomingMessage();
                //net monitor
                if(Globals.netMonitor != null){
                    Globals.netMonitor.logMessage(netMonitorHandle, message, true);
                }
                //print network message
                printMessage(message);
                //do something
                Globals.profiler.beginCpuSample("ClientProtocol.handleMessage");
                this.messageProtocol.handleAsyncMessage(message);
                Globals.profiler.endCpuSample();
            }
        }
    }
    
    
    /**
     * Parses messages from the client
     */
    public void parseMessagesSynchronous(){
        this.messageProtocol.handleSyncMessages();
    }

    /**
     * Print out the network message type, this only prints ping and pong if echoPings is true
     */
    void printMessage(NetworkMessage message){
        //only print ping and pong if echoPings is true
        if(message.getType() == MessageType.SERVER_MESSAGE){
            if((((ServerMessage)message).getMessageSubtype()) == ServerMessageType.PING ||
               (((ServerMessage)message).getMessageSubtype()) == ServerMessageType.PONG
                ){
                        if(this.echoPings == true){
                    LoggerInterface.loggerNetworking.DEBUG_LOOP("[CLIENT] New message " + message.getType());
                }
            } else {
                LoggerInterface.loggerNetworking.DEBUG_LOOP("[CLIENT] New message " + message.getType());
            }
        } else {
            LoggerInterface.loggerNetworking.DEBUG_LOOP("[CLIENT] New message " + message.getType());
        }
    }
    
    /**
     * Queues an outgoing message
     * @param message The message to send to the server
     */
    public void queueOutgoingMessage(NetworkMessage message){
        //net monitor stuff
        if(Globals.netMonitor != null){
            Globals.netMonitor.logMessage(netMonitorHandle, message, false);
        }
        //actually queue
        parser.addOutgoingMessage(message);
    }
    
    /**
     * Gets the message protocol
     * @return The message protocol inside this object
     */
    public MessageProtocol getMessageProtocol(){
        return messageProtocol;
    }

    public void markReceivedPongMessage(){
        lastPongTime = System.currentTimeMillis();
    }
    
}
