package fr.insa.clavardator.client.server.presence;

import fr.insa.clavardator.client.server.proxy.InsaProxy;
import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import fr.insa.clavardator.lib.util.ParametrizedCallback;
import fr.insa.clavardator.lib.util.SimpleCallback;
import org.jetbrains.annotations.Nullable;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * Handles connection to the default INSA Clavardator presence server.
 * <br/>
 * This server does not uses HTTP, everything is done using sockets.
 * The presence server and the proxy each live on a different port.
 * <br/>
 * The client needs to initiate a TCP connection on each port.
 * <br/>
 * On the presence server port, the client will be able to send presence messages
 * such as subscribe, unsubscribe, publish, and receive notifications.
 * <br/>
 * On the proxy port, the client will be able to send regular
 * {@link Message messages} like on the local network.
 * The proxy will forward the message to the appropriate recipient using the provided id.
 */
public class InsaPresence implements Presence {

	private final String path;
	private final int presencePort;
	private final Proxy proxy;
	private TcpConnection presenceConnection;

	public InsaPresence(String path, int presencePort, int proxyPort) {
		this.path = path;
		this.presencePort = presencePort;
		this.proxy = new InsaProxy(path, proxyPort);
	}

	@Override
	public void subscribe(UserInformation userInformation, ParametrizedCallback<ArrayList<UserInformation>> callback, ErrorCallback errorCallback) {
		if (!isConnected()) {
			connectToPresence(
					() -> proxy.connect(() -> {
						notify(userInformation, null, errorCallback);
						receiveSubscribeNotifications(callback, errorCallback);
					}, errorCallback),
					errorCallback);
		} else {
			Log.v(getClass().getSimpleName(), "Already subscribed to presence server");
		}
	}

	/**
	 * Waits for presence server response to the subscribe request
	 *
	 * @param callback      Called when the message is successfully received
	 * @param errorCallback Called on connection error
	 */
	private void receiveSubscribeNotifications(ParametrizedCallback<ArrayList<UserInformation>> callback, @Nullable ErrorCallback errorCallback) {
		presenceConnection.receive(
				msg -> {
					final ArrayList<UserInformation> userList = new ArrayList<>();
					if (msg instanceof ArrayList<?>) {
						final ArrayList<?> list = (ArrayList<?>) msg;
						for (Object obj : list) {
							if (obj instanceof UserInformation) {
								userList.add((UserInformation) obj);
							}
						}
						Log.v(getClass().getSimpleName(), "Receive subscribe response: " + msg);
						Log.v(getClass().getSimpleName(), "Converted list: " + Arrays.toString(userList.toArray()));
						if (callback != null) {
							callback.call(userList);
						}
					} else {
						Log.e(getClass().getSimpleName(), "Unexpected object received: " + msg.getClass().getSimpleName());
						if (errorCallback != null) {
							errorCallback.onError(new RuntimeException("Unexpected object received"));
						}
					}
				}, errorCallback);
	}

	@Override
	public void unsubscribe(SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
		if (isConnected()) {
			disconnect();
		} else {
			Log.v(getClass().getSimpleName(), "Not subscribed to presence server");
		}
	}

	@Override
	public void notify(UserInformation newInformation, @Nullable SimpleCallback callback, @Nullable ErrorCallback errorCallback) {
		presenceConnection.send(newInformation, callback, errorCallback);
	}

	/**
	 * Connects to the presence server by TCP
	 *
	 * @param callback      Called when connection is successful
	 * @param errorCallback Called on connection error
	 */
	private void connectToPresence(SimpleCallback callback, ErrorCallback errorCallback) {
		try {
			presenceConnection = new TcpConnection(InetAddress.getByName(path),
					presencePort,
					(newConnection) -> {
						if (callback != null) {
							callback.call();
						}
					},
					errorCallback);
		} catch (UnknownHostException e) {
			Log.e(getClass().getSimpleName(), "Could not connect to presence server", e);
		}
	}

	/**
	 * Closes the given connection
	 */
	private void closeConnection(@Nullable TcpConnection c) {
		if (c != null && c.isOpen()) {
			c.close();
		}
	}

	/**
	 * Checks if we are connected to the presence server and proxy
	 *
	 * @return True only if we are connected to both presence server and proxy
	 */
	private boolean isConnected() {
		return presenceConnection != null &&
				presenceConnection.isOpen() &&
				proxy.isConnected();
	}

	/**
	 * Disconnects from both presence server and proxy
	 */
	private void disconnect() {
		Log.v(this.getClass().getSimpleName(), "Disconnecting presence server");
		closeConnection(presenceConnection);
		presenceConnection = null;
		proxy.close();
	}

	@Override
	public Proxy getProxy() {
		return proxy;
	}
}
