package com.cardshifter.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Set;

import com.cardshifter.api.config.PlayerConfig;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import com.cardshifter.api.config.DeckConfig;
import net.zomis.cardshifter.ecs.usage.CardshifterIO;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import com.cardshifter.api.both.ChatMessage;
import com.cardshifter.api.both.InviteRequest;
import com.cardshifter.api.both.InviteResponse;
import com.cardshifter.api.both.PlayerConfigMessage;
import com.cardshifter.api.incoming.LoginMessage;
import com.cardshifter.api.incoming.ServerQueryMessage;
import com.cardshifter.api.incoming.ServerQueryMessage.Request;
import com.cardshifter.api.incoming.StartGameRequest;
import com.cardshifter.api.messages.Message;
import com.cardshifter.api.outgoing.AvailableModsMessage;
import com.cardshifter.api.outgoing.NewGameMessage;
import com.cardshifter.api.outgoing.ErrorMessage;
import com.cardshifter.api.outgoing.UserStatusMessage;
import com.cardshifter.api.outgoing.UserStatusMessage.Status;
import com.cardshifter.client.buttons.GameTypeButton;
import com.cardshifter.client.buttons.GenericButton;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;

public class GameClientLobby implements Initializable {
	
	private static final Logger logger = LogManager.getLogger(GameClientLobby.class);
	
	@FXML private AnchorPane rootPane;
	@FXML private ListView<String> usersOnline;
	@FXML private ListView<String> chatMessages;
	@FXML private TextField messageBox;
	@FXML private Button sendMessageButton;
	@FXML private AnchorPane inviteButton;
	@FXML private AnchorPane inviteWindow;
	@FXML private HBox gameTypeBox;
	@FXML private AnchorPane deckBuilderButton;
	
	private final ObjectMapper mapper = CardshifterIO.mapper();
	private final Set<GameClientController> gamesRunning = new HashSet<>();
	private Socket socket;	
	private InputStream in;
	private OutputStream out;
	private String ipAddress;
	private int port;
	private String userName;
	
	private Thread listenThread;
	private final Map<String, Integer> usersOnlineList = new HashMap<>();
	private String userForGameInvite;
	private InviteRequest currentGameRequest;
	private final List<String> gameTypes = new ArrayList<>();
	private String selectedGameType;
	private PlayerConfigMessage currentPlayerConfig;
	private DeckBuilderWindow openDeckBuilderWindow;
	
	public void acceptConnectionSettings(String ipAddress, int port, String userName) {
		// this is passed into this object after it is automatically created by the FXML document
		this.ipAddress = ipAddress;
		this.port = port;
		this.userName = userName;
	}
	public boolean connectToLobby() {
		// this is called on the object before the scene is displayed
		try {
			this.socket = new Socket(this.ipAddress, this.port);
			this.out = socket.getOutputStream();
			this.in = socket.getInputStream();
			this.listenThread = new Thread(this::listen);
			this.listenThread.start();
		} catch (IOException ex) {
			logger.info("Connection Failed");
			return false;
		}
		
		this.sendLoginMessage();
		this.sendServerQueryMessage();
		
		this.usersOnline.setOnMouseClicked(this::selectUserForGameInvite);
		this.inviteButton.setOnMouseClicked(this::startGameWithUser);
		this.deckBuilderButton.setOnMouseClicked(this::openDeckBuilderWindowWithoutGame);
		
		return true;
	}
	
	private void listen() {
		while (true) {
			if (socket.isClosed()) {
				chatOutput("Connection Closed");
				break;
			}
			try {
				MappingIterator<Message> values = mapper.readValues(new JsonFactory().createParser(this.in), Message.class);
				while (values.hasNextValue()) {
					Message message = values.next();
					//This is where all the magic happens for message handling
					Platform.runLater(() -> this.processMessageFromServer(message));
				}
			} catch (SocketException e) {
				this.chatOutput("Error receiving message: " + e.getMessage());
				return;
			} catch (IOException e) {
				this.chatOutput("Lost connection to server");
			}
		}
	}
	
	private void send(Message message) {
		try {
			logger.info("Sending: " + this.mapper.writeValueAsString(message));
			this.mapper.writeValue(out, message);
		} catch (IOException e) {
			logger.info("Error sending message: " + message);
			throw new RuntimeException(e);
		}
	}
	
	private void sendLoginMessage() {
		LoginMessage loginMessage = new LoginMessage(this.userName);
		this.send(loginMessage);
	}
	
