package me.despawningbone.arithtrain;

import java.io.IOException;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import javafx.animation.PauseTransition;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
import me.despawningbone.arithtrain.ExpressionGenerator;
import me.despawningbone.arithtrain.Main;
import me.despawningbone.arithtrain.User;


/**
 * The UIHelper class for providing GUI pages and elements.<br>
 * It has been designed so that multiple stages can utilize the UIHelper with multiple UIHelper instances.
 * 
 * @author despawningbone
 */
@SuppressWarnings({"unchecked", "rawtypes"}) //had to do this to drastically reduce code lines; besides i wont actually use the events in the functions anyways
public class UIHelper {
	private Stage m;
	private int h;
	private int w;

	/**
	 * Instantiates the UIHelper with the respective window stage, and its size.
	 * @param main the window stage the UIHelper should operate on
	 * @param width the width of the window stage, in pixels
	 * @param height the height of the window stage, in pixels
	 */
	public UIHelper(Stage main, int width, int height) {
		m = main;
		h = height;
		w = width;
	}

	/**
	 * Displays a page according to the node given, adding suitable uniformed styling in the process.
	 * @param node the page node to be displayed
	 * @param darkenBg whether the background should be darkened or not
	 */
	public void display(Parent node, boolean darkenBg) {
		Scene scene = new Scene(node, (double) w, (double) h);
		scene.getStylesheets().add("resources/main.css");
		node.setStyle("-fx-background-image: url(\"resources/bg" + (darkenBg ? "-darken.png" : ".jpg")
				+ "\"); -fx-background-size: cover;");
		this.m.setScene(scene);
	}
	
	/**
	 * A helper method for retrieving the 2 main menu buttons.
	 * @param op the name of the button to be retrieved
	 * @return the button object
	 */
	public Button getMenuButton(String op) {      //wrapper
		return this.getPicButton(op, (event) -> { //no need to unrelease it
			if (op.equals("train")) {
				this.showLogin(false);
			} else if (op.equals("solve")) {
				this.showSolver();
			}

		}, 0.2D, false);
	}

	/**
	 * Retrieves a generic PNG-based button from the program's resources directory, and assigns generic reaction listeners to the button.
	 * @param op the name of the button to be retrieved
	 * @param func the function to be executed on mouse press (or enter if the defaultButton is true)
	 * @param scaleFactor how large the button should appear
	 * @param defaultButton whether the button should be triggered on pressing enter anywhere on the GUI. 
	 * @return the retrieved button
	 */
	private Button getPicButton(String op, EventHandler func, double scaleFactor, boolean defaultButton) {
		Button b = new Button();
		b.setGraphic(this.getImage(op, "normal", scaleFactor));
		b.setStyle("-fx-background-color: transparent;");
		b.setOnMouseEntered((event) -> {
			b.setGraphic(this.getImage(op, "hover", scaleFactor));
		});
		b.setOnDragEntered((event) -> {
			b.setGraphic(this.getImage(op, "hover", scaleFactor));
		});
		b.setOnMouseExited((event) -> {
			b.setGraphic(this.getImage(op, "normal", scaleFactor));
		});
		b.setOnDragExited((event) -> {
			b.setGraphic(this.getImage(op, "normal", scaleFactor));
		});
		b.setOnMousePressed((event) -> {
			b.setGraphic(this.getImage(op, "pressed", scaleFactor));
		});
		b.setOnMouseReleased(func);
		b.setOnMouseClicked((event) -> {
			b.setGraphic(this.getImage(op, "hover", scaleFactor));
		});
		if (defaultButton) {
			b.setDefaultButton(true);
			b.setOnAction(func);
		}

		return b;
	}
	
