package robombs.clientserver;

import java.net.*;
import java.io.*;
import java.util.*;

/**
 * This the client's counterpart to the SimpleServer. If connects to a SimpleServer and exchanges DataContainers with it.
 */
public class SimpleClient {

    private boolean exit = false;
    private List<DataTransferListener> listener = new ArrayList<DataTransferListener>();
    private List<ClientPreProcessor> pres = new ArrayList<ClientPreProcessor>();
    private DataContainer[] nextData = null;
    private int myID = -1;

    private String server = null;
    private int port = 0;
    private boolean zip = false;
    private Sender sender = null;
    private Thread senderThread=null;
    private boolean connected = false;
    private DataContainer initialDG = null;
    private volatile boolean waitingForData=false;

    private PerformanceCounter pc = new PerformanceCounter("Client");

    private final static Object SYNC = new Object();

    /**
     * Creates a new SimpleClient. The client won't connect to the server unless the connect()-method has been called.
     * @param server the address of the server. This can be IPv4, IPv6 or a name.
     * @param port the port on which the server is listening for clients to connect
     * @param zip if true, all transfers (except for the login itself) will be zipped. This consumes more cpu power but reduces bandwidth usage.
     * @param data an initial DataContainer. This container will be passed to the registered ClientLoginListeners (if any) by the server. The SimpleServer itself won't process it.
     */
    public SimpleClient(String server, int port, boolean zip, DataContainer data) {
        this.server = server;
        this.port = port;
        this.zip = zip;
        this.initialDG = data;
    }

    /**
     * Creates a new SimpleClient. The client won't connect to the server unless the connect()-method has been called.
     * @param se the ServerEntry (usually taken from the ServerBrowser) that holds the server's data.
     * @param zip if true, all transfers (except for the login itself) will be zipped. This consumes more cpu power but reduces bandwidth usage.
     * @param data an initial DataContainer. This container will be passed to the registered ClientLoginListeners (if any) by the server. The SimpleServer itself won't process it.
     */
    public SimpleClient(ServerEntry se, boolean zip, DataContainer data) {
        this(se.getAddress().getHostName(), se.getPort(), zip, data);
    }

    /**
     * Tries to connect to the configured server. If that fails, an Exception will be thrown.
     * @throws Exception failed?
     */
    public void connect() throws Exception {
        sender = new Sender();
        senderThread=new Thread(sender);
        senderThread.start();
    }

    /**
     * Disconnects from a server. If there is no connection established, nothing will be done.
     */
    public void disconnect() {
        if (sender != null) {
            sender.disconnect();
        }
        sender = null;
    }
    
    public String getServer() {
    	String ip=sender.conn.toString();
    	int pos=ip.indexOf("/");
    	if (pos!=-1) {
    		ip=ip.substring(pos+1);
    	}
    	return ip.trim();
    }

    /**
     * Adds a DataTransferListener to the client. This listener will be called when the client receives data from the server.
     * @param sl the listener
     */
    public void addListener(DataTransferListener sl) {
        listener.add(sl);
    }

    /**
     * Adds a ClientPreProcessor to the client. A ClientPreProcessor will be called before sending to the server, before receiving
     * from the server and after receiving from the server to allow its implementing class to do whats needed in these stages. For example,
     * "before sending" should prepare the data to be send to the server and inject it into the SimpleClient.
     * @param cpp ClientPreProcessor
     */
    public void addPreProcessor(ClientPreProcessor cpp) {
        pres.add(cpp);
    }

    /**
     * Sets the data to be transfered to the server in the next transfer. This is usually called from inside an implementation
     * of a ClientPreProcessor's beforeSending()-method.
     * @param c an array of DataContainers
     */
    public void setContainers(DataContainer[] c) {
       synchronized (SYNC) {
           nextData = c;
       }
       if (waitingForData) {
    	   // Already waiting for data? Interrupt that...
    	   senderThread.interrupt();
       }
   }

   /**
    * Returns this client's id. Once connected, this is a positive integer that is unique on the server.
    * @return int the id
    */
   public int getClientID() {
        return myID;
    }

    /**
     * Is this client connected to a server?
     * @return boolean is it?
     */
    public boolean isConnected() {
        return connected;
    }
    
    /**
     * Make the client thread process new data regardless if he's waiting or not...
     * This increases network load but smoothes movement of remote entities.
     */
    public void triggerTransfer() {
    	senderThread.interrupt();
    }

    /**
     * The working thread for a client. In this thread, data is being sent and received to/from the server.
     */
    private class Sender implements Runnable {

        private Socket conn = null;
        private InputStream is = null;
        private OutputStream os = null;
        private boolean disconnect = false;

        /**
         * Connects to the server with the configured address/port.
         * @throws Exception if the connection fails, the client thread fails...
         */
        public Sender() throws Exception {
            NetLogger.log("Trying to connect to " + server + ":" + port);
            conn = new Socket(server, port);
            conn.setSoTimeout(10000);
            conn.setTcpNoDelay(NetGlobals.lowLatency);
            conn.setPerformancePreferences(0, 2, 1);
            os = conn.getOutputStream();
            is = conn.getInputStream();
            connect();
        }

        /**
         * Disconnects the client from the server. This is done sending a logout request to the server and then
         * terminating the thread. If the server gets the request, it can't vote against it, i.e. the client
         * will be disconnected in every case.
         */
        private void disconnect() {
            disconnect = true;
        }

