package electrosphere.net.server;

import electrosphere.entity.types.creature.CreatureTemplate;
import electrosphere.engine.Globals;
import electrosphere.engine.Main;
import electrosphere.entity.ServerEntityUtils;
import electrosphere.logger.LoggerInterface;
import electrosphere.net.parser.net.message.AuthMessage;
import electrosphere.net.parser.net.message.NetworkMessage;
import electrosphere.net.parser.net.message.ServerMessage;
import electrosphere.net.parser.net.raw.NetworkParser;
import electrosphere.net.server.player.Player;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * A connection to the server
 */
public class ServerConnectionHandler implements Runnable {
    
    //the player id associated with this connection
    static int playerIdIncrementer = 0;

    //local carrier variables
    boolean local = false;

    //socket carrier variables
    Socket socket;

    //the streams for the connection
//    CryptoInputStream inputStream;
//    CryptoOutputStream outputStream;
    /**
     * The input stream for packets
     */
    InputStream inputStream;
    /**
     * The output stream for packets
     */
    OutputStream outputStream;

    //the network parser for the streams
    NetworkParser networkParser;

    //initialized status
    boolean initialized;
    //authentication status
    boolean isAuthenticated = false;

    //the player id
    int playerID;
    //the player's entity id
    int playerEntityID;

    //the creature template associated with this player
    CreatureTemplate currentCreatureTemplate;
    
    //the server protocol object associated with this player
    MessageProtocol messageProtocol;

    //thresholds for determining when to send pings and when a client has disconnected
    static final long SEND_PING_THRESHOLD = 3000;
    static final long PING_DISCONNECT_THRESHOLD = 20000;
    //used to keep track of ping/pong messages with client
    long lastPingTime = 0;
    long lastPongTime = 0;
    //flag to disconnect due to pipe break
    boolean socketException = false;


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

    //the lock used for synchronizing the synchronous message queue
    Semaphore synchronousMessageLock = new Semaphore(1);
    //the queue of synchonous network messages
    List<NetworkMessage> synchronousMessageQueue = new CopyOnWriteArrayList<NetworkMessage>();
    
    /**
     * Constructs a connection from a socket
     * @param socket the socket
     */
    public ServerConnectionHandler(Socket socket) {
        this.socket = socket;
        playerID = getNewPlayerID();
        LoggerInterface.loggerNetworking.INFO("Player ID: " + playerID);
        this.messageProtocol = new MessageProtocol(this);
    }

    /**
     * Constructs a connection from an arbitrary input and output stream
     * @param serverInputStream the input stream
     * @param serverOutputStream the output stream
     */
    public ServerConnectionHandler(InputStream serverInputStream, OutputStream serverOutputStream){
        this.local = true;
        playerID = getNewPlayerID();
        LoggerInterface.loggerNetworking.INFO("Player ID: " + playerID);
        inputStream = serverInputStream;
        outputStream = serverOutputStream;
        this.messageProtocol = new MessageProtocol(this);
    }