	/**
	 * Constructs a generic CSS-based button, and assigns generic reaction listeners to the button.
	 * @param name the name of the CSS button 
	 * @param style the CSS styling for the main button
	 * @param pressedStyle the CSS styling for when the button is pressed
	 * @param hoverStyle the CSS styling for when the button is hovered
	 * @param func the function to be executed on mouse press (or enter if the defaultButton is true)
	 * @param defaultButton whether the button should be triggered on pressing enter anywhere on the GUI. 
	 * @return the retrieved button
	 */
	private Button getCSSButton(String name, String style, String pressedStyle, String hoverStyle, EventHandler func,
			boolean defaultButton) {
		Button b = new Button(name);
		b.setStyle(style);
		b.setOnMouseEntered((event) -> {
			b.setStyle(hoverStyle);
		});
		b.setOnDragEntered((event) -> {
			b.setStyle(hoverStyle);
		});
		b.setOnMouseExited((event) -> {
			b.setStyle(style);
		});
		b.setOnDragExited((event) -> {
			b.setStyle(style);
		});
		b.setOnMousePressed((event) -> {
			b.setStyle(pressedStyle);
		});
		b.setOnMouseReleased(func);
		b.setOnMouseClicked((event) -> {
			b.setStyle(hoverStyle);
		});
		if (defaultButton) {
			b.setDefaultButton(true);
			b.setOnAction(func);
		}

		return b;
	}
	
	/**
	 * Gets an image from the program's resource directory with pre-defined names.
	 * @param op the name of the image
	 * @param type the type of the image (hover, normal, pressed etc)
	 * @param scaleFactor how large the image should appear
	 * @return the retrieved image
	 */
	private ImageView getImage(String op, String type, double scaleFactor) {
		ImageView img = new ImageView(new Image("resources/" + op + "_" + type + ".png"));
		img.setFitHeight((double) h * scaleFactor);
		img.setPreserveRatio(true);
		return img;
	}

	/**
	 * Constructs a uniform footer for GUI pages that needs a back button.
	 * @param func a custom function to be executed on pressing the back button, or null for returning to the main menu. 
	 * @return the footer as a StackPane
	 */
	public StackPane getFooter(EventHandler func) {
		StackPane footer = new StackPane();
		Rectangle box = new Rectangle(0.0D, 0.0D, (double) w, (double) (h / 8));  //h isnt / 10 to get all of available space
		box.setStyle("-fx-fill: rgba(0, 0, 0, 0.5);");
		Button back = this.getPicButton("back", func == null ? (event) -> {
			this.showMainMenu();
		} : func, 0.1D, false);
		back.setCancelButton(true);  
		back.setOnAction(func == null ? (event) -> {
			this.showMainMenu();
		} : func);
		footer.getChildren().add(box);
		footer.getChildren().add(back);
		footer.setPrefSize((double) w, (double) (h / 10));
		footer.setAlignment(Pos.CENTER_LEFT);
		StackPane.setAlignment(footer, Pos.BOTTOM_CENTER);
		return footer;
	}

	/**
	 * Constructs a text field with the icon specified, according to the size instructed. 
	 * @param name the name of the text field, this will be shown when the text field is not focused
	 * @param img the icon name in the resources directory of the program
	 * @param width the specified field width
	 * @param height the specified field height
	 * @return the constructed TextField
	 */
	public TextField getTextField(String name, String img, double width, double height) {
		Object f = img != null && img.equals("pw") ? new PasswordField() : new TextField();  //exception for passwords
		((TextField) f).setPromptText(name);
		((TextField) f).setMaxSize(width, height);
		if (img != null) {
			((TextField) f).setPadding(new Insets(0.0D, 0.0D, 0.0D, (double) (w / 30)));
		}

		((TextField) f).setStyle("-fx-background-insets: -0.5em;"
				+ (img == null ? ""
						: " -fx-background-image:url(\"resources/" + img
								+ ".png\"); -fx-background-size: 1.8em; -fx-background-repeat: no-repeat; -fx-background-position: left center;")
				+ "-fx-background-radius: 5em; -fx-background-color: #fff; -fx-font-size: 1.8em;");
		return (TextField) f;
	}

	//END HELPER METHODS
	//START PAGES
	
	/**
	 * Shows the main menu page.
	 */
	public void showMainMenu() {
		VBox menu = new VBox();
		menu.getChildren().add(this.getMenuButton("train"));
		menu.getChildren().add(this.getMenuButton("solve"));
		menu.setAlignment(Pos.CENTER);
		this.display(menu, false);
	}