	private void sendServerQueryMessage() {
		ServerQueryMessage initialMessage = new ServerQueryMessage(Request.USERS);
		this.send(initialMessage);
	}
	
	private void processMessageFromServer(Message message) {	
		//this is for diagnostics so I can copy paste the messages to know their format
		logger.info(message);
		
		for (GameClientController gameController : this.gamesRunning) {
			gameController.processMessageFromServer(message);
		}
		
		if (message instanceof NewGameMessage) {
			this.startNewGame((NewGameMessage)message);
		} else if (message instanceof UserStatusMessage) {
			this.processUserStatusMessage((UserStatusMessage)message);
		} else if (message instanceof ChatMessage) {
			ChatMessage msg = (ChatMessage) message;
			this.chatOutput(msg.getFrom() + ": " + msg.getMessage());
		} else if (message instanceof ErrorMessage) {
			ErrorMessage msg = (ErrorMessage) message;
			this.chatOutput("SERVER ERROR: " + msg.getMessage());
		} else if (message instanceof PlayerConfigMessage) {
			PlayerConfigMessage msg = (PlayerConfigMessage) message;
			this.showConfigDialog(msg);
		} else if (message instanceof InviteRequest) {
			this.currentGameRequest = (InviteRequest)message;
			this.createInviteWindow((InviteRequest)message);
		} else if (message instanceof AvailableModsMessage) {
			this.gameTypes.addAll(Arrays.asList(((AvailableModsMessage)message).getMods()));
			this.createGameTypeButtons();
		}
	}
	
	private void showConfigDialog(PlayerConfigMessage configMessage) {
		this.currentPlayerConfig = configMessage;
		
		Map<String, PlayerConfig> configs = configMessage.getConfigs();
		
		for (Entry<String, PlayerConfig> entry : configs.entrySet()) {
			Object value = entry.getValue();
			if (value instanceof DeckConfig) {
				DeckConfig deckConfig = (DeckConfig) value;
				this.showDeckBuilderWindow(deckConfig, configMessage.getModName(), true);
			}
		}		
	}
	
	public void sendDeckAndPlayerConfigToServer(DeckConfig deckConfig) {
		Map<String, PlayerConfig> configs = this.currentPlayerConfig.getConfigs();
		
		for (Entry<String, PlayerConfig> entry : configs.entrySet()) {
			Object value = entry.getValue();
			if (value instanceof DeckConfig) {
				DeckConfig config = (DeckConfig) value;
				deckConfig.getChosen().forEach((id, count) -> config.setChosen(id, count));
			}
		}
		
		this.send(new PlayerConfigMessage(this.currentPlayerConfig.getGameId(), currentPlayerConfig.getModName(), configs));
	}
	
	private void openDeckBuilderWindowWithoutGame(MouseEvent event) {
		this.send(new ServerQueryMessage(Request.DECK_BUILDER, selectedGameType));
	}
	
	private void showDeckBuilderWindow(DeckConfig deckConfig, String modName, boolean startingGame) {
		try {
			FXMLLoader loader = new FXMLLoader(getClass().getResource("DeckBuilderDocument.fxml"));
			Parent root = (Parent)loader.load();
			DeckBuilderWindow controller = loader.<DeckBuilderWindow>getController();
			
			controller.acceptDeckConfig(deckConfig, modName, conf -> this.sendDeckAndPlayerConfigToServer(conf));
			controller.configureWindow();
			
			this.openDeckBuilderWindow = controller;
			
			if (!startingGame) {
				controller.disableGameStart();
			}
			
			Scene scene = new Scene(root);
			Stage stage = new Stage();
			stage.setScene(scene);
			stage.setOnCloseRequest(windowEvent -> this.closeDeckBuilderWindow());
			stage.show();
		}
        catch (Exception e) {
            throw new RuntimeException(e);
        }
	}
	
	private void startNewGame(NewGameMessage message) {
		if (!gamesRunning.isEmpty()) {
			this.chatOutput("You already have a running game. Unable to start a new one.");
			return;
		}

		try {
			FXMLLoader loader = new FXMLLoader(getClass().getResource("ClientDocument.fxml"));
			Parent root = (Parent)loader.load();
			GameClientController controller = loader.<GameClientController>getController();
			this.gamesRunning.add(controller);
			controller.acceptConnectionSettings(message, this::send);
			
			Scene scene = new Scene(root);
			Stage gameStage = new Stage();
			gameStage.setScene(scene);
			gameStage.setOnCloseRequest(windowEvent -> this.closeController(controller));
			gameStage.show();
		}
        catch (Exception e) {
            throw new RuntimeException(e);
        }
	}
	
