package fr.insa.clavardator.lib.network;

import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.Socket;

public class TcpConnection {

	private final int port;
	private final Object outputStreamGuard = new Object();
	private Socket socket;
	private ObjectOutputStream outputStream;
	private ObjectInputStream inputStream;
	private boolean shouldStop = false;

	/**
	 * Creates a new connection, and connects to the peer
	 *
	 * @param ipAddr        The IP address of the peer
	 * @param callback      The function to call when connected
	 * @param errorCallback The function to call on error
	 */
	public TcpConnection(InetAddress ipAddr, int port, SocketConnectedCallback callback, ErrorCallback errorCallback) {
		this.port = port;
		Connector connector = new Connector(ipAddr, callback, errorCallback);
		connector.start();
	}

	/**
	 * Creates a new connection using an already connected socket
	 *
	 * @param socket A socket already connected to the host
	 */
	public TcpConnection(Socket socket) {
		this.port = socket.getPort();
		this.socket = socket;
	}

	/**
	 * Sends a message to the peer
	 *
	 * @param message       The message to send
	 * @param callback      The function to call on success
	 * @param errorCallback The function to call on error
	 */
	public void send(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
		Sender sender = new Sender(message, callback, errorCallback);
		sender.start();
	}

	/**
	 * Subscribe to all incoming messages from the peer
	 *
	 * @param callback      The function to call when a message is received
	 * @param errorCallback The function to call on error
	 */
	public void receive(MessageReceivedCallback callback, ErrorCallback errorCallback) {
		Receiver receiver = new Receiver(callback, errorCallback, true);
		receiver.start();
	}

	/**
	 * Subscribe to the first incoming message from the peer
	 *
	 * @param callback      The function to call when a message is received
	 * @param errorCallback The function to call on error
	 */
	public void receiveOne(MessageReceivedCallback callback, ErrorCallback errorCallback) {
		Receiver receiver = new Receiver(callback, errorCallback, false);
		receiver.start();
	}

	/**
	 * Closes the socket, and silently cancels all running send and receive operations.
	 */
	public void close() {
		shouldStop = true;
		try {
			if (outputStream != null)
				outputStream.close();
			if (inputStream != null)
				inputStream.close();
			if (socket != null)
				socket.close();
		} catch (IOException ignored) {
		}
	}

	/**
	 * Checks if the current connection is open
	 *
	 * @return True if the socket is still open
	 */
	public boolean isOpen() {
		return socket != null && socket.isConnected() && !socket.isClosed();
	}

	public interface SocketConnectedCallback {
		void onSocketConnected(TcpConnection connection);
	}


	public interface MessageReceivedCallback {
		void onMessageReceived(Object msg) throws IOException;
	}

	private class Connector extends Thread {
		private final InetAddress ipAddr;
		private final SocketConnectedCallback callback;
		private final ErrorCallback errorCallback;

		public Connector(InetAddress ipAddr, SocketConnectedCallback callback, @Nullable ErrorCallback errorCallback) {
			this.ipAddr = ipAddr;
			this.callback = callback;
			this.errorCallback = errorCallback;
		}

		@Override
		public void run() {
			try {
				if (socket != null) {
					close();
				}
				socket = new Socket(ipAddr, port);
				callback.onSocketConnected(TcpConnection.this);
			} catch (IOException e) {
				if (errorCallback != null)
					errorCallback.onError(e);
			}
		}
	}

	private class Sender extends Thread {
		private final Serializable message;
		private final SimpleCallback callback;
		private final ErrorCallback errorCallback;

		/**
		 * Constructs a thread that sends a message using the socket of the outer class
		 *
		 * @param message       The message to send
		 * @param callback      The function to call on success
		 * @param errorCallback The function to call on error
		 */
		public Sender(Serializable message, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
			this.message = message;
			this.callback = callback;
			this.errorCallback = errorCallback;
		}

		@Override
		public void run() {
			try {
				synchronized (outputStreamGuard) {
					if (outputStream == null) {
						outputStream = new ObjectOutputStream(socket.getOutputStream());
					}
					outputStream.writeObject(message);
				}
				if (callback != null)
					callback.call();
			} catch (IOException e) {
				if (errorCallback != null && !shouldStop)
					errorCallback.onError(e);
			}
		}
	}

	private class Receiver extends Thread {
		private final MessageReceivedCallback callback;
		private final ErrorCallback errorCallback;
		private final boolean loop;

		/**
		 * Constructs a thread that receives a message using the socket of the outer class
		 *
		 * @param callback      The function to call on success
		 * @param errorCallback The function to call on error
		 */
		public Receiver(@NotNull MessageReceivedCallback callback, @Nullable ErrorCallback errorCallback, boolean loop) {
			this.callback = callback;
			this.errorCallback = errorCallback;
			this.loop = loop;
		}

		@Override
		public void run() {
			try {
				if (inputStream == null) {
					inputStream = new ObjectInputStream(socket.getInputStream());
				}
				do {
					callback.onMessageReceived(inputStream.readObject());
				} while (!shouldStop && loop);
			} catch (IOException | ClassNotFoundException e) {
				if (errorCallback != null && !shouldStop)
					errorCallback.onError(e);
			}
		}
	}
}