	/**
	 * Shows the login or the register page.
	 * @param reg true to show the register page, and false for the login page.
	 */
	public void showLogin(boolean reg) {
		BorderPane pane = new BorderPane();
		VBox box = new VBox();
		box.setStyle(
				"-fx-background-color: linear-gradient(to bottom right, rgba(50, 120, 134, 0.7), rgba(53, 204, 147, 0.9)); -fx-background-radius: 1.5em; -fx-padding: 2em;");
		box.setAlignment(Pos.CENTER);
		box.setSpacing((double) (h / 20));
		HBox header = new HBox();
		header.setMaxSize((double) (w / 2), (double) (h / 10));
		header.setAlignment(Pos.CENTER_LEFT);
		header.setSpacing((double) (w / 60));
		Text name = new Text(reg ? "Register" : "Sign In");
		name.setStyle("-fx-font-weight: bold; -fx-font-size: 2.5em; -fx-fill: #fff");
		name.setFont(Font.font((String) null, FontWeight.EXTRA_BOLD, 25.0D));
		TextField user = this.getTextField("Username", "user", (double) (w / 3), (double) (h / 10));
		TextField password = this.getTextField("Password", "pw", (double) (w / 3), (double) (h / 10));
		TextField reEnter = this.getTextField("Confirm Password", "pw", (double) (w / 3), (double) (h / 10));
		HBox buttons = new HBox();
		Button signIn = this.getCSSButton(reg ? "Register" : "Sign In",
				"-fx-background-color: rgba(20, 20, 20, 0.7); -fx-text-fill: #fff; -fx-background-radius: 0.5em;",
				"-fx-background-color: rgba(20, 20, 20, 0.85); -fx-text-fill: #fff; -fx-background-radius: 0.5em;",
				"-fx-background-color: rgba(20, 20, 20, 0.55); -fx-text-fill: #fff; -fx-background-radius: 0.5em;",
				(event) -> {
					try {
						if (reg) {
							this.showTrainer(User.register(user.getText(), password.getText(), reEnter.getText()));
						} else {
							this.showTrainer(new User(user.getText(), password.getText()));
						}
					} catch (IllegalArgumentException | NoSuchElementException | IOException e) {
						Text error = new Text(e instanceof IOException ? "Something went wrong: " + e.getMessage()
								: e.getMessage()); 
						error.setStyle("-fx-font-size: 1em; -fx-fill: #d00");
						error.setWrappingWidth((double) w / 3.5D);
						if (header.getChildren().size() == 1) {
							header.getChildren().add(error);
						} else {
							header.getChildren().set(1, error);
						}
					}

				}, true);
		signIn.setPrefSize((double) (w / 8), (double) (h / 15));
		signIn.setMinSize((double) (w / 8), (double) (h / 15));
		header.getChildren().add(name);
		box.getChildren().add(header);
		box.getChildren().add(user);
		box.getChildren().add(password);
		buttons.getChildren().add(signIn);
		buttons.setAlignment(Pos.CENTER);
		buttons.setSpacing((double) (w / 12));
		if (!reg) {
			Button regButton = this.getCSSButton("Create Account",
					"-fx-background-color: transparent; -fx-text-fill: #fff;",
					"-fx-background-color: transparent; -fx-text-fill: #aaa; -fx-underline: true",
					"-fx-background-color: transparent; -fx-text-fill: #fff; -fx-underline: true", (event) -> {
						this.showLogin(true);
					}, false);
			regButton.setPrefSize((double) (w / 8), (double) (h / 10));
			buttons.getChildren().add(regButton);
		} else {
			box.getChildren().add(reEnter);
		}

		box.getChildren().add(buttons);
		box.setMaxSize((double) (w / 2), (double) (h / 2));
		pane.setCenter(box);
		pane.setBottom(reg ? this.getFooter((event) -> {
			this.showLogin(false);
		}) : this.getFooter((EventHandler) null));
		this.display(pane, true);
	}

	
	private String currentResult; 
	/**
	 * Shows the trainer page.
	 * @param user the logged in user that the trainer page should operate in context of
	 */
	public void showTrainer(User user) {
		BorderPane pane = new BorderPane();
		pane.setPadding(new Insets((double) (h / 30), 0.0D, 0.0D, 0.0D));
		HBox top = new HBox();
		Text level = new Text("Level");
		level.setStyle("-fx-font-weight: bold; -fx-font-size: 2em; -fx-fill: #fff");
		StackPane progress = new StackPane();
		progress.setMaxSize((double) w * 0.8D, (double) (h / 10));
		ProgressBar bar = new ProgressBar();
		bar.setProgress(user.getNextLevelPercent());
		bar.setPrefSize((double) w * 0.8D, (double) (h / 10));
		bar.layout();
		int initLv = user.getLevel();
		Label fromLevel = new Label(String.valueOf(initLv));
		fromLevel.setPrefSize((double) (h / 10), (double) (h / 10));
		Label toLevel = new Label(String.valueOf(initLv + 1));
		toLevel.setMaxSize((double) (h / 10), (double) (h / 10));
		progress.getChildren().add(bar);
		progress.getChildren().add(fromLevel);
		toLevel.setAlignment(Pos.CENTER);
		toLevel.setStyle(
				"-fx-background-radius: 5em; -fx-background-color: #fff; -fx-font-size: 2em; -fx-background-insets: 1em;");
		fromLevel.setAlignment(Pos.CENTER);
		fromLevel.setStyle(
				"-fx-background-radius: 5em; -fx-background-color: #fff; -fx-font-size: 2em; -fx-background-insets: 1em;");
		progress.getChildren().add(toLevel);
		StackPane.setAlignment(fromLevel, Pos.CENTER_LEFT);
		StackPane.setAlignment(toLevel, Pos.CENTER_RIGHT);
		DecimalFormat df = new DecimalFormat("#" + (Main.precision > 0 ? "." + String.join("", Collections.nCopies(Main.precision, "#")) : ""));
		df.setRoundingMode(RoundingMode.HALF_UP);
		PauseTransition wait = new PauseTransition(Duration.seconds(1.0D));
		VBox center = new VBox();
		StackPane exp = new StackPane();
		List<String> gen = user.getCurrentGenerator().generate();
		Label expContent = new Label(gen.get(0));
		currentResult = df.format(Double.parseDouble(gen.get(gen.size() - 1)));
		exp.getStyleClass().add("exp");
		exp.setMaxSize((double) (w / 2), (double) (h / 4));
		exp.setFocusTraversable(false);
		expContent.setPrefSize((double) (w / 2), (double) (h / 4));
		String expStyle = "-fx-padding: 0.5em; -fx-font-weight: bold; -fx-text-fill: #fff; -fx-alignment: center;"
				+ "-fx-background-radius: 1.2em; -fx-background-color:linear-gradient(to bottom right, rgba(36, 224, 216, 0.6), rgba(62, 106, 169, 0.8));";
		expContent.setStyle(expStyle + " -fx-font-size: 3em;");
		exp.getChildren().add(expContent);
		exp.setAlignment(Pos.CENTER);
		HBox hB = new HBox();
		hB.setPrefSize((double) w, (double) (h / 10));
		hB.setPadding(new Insets(0.0D, 0.0D, 0.0D, (double) (w / 80))); // to offset the inflated textfield
		TextField input = this.getTextField("Answer", (String) null, (double) (w / 2), (double) (h / 12));
		input.setPrefSize((double) w, (double) (h / 10));
		StackPane footer = this.getFooter((event) -> {
			try {
				user.save();
			} catch (IOException e) {
				e.printStackTrace();
			}

			this.showMainMenu();
		});
		Label score = new Label("Current Score: " + user.getScore());
		score.setPadding(new Insets(0.0D, (double) (w / 50), 0.0D, 0.0D));
		score.setStyle("-fx-font-weight: bold; -fx-font-size: 2.5em; -fx-text-fill: #fff;");
		footer.getChildren().add(score);
		StackPane.setAlignment(score, Pos.CENTER_RIGHT);
		Button send = this.getPicButton("send", (event) -> {
			int lv = Integer.parseInt(fromLevel.getText());
			System.out.println("Expression generated: " + expContent.getText() + "; Expected: " + currentResult + ", Answer:" + input.getText());
			int newLv = 0;
			try {
				if (!currentResult.equals(df.format(Double.parseDouble(input.getText())))) {  //match with configured precision or preciser
					throw new NumberFormatException();  //pass to exception handler
				}
				expContent.setText("Correct!");
				newLv = user.updateScore(true);
				score.setText("Current Score: " + user.getScore());
			} catch (NumberFormatException e) {
				newLv = user.updateScore(false);
				expContent.setText("Incorrect! The answer was: " + currentResult);
				expContent.setStyle(expStyle + " -fx-font-size: 2em;");
			}

			input.setText("");
			((Button) event.getSource()).setDisable(true);  //so people cant spam it when its in result mode; have to cast coz even tho send is definitely initialized it thinks it isnt
			bar.setProgress(user.getNextLevelPercent());
			if (lv != newLv) {
				fromLevel.setText(String.valueOf(newLv));
				toLevel.setText(String.valueOf(newLv + 1));
				if (lv < newLv) {
					expContent.setText("Leveled up!");
				} else if (lv > newLv) {
					expContent.setText("Oops! You dropped a level.");
				}
			}
			wait.setOnFinished((e) -> {
				((Button) event.getSource()).setDisable(false);
				List<String> newGen = user.getCurrentGenerator().generate();
				expContent.setText(newGen.get(0));
				currentResult = df.format(Double.parseDouble(newGen.get(newGen.size() - 1)));
				expContent.setStyle(expStyle + " -fx-font-size: 3em;");
			});
			wait.play();
		}, 0.1D, true);
		hB.getChildren().add(input);
		hB.getChildren().add(send);
		hB.setSpacing((double) (w / 60));
		hB.setAlignment(Pos.CENTER);
		center.getChildren().add(exp);
		center.getChildren().add(hB);
		center.setSpacing((double) (h / 12));
		center.setMaxSize((double) (w / 2), (double) (h / 2));
		center.setAlignment(Pos.CENTER);
		top.getChildren().add(level);
		top.getChildren().add(progress);
		top.setSpacing((double) (w / 40));
		top.setAlignment(Pos.CENTER);
		pane.setTop(top);
		pane.setCenter(center);
		pane.setBottom(footer);
		this.display(pane, true);
	}