	private void processUserStatusMessage(UserStatusMessage message) {
		if (message.getStatus() == Status.ONLINE) {
			this.usersOnlineList.put(message.getName(), message.getUserId());
		} else if (message.getStatus() == Status.OFFLINE) {
			this.usersOnlineList.remove(message.getName());
			chatOutput(message.getName() + " is now offline.");
		}
		
		this.usersOnline.getItems().clear();
		this.usersOnline.getItems().addAll(this.usersOnlineList.keySet());
	}
	
	
	private void chatOutput(String string) {
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
		String time = "[" + formatter.format(Instant.now()) + "] ";
		Platform.runLater(() -> this.chatMessages.getItems().add(time + string));
	}

	private void createInviteWindow(InviteRequest message) {
		this.inviteWindow.setVisible(true);
		this.inviteWindow.getChildren().add(new InviteWindow(message, this).getRootPane());
	}
	
	public void acceptGameRequest(MouseEvent event) {
		this.send(new InviteResponse(this.currentGameRequest.getId(), true));
		this.closeInviteWindow();
	}
	
	public void declineGameRequest(MouseEvent event) {
		this.send(new InviteResponse(this.currentGameRequest.getId(), false));
		this.closeInviteWindow();
	}
	
	private void closeInviteWindow() {
		this.inviteWindow.getChildren().clear();
		this.inviteWindow.setVisible(false);
	}

	private void selectUserForGameInvite(MouseEvent event) {
		String selected = this.usersOnline.getSelectionModel().getSelectedItem();
		if (selected != null) {
			this.userForGameInvite = selected;
		} else {
			this.usersOnline.getItems().clear();
			this.sendServerQueryMessage();
		}
	}
	
	private void startGameWithUser(MouseEvent event) {
		if (this.userForGameInvite != null) {
			if (this.selectedGameType != null) {
				int userIdToInvite = this.usersOnlineList.get(this.userForGameInvite);
				StartGameRequest startGameRequest = new StartGameRequest(userIdToInvite, this.selectedGameType);
				this.sendInvite(startGameRequest);
				this.chatOutput("Invite sent to " + this.userForGameInvite);
			} else {
				this.chatOutput("No Game Type selected");
			}
		} else {
			this.chatOutput("No Opponent selected");
		}
	}
	
	private void sendInvite(StartGameRequest startGameRequest) {
		if (!gamesRunning.isEmpty()) {
			this.chatOutput("You already have a running game. Unable to start a new one.");
			return;
		}
		
		this.send(startGameRequest);
	}
	
	private void closeController(GameClientController controller) {
		this.gamesRunning.remove(controller);
		controller.closeGame();
		controller.closeWindow();
	}
	
	private void closeDeckBuilderWindow() {
		//this is a workaround to allow the player to "decline" an invite once the deck builder is open
		if(this.gamesRunning.size() == 1) {
			for (GameClientController controller : this.gamesRunning) {
				this.closeController(controller);
				this.openDeckBuilderWindow = null;
			}
		}
	}
	
	private void stopThreads() {
		this.listenThread.interrupt();
	}
	
	private void breakConnection() {
		try {
			this.in.close();
			this.out.close();
		} catch (Exception e) {
			logger.info("Failed to break connection");
		}
	}
	
	public void closeLobby() {
		this.stopThreads();
		this.breakConnection();
		
		for (GameClientController game : this.gamesRunning) {
			game.closeWindow();
		}
		
		if (this.openDeckBuilderWindow != null) {
			this.openDeckBuilderWindow.closeWindow();
		}
	}
	
	private void createGameTypeButtons() {
		for (String string : this.gameTypes) {
			GameTypeButton button = new GameTypeButton(this.gameTypeBox.getPrefWidth() / this.gameTypes.size(), this.gameTypeBox.getPrefHeight(), string, this);
			this.gameTypeBox.getChildren().add(button);
		}
	}
	
	public void clearGameTypeButtons() {
		for (Object button : this.gameTypeBox.getChildren()) {
			((GenericButton)button).unHighlightButton();
		}
	}
	
	public void setGameType(String string) {
		this.selectedGameType = string;
	}
	
	@Override
	public void initialize(URL location, ResourceBundle resources) {
		this.sendMessageButton.setOnAction(e -> this.sendMessage());
		this.messageBox.setOnAction(e -> this.sendMessage());
	}
	
	private void sendMessage() {
		String message = this.messageBox.getText();
		this.send(new ChatMessage(1, "unused", message));
		this.messageBox.clear();
	}
	
}