package fr.insa.clavardator.client.users;

import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.client.network.NetDiscoverer;
import fr.insa.clavardator.client.network.PeerHandshake;
import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.lib.network.TcpListener;
import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;

public class UserList {
	// List kept in sync with the running thread
	private final Map<String, PeerUser> userHashmap = new HashMap<>();
	// List kept in sync with ui thread, may be out out date with rapid interactions
	// Use only for display, not for duplicate checking
	private final ObservableList<PeerUser> userObservableList = FXCollections.observableArrayList();
	private final DatabaseController db = new DatabaseController();
	private final NetDiscoverer netDiscoverer = new NetDiscoverer();
	private final TcpListener tcpListener = new TcpListener();

	public UserList() {
	}

	/**
	 * Discovers all active users over the network
	 * and create a connection for each.
	 *
	 * @param errorCallback The function to call on error
	 */
	public void discoverActiveUsers(int port, ErrorCallback errorCallback) {
		netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> {
			Log.v(this.getClass().getSimpleName(), "Discovered new user: " + data);
			new PeerHandshake().createConnection(
					ipAddr,
					port,
					this::onUserConnectionSuccess,
					errorCallback);
		}, errorCallback);
	}

	public void onReceivePresenceNotification(ArrayList<UserInformation> newPresenceUsers, Proxy proxy) {
		newPresenceUsers.forEach((userInfo -> {
			final PeerUser savedUser = userHashmap.get(userInfo.id);
			if (savedUser != null) {
				if (savedUser.isActive()) {
					if (userInfo.getState() == UserState.DISCONNECTED) {
						Log.v(getClass().getSimpleName(), "Received disconnected user from presence server was already known and connected, disconnecting...");
						savedUser.disconnect();
					} else {
						Log.v(getClass().getSimpleName(), "Received user from presence server was already known and connected, updating info...");
						savedUser.setState(userInfo.getState());
						savedUser.setUsername(userInfo.getUsername());
					}
				} else {
					if (userInfo.getState() == UserState.CONNECTED) {
						Log.v(getClass().getSimpleName(), "Received user from presence server was already known but not connected, connecting...");
						savedUser.init(proxy, userInfo.id, userInfo.getUsername(),
								e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
					} else {
						Log.v(getClass().getSimpleName(), "Received disconnected user from presence server was already known and disconnected.");
					}
				}
			} else {
				if (userInfo.getState() == UserState.CONNECTED) {
					Log.v(getClass().getSimpleName(), "Received new connected user from presence server");
					final PeerUser user = new PeerUser();
					user.init(proxy, userInfo.id, userInfo.getUsername(),
							e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
					userHashmap.put(user.getId(), user);
					Platform.runLater(() -> userObservableList.add(user));
				} else {
					Log.v(getClass().getSimpleName(), "Received new disconnected user from presence server");
					final PeerUser user = new PeerUser(userInfo.id, userInfo.getUsername());
					userHashmap.put(user.getId(), user);
					Platform.runLater(() -> userObservableList.add(user));
				}
			}
		}));
	}

	/**
	 * Starts listening for other broadcasts.
	 * Only answers and waits for them to initiate the connection.
	 */
	public void startDiscoveryListening() {
		netDiscoverer.startDiscoveryListening(
				CurrentUser.getInstance().getId(),
				null,
				Throwable::printStackTrace);
	}

	public void retrievedPreviousUsers(UserListLoadedCallback onFinish, ErrorCallback errorCallback) {
		db.getAllUsers(users -> {
			users.forEach(user -> createNewInactiveUser(user.getId(), user.getUsername()));
			onFinish.onLoaded();
		}, errorCallback);
	}

	/**
	 * Starts listening to user connection requests.
	 *
	 * @param errorCallback Callback on error
	 */
	public void startUserListening(int port, ErrorCallback errorCallback) {
		tcpListener.acceptConnection(
				port,
				(clientSocket) -> {
					Log.v(this.getClass().getSimpleName(),
							"new connection from user at address: " + clientSocket.getInetAddress().toString());
					new PeerHandshake().acceptConnection(clientSocket, this::onUserConnectionSuccess, errorCallback);
				},
				errorCallback);
	}

	/**
	 * Notify observers and subscribe to user changes.
	 */
	private void onUserConnectionSuccess(PeerHandshake handshake) {
		// Check if we already know this user
		final UserInformation userInfo = handshake.getUserInformation();
		final PeerUser savedUser = userHashmap.get(userInfo.id);
		if (savedUser != null) {
			savedUser.init(handshake.getConnection(), userInfo.id, userInfo.getUsername(),
					e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));

		} else {
			final PeerUser user = new PeerUser();
			user.init(handshake.getConnection(), userInfo.id, userInfo.getUsername(),
					e -> Log.e(getClass().getSimpleName(), "Error with user " + userInfo.getUsername(), e));
			userHashmap.put(user.getId(), user);
			Platform.runLater(() -> userObservableList.add(user));
		}
	}

	/**
	 * Creates a new user from its id and username and put it in inactive user list.
	 * We first try to fetch it from active and inactive users to prevent duplicates.
	 * We DO notify observers as the created user has all the information needed.
	 *
	 * @param id       The new user's id.
	 * @param username The new user's username.
	 */
	private void createNewInactiveUser(String id, String username) {
		final PeerUser user = new PeerUser(id, username);
		userHashmap.put(id, user);
		Platform.runLater(() -> userObservableList.add(user));
	}

	/**
	 * Tests locally if a username is available
	 *
	 * @param username The username to test
	 * @return True if the username is available
	 */
	public boolean isUsernameAvailable(String username) {
		Predicate<User> usernameEqual = user -> user.getUsername() != null && user.getUsername().equals(username);
		return userHashmap.values().stream().noneMatch(usernameEqual);
	}

	/**
	 * Tells all active users that our username changed
	 */
	public void propagateUsernameChange() {
		userHashmap.forEach((id, user) -> user.sendCurrentUser(Throwable::printStackTrace));
	}

	/**
	 * Closes all running threads, sockets and db connection.
	 * Must be called before exiting the app.
	 */
	public void destroy() {
		netDiscoverer.stopDiscovery();
		tcpListener.stopAccepting();
		db.close();
		for (PeerUser user : userHashmap.values()) {
			user.disconnect();
		}
	}

	public ObservableList<PeerUser> getUserObservableList() {
		return userObservableList;
	}

	public interface UserListLoadedCallback {
		void onLoaded();
	}
}
