package fr.insa.clavardator.server;

import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.network.TcpListener;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.lib.util.Log;

import java.io.EOFException;
import java.io.Serializable;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class Presence {

	private final Map<String, TcpConnection> subscribers = new HashMap<>();
	private final ArrayList<UserInformation> connectedUsers = new ArrayList<>();
	private final TcpListener presenceListener;

	public Presence(int port) {
		presenceListener = new TcpListener();
		presenceListener.acceptConnection(
				port,
				this::subscribe,
				e -> Log.e(getClass().getSimpleName(), "Error while registering a user", e));
	}

	public void publish(UserInformation info) {
		ArrayList<UserInformation> msg = new ArrayList<>(1);
		msg.add(info);
		for (Map.Entry<String, TcpConnection> entry : subscribers.entrySet()) {
			// Do not send update to self
			if (!entry.getKey().equals(info.id)) {
				entry.getValue().send(msg, null,
						e -> Log.e(getClass().getSimpleName(), "Error while publishing user information", e));
			}
		}
	}

	private void updateUser(UserInformation userInformation) {
		connectedUsers.removeIf(ui -> userInformation.id.equals(ui.id));
		connectedUsers.add(userInformation);
	}

	private void adduser(UserInformation userInformation, TcpConnection userConnection) {
		subscribers.put(userInformation.id, userConnection);
		connectedUsers.add(userInformation);
	}

	public void subscribe(Socket socket) {
		TcpConnection userConnection = new TcpConnection(socket);

		// Receive user information
		userConnection.receiveOne(o -> {
			if (o instanceof UserInformation) {
				UserInformation userInformation = ((UserInformation) o);
				Log.v(getClass().getSimpleName(), "Registering user " + userInformation.id +
						" (" + userInformation.getUsername() + ")");

				// publish new user info to all subscribers
				publish(userInformation);

				// Send the list of connected users to the new subscriber. We're cloning it because we're modifying it
				// just after this call, while the sender thread might not have send it yet
				userConnection.send((Serializable) connectedUsers.clone(), null,
						e -> Log.e(getClass().getSimpleName(), "Error while receiving user information", e));

				adduser(userInformation, userConnection);

				userConnection.receive(msg -> {
							if (msg instanceof UserInformation) {
								UserInformation newUserInformation = ((UserInformation) msg);
								Log.v(getClass().getSimpleName(), "Receiving user status update " + newUserInformation.id +
										" (" + newUserInformation.getUsername() + ")");
								updateUser(newUserInformation);
								publish(newUserInformation);
							}
						},
						e -> {
							if (e instanceof EOFException) {
								unsubscribe(userInformation);
							}
						});
			} else {
				Log.e(getClass().getSimpleName(), "Received unexpected message type: " + o.getClass().getSimpleName());
			}
		}, e -> {
			if (!(e instanceof EOFException)) {
				Log.e(getClass().getSimpleName(), "Error while receiving user information", e);
			}
		});
	}

	public void unsubscribe(UserInformation userInformation) {
		Log.v(getClass().getSimpleName(), "Unsubscribing user " + userInformation.id + "(" + userInformation.getUsername() + ")");
		subscribers.get(userInformation.id).close();
		subscribers.remove(userInformation.id);
		publish(new UserInformation(userInformation.id, userInformation.getUsername(), UserState.DISCONNECTED));
		connectedUsers.removeIf(ui -> userInformation.id.equals(ui.id));
	}

	public void stop() {
		presenceListener.stopAccepting();
		for (TcpConnection connection : subscribers.values()) {
			connection.close();
		}
	}
}
