package fr.insa.clavardator.client.ui;

import com.jfoenix.controls.JFXSnackbar;
import fr.insa.clavardator.client.config.Config;
import fr.insa.clavardator.client.config.ConfigLoader;
import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.client.server.ServerType;
import fr.insa.clavardator.client.server.presence.Presence;
import fr.insa.clavardator.client.server.presence.PresenceFactory;
import fr.insa.clavardator.client.server.presence.UnknownPresenceException;
import fr.insa.clavardator.client.ui.chat.ChatController;
import fr.insa.clavardator.client.ui.dialogs.AboutDialogController;
import fr.insa.clavardator.client.ui.dialogs.EditUsernameDialogController;
import fr.insa.clavardator.client.ui.dialogs.SnackbarController;
import fr.insa.clavardator.client.ui.users.UserListController;
import fr.insa.clavardator.client.users.CurrentUser;
import fr.insa.clavardator.client.users.UserList;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.util.Log;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.layout.StackPane;

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;

public class MainController implements Initializable {

	private final CurrentUser currentUser;
	private final int DEFAULT_PORT = 31598;
	@FXML
	private StackPane root;
	@FXML
	private StackPane mainContainer;
	@FXML
	private ToolbarController toolbarController;
	@FXML
	private EditUsernameDialogController editUserDialogController;
	@FXML
	private AboutDialogController aboutDialogController;
	@FXML
	private UserListController listController;
	@FXML
	private ChatController chatController;
	@FXML
	private LoadingScreenController loadingController;
	@FXML
	private ErrorScreenController errorController;
	private JFXSnackbar snackbar;
	private UserList userList;
	private Presence presenceServer;
	private boolean online;
	private int port = DEFAULT_PORT;
	private boolean localEnabled = true;

	public MainController() {
		online = false;
		currentUser = CurrentUser.getInstance();
		currentUser.addObserver(propertyChangeEvent -> {
			final String propertyName = propertyChangeEvent.getPropertyName();
			if (propertyName.equals("state")) {
				final CurrentUser.State newState = (CurrentUser.State) propertyChangeEvent.getNewValue();
				onCurrentUserStateChange(newState);
			} else if (propertyName.equals("username")) {
				final String newUsername = (String) propertyChangeEvent.getNewValue();
				onCurrentUserNameChange(newUsername);
			}
		});
	}

	private void onCurrentUserNameChange(String newUsername) {
		if (online) {
			if (userList != null) {
				userList.propagateUsernameChange();
			}
			if (presenceServer != null) {
				presenceServer.notify(
						new UserInformation(CurrentUser.getInstance().getId(), newUsername),
						null,
						null);
			}
		}
	}

	/**
	 * If the current user becomes valid, start the chat.
	 * If it is invalid or not set, show the login screen.
	 * <p>
	 *
	 * @param newState The new user state
	 */
	private void onCurrentUserStateChange(CurrentUser.State newState) {
		if (newState == CurrentUser.State.VALID)
			startChat();
		else if (newState != CurrentUser.State.UNINITIALIZED) {
			final boolean isError = newState == CurrentUser.State.INVALID;
			if (isError) {
				endChat();
			}
			Platform.runLater(() -> showLogin(isError));
		}
	}

	/**
	 * Opens a dialog allowing the user to set its username.
	 * Depending on the mode, the behavior is different.
	 * <ul>
	 *   <li>
	 *     On initial mode, canceling the dialog exists the app.
	 *     Any other mode shows the chat.
	 *   </li>
	 *   <li>
	 *     On edit mode, confirming the dialog shows a snackbar.
	 *   </li>
	 * </ul>
	 *
	 * @param mode The dialog mode
	 */
	private void openEditUsernameDialog(EditUsernameDialogController.Mode mode) {
		final boolean initialMode = mode == EditUsernameDialogController.Mode.INITIAL;
		editUserDialogController.setOnCancelListener(() -> {
			if (initialMode) {
				System.exit(0);
			} else {
				showChat();
			}
		});
		editUserDialogController.setOnSuccessListener(() -> {
			if (mode == EditUsernameDialogController.Mode.EDIT) {
				showSnackbarEvent("Nom d'utilisateur changé !", SnackbarController.Mode.SUCCESS);
			}
		});
		Platform.runLater(() -> editUserDialogController.show(root, mode));
	}