    @Override
    public void run() {
        LoggerInterface.loggerNetworking.INFO("ServerConnectionHandler start");
        initialized = false;


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


        //startup is different whether need to set socket streams or received in construction of object (ie if creating local "connection")
        if(this.local){
            //run if serverconnectionHandler is created by passing in input/output streams
            networkParser = new NetworkParser(inputStream,outputStream);
            messageProtocol = new MessageProtocol(this);
        } else {
            //run if ServerConnectionHandler is created by passing in a socket
            try {
                socket.setSoTimeout(100);
            } catch (SocketException ex) {
                ex.printStackTrace();
            }

            //TODO: use this commented block of code as a reference for implementing encryption on top of the game connection
            //        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();
//            System.exit(1);
//        }
//        try {
//            outputStream = new CryptoOutputStream("AES/ECB/PKCS5Padding",properties,socket.getOutputStream(),key,spec);
//        } catch (IOException ex) {
//            ex.printStackTrace();
//            System.exit(1);
//        }
            try {
                inputStream = socket.getInputStream();
                outputStream = socket.getOutputStream();
                networkParser = new NetworkParser(inputStream,outputStream);
                messageProtocol = new MessageProtocol(this);
            } catch (IOException ex) {
                ex.printStackTrace();
                LoggerInterface.loggerNetworking.ERROR("", ex);
            }
        }
        
        NetworkMessage pingMessage = ServerMessage.constructPingMessage();
        NetworkMessage authRequestMessage = AuthMessage.constructAuthRequestMessage();

        //net monitor registration
        if(Globals.netMonitor != null){
            this.netMonitorHandle = Globals.netMonitor.registerConnection();
            Globals.netMonitor.logMessage(netMonitorHandle, pingMessage, false);
            Globals.netMonitor.logMessage(netMonitorHandle, authRequestMessage, false);
        }

        networkParser.addOutgoingMessage(pingMessage);
        networkParser.addOutgoingMessage(authRequestMessage);




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


        initialized = true;
        while(Main.isRunning()){

            //
            // Main Loop
            //
            //parse messages both incoming and outgoing
            try {
                parseMessages();
            } catch (SocketException e) {
                //if we get a SocketException broken pipe (basically the client dc'd without telling us)
                //set flag to disconnect client
                //TODO: fix, this doesn't actually catch the socket exception which is exceedingly obnoxious
                socketException = true;
                LoggerInterface.loggerNetworking.DEBUG(e.getLocalizedMessage());
            }

            //
            // Pings
            //
            //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){
                addMessagetoOutgoingQueue(ServerMessage.constructPingMessage());
                lastPingTime = currentTime;
                if(lastPongTime == 0){
                    lastPongTime = lastPingTime;
                }
            }

            //
            // Disconnections
            //
            //check if we meet disconnection criteria
            //has it been too long since the last ping?
            //have we had a socket exception?
            if(lastPingTime - lastPongTime > PING_DISCONNECT_THRESHOLD || this.socketException == true){
                //disconnected from the server
                LoggerInterface.loggerNetworking.WARNING("Client disconnected");
                //run disconnect routine
                disconnect();
                break;
            }
        }
    }

    /**
     * Had to wrap the message parsing block in a function to throw a SocketException
     * without my linter freaking out
     * @throws SocketException
     */
    void parseMessages() throws SocketException {
        //
        //Read in messages
        //
        //attempt poll incoming messages
        networkParser.readMessagesIn();


        //
        //Net monitor (debug)
        //
        //net monitor
        if(Globals.netMonitor != null){
            //incoming
            netMonitorCache.clear();
            networkParser.copyIncomingMessages(netMonitorCache);
            for(NetworkMessage message : netMonitorCache){
                Globals.netMonitor.logMessage(netMonitorHandle, message, true);
            }
        }
        
        //
        //Parse messages
        //
        //ponder incoming messages
        while(networkParser.hasIncomingMessaage()){
            NetworkMessage message = networkParser.popIncomingMessage();
            this.messageProtocol.handleAsyncMessage(message);
        }


        //
        //Net monitor (debug)
        //
        if(Globals.netMonitor != null){
            //outgoing
            netMonitorCache.clear();
            networkParser.copyOutgoingMessages(netMonitorCache);
            for(NetworkMessage message : netMonitorCache){
                Globals.netMonitor.logMessage(netMonitorHandle, message, false);
            }
        }

        //
        //Send out messages
        //
        //push outgoing message
        networkParser.pushMessagesOut();
        try {
            //sleep
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Handles synchronous packets in the queue
     */
    public void handleSynchronousPacketQueue(){
        this.messageProtocol.handleSyncMessages();
    }
    
    public void setPlayerId(int id){
        playerID = id;
    }

    public int getPlayerId(){
        return playerID;
    }
    
    static int getNewPlayerID(){
        playerIdIncrementer++;
        return playerIdIncrementer;
    }

    public void setPlayerEntityId(int id){
        LoggerInterface.loggerNetworking.DEBUG("Set player(" + this.playerID + ")'s entity ID to be " + id);
        playerEntityID = id;
    }

    public int getPlayerEntityId(){
        return playerEntityID;
    }

    /**
     * Gets the player associated with this connection
     * @return The player object
     */
    public Player getPlayer(){
        return Globals.playerManager.getPlayerFromId(playerID);
    }

    public String getIPAddress(){
        if(local){
            return "127.0.0.1";
        } else {
            return socket.getRemoteSocketAddress().toString();
        }
    }
    
    public void addMessagetoOutgoingQueue(NetworkMessage message){
        networkParser.addOutgoingMessage(message);
    }

    boolean isConnectionPlayerEntity(int id){
        return id == this.playerEntityID;
    }
    
    /**
     * Returns true if running both client and server in same instance of the app and this is the player for that instance of the app
     * @return
     */
    boolean isServerClient(){
        return Globals.RUN_SERVER && Globals.RUN_CLIENT && Globals.clientPlayer != null && this.playerID == Globals.clientPlayer.getId();
    }

    public void setCreatureTemplate(CreatureTemplate currentCreatureTemplate){
        this.currentCreatureTemplate = currentCreatureTemplate;
    }

    public CreatureTemplate getCurrentCreatureTemplate(){
        return this.currentCreatureTemplate;
    }

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

    /**
     * Routine to run when the client disconnects
     */
    void disconnect(){
        //close socket
        if(socket.isConnected()){
            try {
                socket.close();
            } catch (IOException e) {
                LoggerInterface.loggerNetworking.ERROR("Error closing socket", e);
            }
        }
        //figure out what player we are
        Player playerObject = Globals.playerManager.getPlayerFromId(getPlayerId());
        //tell all clients to destroy the entity
        ServerEntityUtils.destroyEntity(playerObject.getPlayerEntity());
    }

}
