import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Provides an Interface to communicate using a socket asynchronously
 * @author Luca Conte
 */
public class AsyncSocket {
	private Socket socket;
	private Thread checkerThread;
	private Thread connectorThread;
	private AsyncSocketListener handler;
	private boolean shouldStop = false;

	private String sendBuffer = "";

	private BufferedReader in;
	private BufferedWriter out;

	/**
	 * Creates a socket server and turns the first incoming connection into an AsyncSocket
	 * @param port the port to listen on for a connection
	 * @param handler the handler that will be called when a message is received
	 * @author Luca Conte
	 */
	public AsyncSocket(int port, AsyncSocketListener handler) {
		this.setHandler(handler);

		// start server in new thread
		this.connectorThread = new Thread(() -> {
			try {
				ServerSocket serverSocket = new ServerSocket(port);

				System.out.println("Waiting for client connection on port " + port);

				Socket socket = serverSocket.accept();

				System.out.println("Socket connected.");

				serverSocket.close();

				this.initSocket(socket);

			} catch (IOException e) {
				e.printStackTrace();
				// TODO: proper error handling
			}
		});
		this.connectorThread.start();
	}

	/**
	 * Connects to the address provided and turns the resulting socket into an AsyncSocket
	 * @param address the socket address to connect to
	 * @param handler the handler that will be called when a message is received
	 * @author Luca Conte
	 */
	public AsyncSocket(InetSocketAddress address, AsyncSocketListener handler) {
		this.setHandler(handler);

		// start client in new thread
		this.connectorThread = new Thread(() -> {
			System.out.println("Connecting to " + address.toString());

			Socket socket = new Socket();
			try {
				socket.connect(address, 10);
				System.out.println("Socket connected.");

				this.initSocket(socket);

			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException("Connection timed out");
			}

		});
		this.connectorThread.start();
	}

	/**
	 * @param socket the socket to be wrapped in an AsyncSocket
	 * @param handler the handler that will be called when a message is received
	 * @author Luca Conte
	 */
	public AsyncSocket(Socket socket, AsyncSocketListener handler) throws IOException {
		this.setHandler(handler);
		this.initSocket(socket);
	}

	/**
	 * creates input and ouput writer / readers as well as a checker thread to repeatedly check
	 * for incoming messages
	 * @param socket the socket to be wrapped
	 * @author Luca Conte
	 */
	private void initSocket(Socket socket) throws IOException {
		System.out.println("Initialising sockets");
		this.socket = socket;

		this.in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
		this.out = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));

		this.shouldStop = false;
		this.checkerThread = new Thread(() -> {
			while (!this.shouldStop) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

				if (!this.connectorThread.isAlive()) {
					try {
						this.connectorThread.join();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}

				try {
					if (!this.in.ready()) {
						continue;
					}
					if (this.handler == null) {
						continue;
					}

					String message = this.in.readLine();
					if (message.length() <= 0) continue;


					message = message.strip();
					System.out.println("RECEIVED - " + message);
					this.handler.receive(message);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		});
		System.out.println("starting checker thread");
		this.checkerThread.start();
		this.flushBuffer();
	}

	/**
	 * sets the message handler for the async socket
	 * @param handler the `AsyncSocketListener` to be set as the new handler
	 * @author Luca Conte
	 */
	public void setHandler(AsyncSocketListener handler) {
		this.handler = handler;
	}


	/**
	 * sends a message through the socket
	 * @param socketPackage the socket package to be sent
	 * @author Luca Conte
	 */
	public synchronized void send(SocketPackage socketPackage) {
		this.sendLine(socketPackage.toString());
	}
	/**
	 * sends a message through the socket
	 * @param packageName the name of the package to be sent
	 * @author Luca Conte
	 */
	public synchronized void send(String packageName) {
		this.send(packageName, "");
	}
	/**
	 * sends a message through the socket
	 * @param packageName the name of the package to be sent
	 * @param packageContent the content of the package to be sent.
	 * `packageName` and `packageContent` are joined with a space " "
	 * @author Luca Conte
	 */
	public synchronized void send(String packageName, String packageContent) {
		if (packageContent.length() > 0) {
			packageContent = " " + packageContent;
		}
		this.sendLine(packageName + packageContent);
	}

	/**
	 * sends a string of text into the socket, concatenated with CRLF
	 * @author Luca Conte
	 */
	public synchronized void sendLine(String message) {
		sendBuffer = sendBuffer + message + "\r\n";
		this.flushBuffer();
	}

	/**
	 * flushes the buffers to send all pending messages
	 * @author Luca Conte
	 */
	private synchronized void flushBuffer() {
		if (!this.sendBuffer.isEmpty() && this.out != null) {
			try {
				this.out.write(sendBuffer);
				System.out.println("SENT - " + sendBuffer);
				sendBuffer = "";
				this.out.flush();
			} catch (IOException e) {
				e.printStackTrace();
				// TODO: handle writing error
			}
		}
	}

	/**
	 * closes the socket connection and removes the checker thread
	 * @author Luca Conte
	 */
	public void close() {
		this.shouldStop = true;
		
		try {
			this.socket.close();
			if (this.checkerThread != null) this.checkerThread.join();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}