	/**
	 * Shows a snackbar
	 *
	 * @param text The text to show
	 * @param mode The mode to use
	 */
	private void showSnackbarEvent(String text, SnackbarController.Mode mode) {
		try {
			final FXMLLoader loader = new FXMLLoader(getClass().getResource("dialogs/snackbar.fxml"));
			snackbar.enqueue(new JFXSnackbar.SnackbarEvent(
					loader.load()
			));
			final SnackbarController controller = loader.getController();
			controller.setText(text);
			controller.setMode(mode);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Opens the about dialog
	 */
	private void openAboutDialog() {
		aboutDialogController.show(root);
	}

	/**
	 * Sends a broadcast over the network to discover active users.
	 * If any error happens, disconnect the user from chat and ask for reconnection.
	 */
	private void discoverActiveUsers() {
		if (userList != null && online && localEnabled) {
			userList.discoverActiveUsers(
					port,
					(e) -> {
						Log.e(this.getClass().getSimpleName(), "Error discovering users", e);
						CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
					});
		}
	}

	/**
	 * Start listening to broadcast and chat requests.
	 * If any error happens, disconnect the user from chat and ask for reconnection.
	 */
	private void startListening() {
		if (userList != null && online && localEnabled) {
			userList.startDiscoveryListening();
			userList.startUserListening(
					port,
					(e) -> {
						Log.e(this.getClass().getSimpleName(), "Error listening to users", e);
						CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
					});
		}
	}

	/**
	 * Starts network related functions to allow for chat functionality.
	 */
	private void startChat() {
		online = true;
		listController.setRefreshButtonEnabled(true);
		Log.v(this.getClass().getSimpleName(), "Chat started");
		discoverActiveUsers();
		startListening();
		subscribeToPresenceServer();
		Platform.runLater(this::showChat);
	}

	/**
	 * Stops any network related functions to disable chat functionality.
	 */
	private void endChat() {
		online = false;
		listController.setRefreshButtonEnabled(false);
		Log.v(this.getClass().getSimpleName(), "Chat ended");
		stop();
	}

	/**
	 * Simply shows the chat.
	 * This does not start any network related functions.
	 * Use this for offline browsing.
	 */
	private void showChat() {
		Log.v(this.getClass().getSimpleName(), "Chat shown");
		loadingController.hide();
		mainContainer.setVisible(true);
	}

	/**
	 * Shows a screen allowing the user to set its username.
	 *
	 * @param isError True is the screen is shown because of an error, false otherwise
	 */
	private void showLogin(boolean isError) {
		Log.v(this.getClass().getSimpleName(), "Login shown");
		mainContainer.setVisible(false);
		loadingController.hide();
		if (isError) {
			Timer t = new Timer();
			t.schedule(new TimerTask() {
				@Override
				public void run() {
					openEditUsernameDialog(EditUsernameDialogController.Mode.ERROR);
					t.cancel();
				}
			}, 500);
		} else {
			openEditUsernameDialog(EditUsernameDialogController.Mode.INITIAL);
		}
	}

	/**
	 * Shows an error screen telling the user the app failed to start
	 */
	private void showError() {
		Log.v(this.getClass().getSimpleName(), "Error shown");
		mainContainer.setVisible(false);
		loadingController.hide();
		errorController.show();
	}

	private void onInitError(Exception e) {
		Log.e("INIT", "Error during initialization", e);
		showError();
	}

	/**
	 * Reads user config, then creates database if needed,
	 * then init current user, and finally load user list.
	 */
	private void initBackend() {
		ConfigLoader.load(
				(Config config) -> {
					initLocalNetwork(config.getLocalConfig());
					new DatabaseController().initTables(
							() -> userList.retrievedPreviousUsers(
									() -> currentUser.init(
											() -> initPresenceServer(config.getServerConfig()),
											this::onInitError),
									this::onInitError
							), this::onInitError);
				});
	}

	private void initLocalNetwork(Config.LocalConfig localConfig) {
		port = localConfig.getPort();
		localEnabled = localConfig.isEnabled();
		if (localEnabled) {
			Log.v(getClass().getSimpleName(), "Local network support enabled on port: " + port);
		} else {
			Log.v(getClass().getSimpleName(), "Local network support disabled");
		}
	}

	/**
	 * Initializes the presence server based on user config
	 *
	 * @param serverConfig The server config
	 */
	private void initPresenceServer(Config.ServerConfig serverConfig) {
		if (serverConfig.isEnabled()) {
			try {
				final ServerType type = serverConfig.getType();
				final String uri = serverConfig.getUri();
				final int presencePort = serverConfig.getPresencePort();
				final int proxyPort = serverConfig.getProxyPort();
				presenceServer = PresenceFactory.create(type, uri, presencePort, proxyPort);
				Log.v(getClass().getSimpleName(), "Presence server support enabled: " + type + "@" + uri + ':' + presencePort + " / proxy:" + proxyPort);
				subscribeToPresenceServer();
			} catch (UnknownPresenceException e) {
				Log.e(getClass().getSimpleName(), "Presence server type not found", e);
				presenceServer = null;
			}
		} else {
			Log.v(getClass().getSimpleName(), "Presence server support disabled.");
		}
	}

	private void subscribeToPresenceServer() {
		if (presenceServer != null && online) {
			presenceServer.subscribe(
					new UserInformation(CurrentUser.getInstance()),
					param -> userList.onReceivePresenceNotification(
							param,
							presenceServer.getProxy()),
					(e) -> Log.v(
							getClass().getSimpleName(),
							"Error subscribing to presence server",
							e));
		}
	}

	private void unsubscribeToPresenceServer() {
		if (presenceServer != null) {
			presenceServer.unsubscribe(
					null,
					(e) -> Log.v(
							getClass().getSimpleName(),
							"Error unsubscribing to presence server",
							e));
		}
	}

	/**
	 * Stop all threads and active connections before exiting
	 */
	public void stop() {
		if (userList != null) {
			userList.destroy();
		}
		unsubscribeToPresenceServer();
	}

	@Override
	public void initialize(URL url, ResourceBundle rb) {
		loadingController.show("Initialisation de Clavardator...");
		snackbar = new JFXSnackbar(root);
		userList = new UserList();


		listController.setUserList(userList);
		listController.setRefreshUserListener(this::discoverActiveUsers);
		listController.setUserSelectedListener((user) -> chatController.setRemoteUser(user));

		chatController.setSendErrorListener((e) -> showSnackbarEvent("Erreur: Message non envoyé", SnackbarController.Mode.ERROR));

		toolbarController.setEditListener(() -> openEditUsernameDialog(EditUsernameDialogController.Mode.EDIT));
		toolbarController.setAboutListener(this::openAboutDialog);

		editUserDialogController.setUserList(userList);

		initBackend();
	}

}
