import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The central Backend Component
 * @author Luca Conte
 */
public class GameController {

	private static MainFrame mainFrame;

	/**
	 * @return the current MainFrame
	 * @author Luca Conte
	 */
	public static MainFrame getMainFrame() {
		return GameController.mainFrame;
	}
	/**
	 * @param mainFrame the current MainFrame
	 * @author Luca Conte
	 */
	public static void setMainFrame(MainFrame mainFrame) {
		GameController.mainFrame = mainFrame;
	}

	/**
	 * converts a semester to be played in into its respective board size
	 * @param semester the semester to be played in
	 * @author Luca Conte
	 */
	public static int semesterToBoardSize(int semester) {
		return semester + 13;
	}
	/**
	 * converts a board size into its respective semester
	 * @param size the board size to get the semester of
	 * @author Luca Conte
	 */
	public static int boardSizeToSemester(int size) {
		return size - 13;
	}
	
	/**
	 * the list of supported versions of the online protocol
	 * https://github.com/lgc-4/ProgProjekt-Netzwerkstandard
	 */
	public static HashMap<String, Class<? extends OnlinePlayer>> supportedVersions = new HashMap<>(Map.of(
		"1.1.0", OnlinePlayer_1_1_0.class
	));

	/**
	 * Starts a game with one local player and one online player.
	 * @param localPlayerClass the class of the local player to be instantiated. must extend `LocalPlayer`
	 * @param localPlayerName the name of the local player. Only necessarry when localPlayerClass is `HumanPlayer`
	 * @param address the Address of the opposing player. If the hostname of the address is set to be 0.0.0.0, it
	 * is interpreted that this instance should instead listen on the defined port for incoming connections.
	 * @param size the board size that it is intended to be played in
	 * @author Luca Conte
	 */
	public static void startOnlineGame(Class<? extends LocalPlayer> localPlayerClass, String localPlayerName, InetSocketAddress address, int size) throws IOException {
		AsyncSocket clientSocket;

		boolean localPlayerIsServer = address.getHostName() == null || address.getHostName().isEmpty() || address.getHostName().equals("0.0.0.0");

		if (localPlayerIsServer) {
			// SERVER MODE
			clientSocket = new AsyncSocket(address.getPort(), null);

		} else {
			// CLIENT MODE
			clientSocket = new AsyncSocket(address, null);
		}

		clientSocket.send("VERSION", "Gruppe03 " + String.join(" ", supportedVersions.keySet()));
		clientSocket.setHandler((message) -> {

			SocketPackage socketPackage = new SocketPackage(message);
			if (socketPackage.getName().equals("VERSION")) {

				List<String> opponentSupportedVersions = socketPackage.splitData();

				String usedVersion = findMostRecentVersion(new ArrayList<String>(supportedVersions.keySet()), opponentSupportedVersions);

				if (usedVersion == null) {
					throw new RuntimeException("No common versions found");
				}

				// instantiate players
				Class<? extends OnlinePlayer> onlinePlayerClass = supportedVersions.get(usedVersion);

				LocalPlayer localPlayer;
				OnlinePlayer onlinePlayer;
				try {
					localPlayer = localPlayerClass.getDeclaredConstructor().newInstance();
					onlinePlayer = onlinePlayerClass.getDeclaredConstructor(Integer.class, AsyncSocket.class).newInstance((Integer)size, clientSocket);
				} catch (Exception e) {
					e.printStackTrace();
					throw new RuntimeException("Unable to instantiate players");
				}


				localPlayer.isServer = localPlayerIsServer;
				onlinePlayer.isServer = !localPlayerIsServer;
				
				localPlayer.setName(localPlayerName);

				localPlayer.setEnemy(onlinePlayer);
				onlinePlayer.setEnemy(localPlayer);

				onlinePlayer.sendIAM();

				// Start game only after IAM Package was exchanged
			} else {
				throw new RuntimeException("Unexpected Package received before game initialisation");
			}
		});

	}