        /**
         * Tries to disconnect from the server. If this isn't possible, this is not an error. Maybe it has already been
         * done before or the server is dead...
         */
        private void disconnectInternal() {
            try {
                NetLogger.log("Trying to disconnect from " + server + ":" + port);
                DataContainer c = new DataContainer();
                c.setMessageType(MessageTypes.LOGOUT_REQUEST);
                os.write(DataContainerFactory.toByteArray(new DataContainer[] {c}, false));
                os.flush();
                c = new DataContainer(StreamConverter.convert(is));
                if (c.getMessageType() == MessageTypes.LOGOUT_SUCCESS) {
                    NetLogger.log("Client: Disconnected!");
                } else {
                    NetLogger.log("Client: Unexpected server response!");
                }
            } catch (Exception e) {
                NetLogger.log("Client: Unable to disconnect (already disconnected?)!");
            }
            exit = true;
        }

        /**
         * Connects to the server
         * @throws Exception
         */
        private void connect() throws Exception {
            boolean con = false;
            myID = -1;

            while (!con) {
                DataContainer c = new DataContainer();
                c.setMessageType(MessageTypes.LOGIN_REQUEST);
                if (zip) {
                    c.add((byte) 1);
                } else {
                    c.add((byte) 0);
                }

                DataContainer[] cont = null;
                if (initialDG != null) {
                    cont = new DataContainer[] {c, initialDG};
                } else {
                    cont = new DataContainer[] {c};
                }

                os.write(DataContainerFactory.toByteArray(cont, false));
                c = new DataContainer(StreamConverter.convert(is));

                boolean ok = false;
                if (c.getMessageType() == MessageTypes.LOGIN_SUCCESS) {
                    ok = true;
                    con = true;
                    myID = c.getNextInt();
                    pc = new PerformanceCounter("Client "+myID);
                    NetLogger.log("Client: Logged in - ID is: " + myID + "!");
                    connected = true;
                }
                if (!ok) {
                    NetLogger.log("Client: Error logging in!");
                }
                if (!con) {
                    // retry!
                    Thread.sleep(200);
                }
            }
        }

        public void run() {
        	Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            try {
            	while (!exit) {
            		long start=System.nanoTime();
                    pc.printStats();
                    synchronized (SYNC) {
                        // Call the PreProcessors. It's most likely that those will create the Packet to
                        // send or something...
                        for (ClientPreProcessor cpp:pres) {
                            cpp.beforeSending();
                        }
                    }
                    byte[] bytes = null;

                    synchronized (SYNC) {
                        if (nextData != null) {
                            bytes = DataContainerFactory.toByteArray(nextData, zip);
                            nextData=null;
                        } 
                    }

                    DataContainer c = null;
                    DataContainer[] cs = null;

                    if (bytes != null) {
                        if (!disconnect) {
                            os.write(bytes);
                            pc.out(bytes.length);
                            os.flush();
                            boolean ok = true;
                            byte[] res = null;

                            try {
                                synchronized (SYNC) {
                                    for (ClientPreProcessor cpp:pres) {
                                        cpp.beforeReceiving();
                                    }
                                }
                                
                                res = StreamConverter.convert(is);
                                if (res == null || res.length == 0) {
                                    NetLogger.log("Client: Got a zero-sized response from server. Trying to disconnect now!");
                                    disconnectInternal();
                                    ok = false;
                                }
                                pc.in(res.length);
                                
                            } catch (Exception e) {
                            	e.printStackTrace();
                                NetLogger.log("Client: Server error (server running?)!");
                                disconnectInternal();
                                ok = false;
                            }
                            /*
                            if (Globals.emulateRemoteServer) {
                    			try {
                    				Thread.sleep(30);
                    			} catch(Exception e){}
                    		}*/
                            
                            if (ok) {
                                cs = DataContainerFactory.extractContainers(res, zip);
                                
                                for (int i = 0; i < cs.length; i++) {
                                    c = cs[i];
                                    int type = c.getMessageType();
                                    for (DataTransferListener sl:listener) {
                                        sl.dataReceived(c, type);
                                    }
                                }

                                for (DataTransferListener sl:listener) {
                                    sl.dataReceivedEnd();
                                }

                                synchronized (SYNC) {
                                    for (ClientPreProcessor cpp:pres) {
                                        cpp.afterReceiving();
                                    }
                                }
                            }
                        } else {
                            disconnectInternal();
                        }
                        long end=System.nanoTime();
                        end=(end-start)/1000000L;
                        
                        if (end>250) {
                        	NetLogger.log("Client: Client lags ("+end+"ms)!");
                        }
                        
                        long st=Math.min(NetGlobals.clientWaitTime, Math.max(0,NetGlobals.clientWaitTime-end));
                        if (st>=0) {
                        	try {
                        		Thread.sleep(st);
                        	} catch(Exception e) {
                        		pc.interrupted();
	                        	// This is intentionally
	                        }
                        } else {
                        	Thread.yield();
                        }
                    } else {
                    	if (!disconnect) {
	                    	// This branch is entered only, if the client doesn't return any data to
	                    	// send. This happens almost never...
	                    	waitingForData=true;
	                    	try {
	                    		Thread.sleep(10);
	                    	} catch(Exception e) {
	                        	// This is intentionally
	                        }
	                    	waitingForData=false;
                    	} else {
                    		disconnectInternal();
                    	}
                    }
                }
                is.close();
                os.close();
                conn.close();
                connected = false;
                NetLogger.log("Client: Network thread terminated!");
            } catch (Exception e) {
            	exit=true;
                throw new RuntimeException(e);
            }
        }
    }
}