	/**
	 * Shows the solver page.
	 */
	public void showSolver() {
		BorderPane pane = new BorderPane();
		VBox vB = new VBox();
		vB.setSpacing((double) (h / 20));
		vB.setStyle("-fx-padding: 4em;");
		HBox hB = new HBox();
		hB.setPrefSize((double) w, (double) (h / 10));
		TextField input = this.getTextField("Enter your expression here", (String) null, (double) w * 0.79D,
				(double) (h / 10));
		input.setPrefSize((double) w, (double) (h / 10));
		TextArea result = new TextArea();
		result.setEditable(false);  //so no input is done in the steps field
		result.setPrefSize((double) w, (double) h);
		result.setFocusTraversable(false);
		String resultStyle = "-fx-focus-color: transparent; -fx-faint-focus-color: transparent; -fx-border-color: transparent;-fx-padding: 0.5em; -fx-font-size: 1.5em; -fx-font-weight: 400;-fx-opacity: 0.8; -fx-background-radius: 1.2em; -fx-background-color:";
		result.setStyle(resultStyle + "rgba(255, 255, 255, 0.7)");
		Button send = this.getPicButton("send", (event) -> {
			String steps;
			try {
				steps = String.join("\n= ", ExpressionGenerator.evaluateWithSteps(input.getText()));
				result.setStyle(resultStyle + "rgba(255, 255, 255, 0.7)");
			} catch (IllegalArgumentException e) {
				//shake the result box?
				result.setStyle(resultStyle + "rgba(255, 146, 146, 0.7);");
				steps = e.getMessage();
			}

			result.setText(steps);
		}, 0.1D, true);
		hB.setSpacing((double) (w / 40));
		hB.getChildren().add(input);
		hB.getChildren().add(send);
		hB.setAlignment(Pos.CENTER);
		vB.getChildren().add(hB);
		vB.getChildren().add(result);
		pane.setCenter(vB);
		pane.setBottom(this.getFooter((EventHandler) null));
		this.display(pane, true);
	}
}