	/**
	 * finds the largest common version in two lists of version strings
	 * @return null if no common versions are found
	 * @author Luca Conte
	 */
	public static String findMostRecentVersion(List<String> versions1, List<String> versions2) {
		if (versions1 == null || versions2 == null) return null;
		String largestCommonVersion = null;
		for (String v1 : versions1) {
			if (!checkVersionString(v1)) continue;

			for (String v2 : versions2) {
				if (!checkVersionString(v2)) continue;

				// strings within the lists must be equal
				// if no largest common version has been found or currently checked version is larger than
				// previous largest common version, new largest version has been found
				if (compareVersions(v1, v2) == 0 && (largestCommonVersion == null || compareVersions(v1, largestCommonVersion) >= 1)) {
					largestCommonVersion = v1;
					continue;
				}
			}
		}
		return largestCommonVersion;
	}

	/**
	 * compares two version strings
	 * @param version1 the first version to be compared
	 * @param version2 the second version to be compared
	 * @return
	 *  0 if versions are equal
	 *  1 if version1 is more recent than version2
	 *  -1 otherwise
	 * @author Luca Conte
	 */
	public static int compareVersions(String version1, String version2) {
		if (!checkVersionString(version1) || !checkVersionString(version2)) {
			throw new IllegalArgumentException("Version is not valid version string");
		}

		String[] version1Split = version1.split("\\.");
		String[] version2Split = version2.split("\\.");

		for (int i = 0; i < version1Split.length; i++) {
			int v1 = Integer.parseInt(version1Split[i]);
			int v2 = Integer.parseInt(version2Split[i]);

			if (v1 == v2) continue;
			if (v1 > v2) {
				return 1;
			} else {
				return -1;
			}
		}
		return 0;
	}

	/**
	 * checks if a provided string matches the format of a version number
	 * @param versionString the version String to be checked
	 * @author Luca Conte
	 */
	public static boolean checkVersionString(String versionString) {
		return versionString != null && versionString.matches("\\d+\\.\\d+\\.\\d+");
	}

	/**
	 * Starts a game with two local players
	 * @param localPlayerClass the class of the local player to be instantiated. must extend `LocalPlayer`
	 * @param localPlayerName the name of the local player. Only necessarry when localPlayerClass is `HumanPlayer`
	 * @param enemyClass the class of the enemy player. Must extend `AiPlayer`. For Humans VS Human games use an online game instead.
	 * @param size the board size that it is intended to be played in
	 * @author Luca Conte
	 */
	public static void startLocalGame(Class<? extends  LocalPlayer> localPlayerClass, String localPlayerName, Class<? extends AiPlayer> enemyClass, int size) {

		LocalPlayer localPlayer;
		AiPlayer aiPlayer;
		try {
			localPlayer = localPlayerClass.getDeclaredConstructor().newInstance();
			aiPlayer = enemyClass.getDeclaredConstructor().newInstance();
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException("Unable to instantiate players"); 
		}
		
        localPlayer.isServer = true;
        aiPlayer.isServer = false;


        localPlayer.setEnemy(aiPlayer);
        aiPlayer.setEnemy(localPlayer);
		
		localPlayer.createBoard(size);
		aiPlayer.createBoard(size);

		localPlayer.setName(localPlayerName);

		startGameWithInstancedPlayers(localPlayer, aiPlayer, size);
	}

	/**
	 * switches to the ingame screen
	 * @param p1 the first player. This player is always shown on the left side of the screen and is expected to be the Human player, if there is one.
	 * @param p2 the second player
	 * @param boardSize the board size to be played in
	 * @author Luca Conte
	 */
	public static void startGameWithInstancedPlayers(LocalPlayer p1, Player p2, int boardSize) {
		mainFrame.showPanelSLG("GameBoard", boardSizeToSemester(boardSize), p1, p2);

		// TODO: frontend configuration
	}

}