/**
	 *	GUI code for Huffman
	 *
	 *	@author Brian Lavallee
	 *	@since 16 November 2015
	 */

	import java.io.File;
	import java.util.ArrayList;
	import java.util.HashMap;
	import java.util.List;
	import java.util.Map;

	import javafx.animation.AnimationTimer;
	import javafx.animation.FadeTransition;
	import javafx.application.Platform;
	import javafx.geometry.Pos;
	import javafx.scene.Group;
	import javafx.scene.Node;
	import javafx.scene.control.Button;
	import javafx.scene.control.CheckBox;
	import javafx.scene.control.ScrollPane;
	import javafx.scene.control.Tab;
	import javafx.scene.control.TabPane;
	import javafx.scene.control.TabPane.TabClosingPolicy;
	import javafx.scene.layout.HBox;
	import javafx.scene.layout.VBox;
	import javafx.scene.paint.Color;
	import javafx.scene.shape.Rectangle;
	import javafx.scene.text.Text;
	import javafx.stage.DirectoryChooser;
	import javafx.stage.FileChooser;
	import javafx.stage.Stage;
	import javafx.util.Duration;

	public class HuffViewer {

		private static final double TAB_HEIGHT = 23;

		private static final double TAB_PADDING = 70;
		private static final double INTERNAL_PADDING = 7;
		private static final double MARGIN = 10;

		private String processor;

		public HuffViewer(String processor) {
			this.processor = processor;
		}

		public Group createLayout(double width, double height) {
			Group root = new Group();

			TabPane holder = new TabPane();
			holder.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
			holder.setMinSize(width, height);
			holder.setTabMinWidth((width - TAB_PADDING) / 4.0);
			holder.setTabMinHeight(TAB_HEIGHT);

			double contentWidth = width - 2 * MARGIN;
			double contentHeight = height - TAB_HEIGHT - 2 * MARGIN;

			Tab compress = new Tab("Compress");
			compress.setContent(createCompressTab(contentWidth, contentHeight));

			Tab decompress = new Tab("Decompress");
			decompress.setContent(createDecompressTab(contentWidth, contentHeight));

			Tab compare = new Tab("Compare");
			compare.setContent(createCompareTab(contentWidth, contentHeight));

			Tab test = new Tab("Test");
			test.setContent(createTestTab(contentWidth, contentHeight));

			holder.getTabs().addAll(compress, decompress, compare, test);
			root.getChildren().add(holder);
			return root;
		}

		private VBox createCompressTab(double contentWidth, double contentHeight) {
			VBox tabContent = new VBox(MARGIN);
			tabContent.setMinSize(contentWidth, contentHeight);
			tabContent.setTranslateX(MARGIN);
			tabContent.setTranslateY(MARGIN);

			HBox upper = new HBox();
			upper.setMinHeight(80);

			HuffPanel panel = new HuffPanel(contentWidth, contentHeight - 150);

			StatusBar status = new StatusBar(contentWidth - 200);

			VBox inputField = new VBox(INTERNAL_PADDING);
			inputField.setMinWidth(200);
			HuffChooser chooser = new HuffChooser(true, "Compress");
			Button compress = new Button("Compress");
			compress.setOnAction((clicked) -> compress(chooser.getChosenFile(), status, panel));
			inputField.getChildren().addAll(chooser.render(), compress);

			upper.getChildren().addAll(inputField, status.render());

			tabContent.getChildren().addAll(upper, panel.render(new String[] { "Info" }));
			return tabContent;
		}

		private VBox createDecompressTab(double contentWidth, double contentHeight) {
			VBox tabContent = new VBox(MARGIN);
			tabContent.setMinSize(contentWidth, contentHeight);
			tabContent.setTranslateX(MARGIN);
			tabContent.setTranslateY(MARGIN);

			HBox upper = new HBox();
			upper.setMinHeight(80);

			HuffPanel panel = new HuffPanel(contentWidth, contentHeight - 150);

			StatusBar status = new StatusBar(contentWidth - 200);

			VBox inputField = new VBox(INTERNAL_PADDING);
			HuffChooser chooser = new HuffChooser(true, "Decompress");
			Button decompress = new Button("Decompress");
			decompress.setOnAction((clicked) -> decompress(chooser.getChosenFile(), status, panel));
			inputField.getChildren().addAll(chooser.render(), decompress);

			upper.getChildren().addAll(inputField, status.render());

			tabContent.getChildren().addAll(upper, panel.render(new String[] { "Info" }));
			return tabContent;
		}

		private VBox createCompareTab(double contentWidth, double contentHeight) {
			VBox tabContent = new VBox(MARGIN);
			tabContent.setMinSize(contentWidth, contentHeight);
			tabContent.setTranslateX(MARGIN);
			tabContent.setTranslateY(MARGIN);

			HBox upper = new HBox();
			upper.setMinHeight(80);

			HuffPanel panel = new HuffPanel(contentWidth, contentHeight - 150);

			StatusBar status = new StatusBar(contentWidth - 200);

			VBox inputField = new VBox(INTERNAL_PADDING);
			HuffChooser chooserA = new HuffChooser(true, "File A");
			HuffChooser chooserB = new HuffChooser(true, "File B");
			Button compare = new Button("Compare");
			compare.setOnAction((clicked) -> compare(chooserA.getChosenFile(), chooserB.getChosenFile(), status, panel));
			inputField.getChildren().addAll(chooserA.render(), chooserB.render(), compare);

			upper.getChildren().addAll(inputField, status.render());

			tabContent.getChildren().addAll(upper, panel.render(new String[] { "Info" }));
			return tabContent;
		}

		private VBox createTestTab(double contentWidth, double contentHeight) {
			VBox tabContent = new VBox(MARGIN);
			tabContent.setMinSize(contentWidth, contentHeight);
			tabContent.setTranslateX(MARGIN);
			tabContent.setTranslateY(MARGIN);

			HBox upper = new HBox();
			upper.setMinHeight(80);

			HuffPanel panel = new HuffPanel(contentWidth, contentHeight - 150);

			StatusBar status = new StatusBar(contentWidth - 200);

			VBox inputField = new VBox(INTERNAL_PADDING);
			HuffChooser chooser = new HuffChooser(false, "Directory");
			CheckBox hf = new CheckBox("test .hf files");
			Button test = new Button("Test");
			test.setOnAction((clicked) -> test(chooser.getChosenFile(), hf.isSelected(), status, panel));
			inputField.getChildren().addAll(chooser.render(), hf, test);

			upper.getChildren().addAll(inputField, status.render());

			tabContent.getChildren().addAll(upper, panel.render(new String[] { "Info" }));
			return tabContent;
		}

		private VBox getInfo(double[] times, File[] originalFiles, File[] newFiles) {
			VBox holder = new VBox(INTERNAL_PADDING);

			int totalOriginalLength = 0;
			double totalTime = 0;
			int totalNewLength = 0;

			VBox info = new VBox(INTERNAL_PADDING);
			info.getChildren().add(new Text());

			for (int i = 0; i < times.length; i++) {
				totalOriginalLength += originalFiles[i].length();
				totalTime += times[i];
				totalNewLength += newFiles[i].length();

				info.getChildren().add(new Text(originalFiles[i].getName() + " -> " + newFiles[i].getName()));
				info.getChildren().add(new Text("\tTime: " + times[i] / 1000.0 + "s"));
				info.getChildren().add(new Text("\tOriginal length: " + originalFiles[i].length() + " bytes"));
				info.getChildren().add(new Text("\tNew length: " + newFiles[i].length() + " bytes"));
				double saved = 1.0 - ((double) totalNewLength) / ((double) totalOriginalLength);
				info.getChildren().add(new Text("\t" + String.format("Percent space saved %.2f", saved * 100.0) + "%"));
				info.getChildren().add(new Text());
			}

			double percentSaved = 1.0 - ((double) totalNewLength) / ((double) totalOriginalLength);
			percentSaved *= 100.0;

			holder.getChildren().add(new Text("Total time: " + totalTime / 1000.0 + "s"));
			holder.getChildren().add(new Text("Total original length: " + totalOriginalLength + " bytes"));
			holder.getChildren().add(new Text("Total new length: " + totalNewLength + " bytes"));
			holder.getChildren().add(new Text(String.format("Percent space saved: %.2f", percentSaved) + "%"));

			holder.getChildren().add(info);

			return holder;
		}

		private class ProgressUpdater extends AnimationTimer {
			private Generator generator;
			private StatusBar status;
			private HuffPanel panel;
			private double progress;

			public ProgressUpdater(StatusBar status, HuffPanel panel) {
				this.status = status;
				this.panel = panel;
				generator = () -> {
					return 0;
				};
			}

			public void handle(long time) {
				status.setProgress(generator.generate());
			}

			public void setGenerator(Generator generator) {
				this.generator = generator;
			}

			public void updateStatus(Status s, String message) {
				Platform.runLater(() -> {
					status.setStatus(s, message);
				});
			}

			public void addContent(String label, Node content) {
				Platform.runLater(() -> {
					panel.addContent(label, content);
				});
			}

			public double progress() {
				return progress;
			}

			public void setProgress(double progress) {
				this.progress = progress;
			}
		}

		private interface Generator {
			public double generate();
		}

		private Processor getProcessor() {
			try {
				return (Processor) Class.forName(processor).newInstance();
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}

		private void compress(File original, StatusBar status, HuffPanel panel) {
			if (original == null) {
				return;
			}

			status.initialize();
			panel.clear();

			File compressed = new File(original.getPath() + ".hf");

			try {
				BitInputStream in = new BitInputStream(original);
				BitOutputStream out = new BitOutputStream(compressed);

				ProgressUpdater updater = new ProgressUpdater(status, panel);

				Thread thread = new Thread(() -> {
					updater.updateStatus(Status.Working, "compressing " + original.getName());
					updater.start();
					updater.setGenerator(() -> {
						return in.bitsRead() / (8.0 * original.length());
					});

					try {
						Processor processor = getProcessor();
						double start = System.currentTimeMillis();
						processor.compress(in, out);
						out.flush();
						updater.addContent("Info", getInfo(new double[] { System.currentTimeMillis() - start },
								new File[] { original }, new File[] { compressed }));
						updater.stop();
						updater.updateStatus(Status.Complete, "compression successful");
					} catch (HuffException e) {
						compressed.delete();
						updater.stop();
						updater.updateStatus(Status.Failed, e.getMessage());
					} catch (Exception e) {
						compressed.delete();
						updater.stop();
						updater.updateStatus(Status.Failed, "unknown error");
						e.printStackTrace();
					}

					out.close();
					in.close();
				});
				thread.start();
			} catch (Exception e) {
				e.printStackTrace();
				compressed.delete();
			}
		}

		private void decompress(File original, StatusBar status, HuffPanel panel) {
			if (original == null) {
				return;
			}

			status.initialize();
			panel.clear();

			String name = original.getPath();
			name = name.endsWith(".hf") ? name.substring(0, name.length() - 3) : name;
			File decompressed = new File(name + ".dehf");

			try {
				BitInputStream in = new BitInputStream(original);
				BitOutputStream out = new BitOutputStream(decompressed);

				ProgressUpdater updater = new ProgressUpdater(status, panel);

				Thread thread = new Thread(() -> {
					updater.setGenerator(() -> {
						return in.bitsRead() / (8.0 * original.length());
					});
					updater.updateStatus(Status.Working, "decompressing " + original.getName());
					updater.start();

					try {
						Processor processor = getProcessor();
						double start = System.currentTimeMillis();
						processor.decompress(in, out);
						updater.addContent("Info", getInfo(new double[] { System.currentTimeMillis() - start },
								new File[] { original }, new File[] { decompressed }));
						updater.stop();
						updater.updateStatus(Status.Complete, "decompression successful");
					} catch (HuffException e) {
						decompressed.delete();
						updater.stop();
						updater.updateStatus(Status.Failed, e.getMessage());
					} catch (Exception e) {
						updater.stop();
						e.printStackTrace();
						decompressed.delete();
						updater.updateStatus(Status.Failed, "unknown error");
					}

					in.close();
					out.close();
				});
				thread.start();
			} catch (Exception e) {
				e.printStackTrace();
				decompressed.delete();
			}
		}

		private void compare(File fileA, File fileB, StatusBar status, HuffPanel panel) {
			if (fileA == null || fileB == null) {
				return;
			}

			status.initialize();

			BitInputStream inA = new BitInputStream(fileA);
			BitInputStream inB = new BitInputStream(fileB);

			ProgressUpdater updater = new ProgressUpdater(status, panel);

			Thread thread = new Thread(() -> {
				updater.setGenerator(() -> {
					return inA.bitsRead() / (8.0 * fileA.length());
				});
				updater.updateStatus(Status.Working, "comparing " + fileA.getName() + " and " + fileB.getName());
				updater.start();

				try {
					inA.reset();
					int bitA = 0;
					int bitB = 0;
					double start = System.currentTimeMillis();
					int count = 0;
					while ((bitA = inA.readBits(1)) != -1 & (bitB = inB.readBits(1)) != -1) {
						if (bitA != bitB) {
							updater.stop();
							updater.updateStatus(Status.Failed, "files differ somewhere");
							inA.close();
							inB.close();
							updater.addContent("Info", getSimpleInfo(System.currentTimeMillis() - start, fileA.length(),
									fileB.length(), count));
							return;
						}
						count++;
					}
					updater.addContent("Info",
							getSimpleInfo(System.currentTimeMillis() - start, fileA.length(), fileB.length(), -1));
					updater.stop();
					inA.close();
					inB.close();
					if (bitA == bitB) {
						updater.updateStatus(Status.Complete, "files are the same");
					} else {
						updater.updateStatus(Status.Failed, "files differ at the end");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
			thread.start();
		}

		private VBox getSimpleInfo(double time, long lengthA, long lengthB, int firstDifference) {
			VBox holder = new VBox(INTERNAL_PADDING);
			holder.getChildren().add(new Text("Time: " + time + "s"));
			holder.getChildren().add(new Text("File A length: " + lengthA + " bytes"));
			holder.getChildren().add(new Text("File B length: " + lengthB + " bytes"));
			holder.getChildren().add(new Text("First difference at bit " + firstDifference));
			return holder;
		}

		private void test(File directory, boolean useHF, StatusBar status, HuffPanel panel) {
			if (directory == null) {
				return;
			}

			status.initialize();
			panel.clear();

			File[] files = directory.listFiles();
			List<File> toCompress = new ArrayList<>();
			for (File file : files) {
				if (file.isDirectory() || file.getName().endsWith(".dehf") || (file.getName().endsWith(".hf") && !useHF)
						|| (!file.getName().endsWith(".hf") && useHF)) {
					continue;
				}
				toCompress.add(file);
			}

			int total = 0;
			for (File file : toCompress) {
				total += file.length();
			}
			final int sum = total;

			ProgressUpdater updater = new ProgressUpdater(status, panel);

			@SuppressWarnings("resource")
			Thread thread = new Thread(() -> {
				updater.start();
				double[] times = new double[toCompress.size()];
				File[] compressed = new File[toCompress.size()];
				for (int i = 0; i < toCompress.size(); i++) {
					File file = toCompress.get(i);
					try {
						updater.updateStatus(Status.Working, "compressing " + file.getName());
						BitInputStream in = new BitInputStream(file);
						compressed[i] = new File(file.getPath() + ".hf");
						BitOutputStream out = new BitOutputStream(compressed[i]);
						updater.setGenerator(() -> {
							return (updater.progress() + in.bitsRead()) / (8.0 * sum);
						});
						Processor processor = getProcessor();
						double start = System.currentTimeMillis();
						processor.compress(in, out);
						times[i] = System.currentTimeMillis() - start;
						in.close();
						out.close();
						updater.setProgress(updater.progress() + file.length() * 8.0);
					} catch (Exception e) {
						System.err.println("problem compressing " + file.getName());
					}
				}

				updater.stop();
				updater.updateStatus(Status.Complete, "test complete");
				updater.addContent("Info", getInfo(times, toCompress.toArray(new File[toCompress.size()]), compressed));
			});
			thread.start();
		}

		public class HuffChooser {

			private final File INITIAL_DIRECTORY = new File("data");

			private final Color VALID = Color.DARKGREEN;
			private final Color INVALID = Color.DARKRED;
			private final Color NEUTRAL = Color.BLACK;

			private static final String DEFAULT = "No File Chosen";
			private static final String CHOOSE = "Choose New File";

			private static final double WIDTH = 200;

			private boolean choosingFile;
			private File chosenFile;
			private String label;

			public HuffChooser(boolean choosingFile, String label) {
				this.choosingFile = choosingFile;
				this.label = label;
			}

			public Node render() {
				HBox holder = new HBox();
				holder.setMinWidth(WIDTH);

				Text title = new Text();
				setText(title, label + ": ", NEUTRAL);

				Text fileName = new Text();
				setText(fileName, DEFAULT, INVALID);

				fileName.setOnMouseEntered((entered) -> fadeText(fileName, CHOOSE, NEUTRAL));
				fileName.setOnMouseExited((exited) -> handleExit(fileName));
				fileName.setOnMouseClicked((clicked) -> handleClick(fileName));

				holder.getChildren().addAll(title, fileName);
				return holder;
			}

			public File getChosenFile() {
				return chosenFile;
			}

			private void handleExit(Text fileName) {
				if (chosenFile == null) {
					fadeText(fileName, DEFAULT, INVALID);
				} else {
					fadeText(fileName, chosenFile.getName(), VALID);
				}
			}

			private void handleClick(Text fileName) {
				fileName.setOnMouseExited(null);

				File temp;
				if (choosingFile) {
					FileChooser chooser = new FileChooser();
					chooser.setInitialDirectory(INITIAL_DIRECTORY);
					chooser.setTitle("Choose File");
					temp = chooser.showOpenDialog(new Stage());
				} else {
					DirectoryChooser chooser = new DirectoryChooser();
					chooser.setInitialDirectory(INITIAL_DIRECTORY);
					chooser.setTitle("Choose Folder");
					temp = chooser.showDialog(new Stage());
				}

				if (temp != null) {
					chosenFile = temp;
				}
				handleExit(fileName);

				fileName.setOnMouseExited((exited) -> handleExit(fileName));
			}

			private void setText(Text text, String value, Color fill) {
				text.setText(value);
				text.setFill(fill);
			}

			private void fadeText(Text text, String value, Color fill) {
				FadeTransition fade = new FadeTransition(Duration.millis(50), text);
				fade.setToValue(0.0);
				fade.setOnFinished((finished) -> {
					setText(text, value, fill);
					fade.setToValue(1.0);
					fade.setOnFinished(null);
					fade.play();
				});
				fade.play();
			}
		}

		public class HuffPanel {

			private static final double HEIGHT = 25;
			private static final double ARC = 10;
			private static final double PADDING = 8;

			private double width, height;
			private Map<String, Node> content;
			private ScrollPane contentHolder;
			private String selected;

			public HuffPanel(double width, double height) {
				this.width = width;
				this.height = height;
				content = new HashMap<>();
			}

			public Node render(String[] options) {
				VBox holder = new VBox();
				holder.setMinSize(width, height);
				contentHolder = new ScrollPane();
				contentHolder.setMinSize(width, height);
				contentHolder.setMaxSize(width, height);
				Group menuBar = createMenu(options);
				holder.getChildren().addAll(menuBar, contentHolder);
				return holder;
			}

			private Group createMenu(String[] options) {
				Rectangle background = new Rectangle(width, HEIGHT, Color.DARKGRAY);
				background.setStroke(Color.BLACK);
				background.setArcHeight(ARC);
				background.setArcWidth(ARC);

				HBox titleHolder = new HBox();
				titleHolder.setMinSize(width, HEIGHT);
				titleHolder.setAlignment(Pos.CENTER_LEFT);

				Text title = new Text();
				titleHolder.getChildren().add(title);

				HBox menuHolder = new HBox(PADDING);
				menuHolder.setMinSize(width - PADDING, HEIGHT);
				menuHolder.setAlignment(Pos.CENTER_RIGHT);

				Text[] buttons = new Text[options.length];
				for (int i = 0; i < options.length; i++) {
					buttons[i] = new Text(options[i]);
				}

				selected = options[0];
				for (Text option : buttons) {
					menuHolder.getChildren().add(option);
					if (option.getText() != selected) {
						option.setOpacity(0.5);
					}
					option.setOnMouseClicked((clicked) -> {
						selected = option.getText();
						for (Text t : buttons) {
							FadeTransition fade = new FadeTransition(Duration.millis(50), t);
							fade.setToValue(0.5);
							fade.play();
						}
						FadeTransition fade = new FadeTransition(Duration.millis(50), option);
						fade.setToValue(1.0);
						fade.play();
						contentHolder.setContent(null);
						if (content.containsKey(option.getText())) {
							contentHolder.setContent(content.get(option.getText()));
						}
					});
				}

				Group menuRoot = new Group();
				menuRoot.getChildren().addAll(background, titleHolder, menuHolder);
				return menuRoot;
			}

			public void addContent(String label, Node node) {
				content.put(label, node);
				if (selected == label) {
					contentHolder.setContent(node);
				}
			}

			public void clear() {
				content.clear();
				contentHolder.setContent(null);
			}
		}

		public enum Status {

			Complete(Color.DARKGREEN, "100%"), Working(Color.DARKGRAY, ""), Failed(Color.DARKRED, "  0%");

			private Color color;
			private String percent;

			Status(Color color, String percent) {
				this.color = color;
				this.percent = percent;
			}

			public Color color() {
				return color;
			}

			public String percent() {
				return percent;
			}
		}

		public class StatusBar {

			private static final double OUTER = 33;
			private static final double INNER = 30.5;
			private static final double BUFFER = 1.25;

			private static final double PADDING = 5;

			private double width;

			private Rectangle progress;
			private Text percent;
			private Text status;

			public StatusBar(double width) {
				this.width = width;
			}

			public Node render() {
				VBox holder = new VBox(PADDING);
				holder.setMinWidth(width);

				status = new Text();
				percent = new Text();
				progress = new Rectangle();

				initialize();

				HBox statusHolder = new HBox();
				statusHolder.setMinWidth(width - INNER);
				statusHolder.setMaxWidth(width - INNER);
				statusHolder.setTranslateX(INNER / 2);
				statusHolder.setAlignment(Pos.CENTER_RIGHT);
				statusHolder.getChildren().add(status);

				HBox percentHolder = new HBox();
				percentHolder.setAlignment(Pos.CENTER);
				percentHolder.setMinSize(width, OUTER);
				percentHolder.getChildren().add(percent);

				Group progressBar = new Group();

				Rectangle background = new Rectangle(width, OUTER);
				background.setFill(Color.LIGHTGRAY);
				background.setStroke(Color.BLACK);
				background.setStrokeWidth(BUFFER);
				background.setArcHeight(OUTER);
				background.setArcWidth(OUTER);

				progressBar.getChildren().addAll(background, progress, percentHolder);
				holder.getChildren().addAll(progressBar, statusHolder);
				return holder;
			}

			public void initialize() {
				percent.setText("  0%");
				status.setText("");
				progress.setHeight(INNER);
				progress.setWidth(0);
				progress.setFill(Color.DARKGRAY);
				progress.setTranslateX(BUFFER);
				progress.setArcHeight(INNER);
				progress.setArcWidth(INNER);
			}

			public void setProgress(double percentComplete) {
				double progressWidth = percentComplete * (width - 2 * BUFFER);

				if (progressWidth < INNER) {
					double height = Math.sqrt(Math.pow(INNER, 2) - (Math.pow(progressWidth - INNER, 2)));
					progress.setHeight(height);
					progress.setTranslateY(BUFFER + (INNER - height) / 2);
				} else {
					progress.setHeight(INNER);
					progress.setTranslateY(BUFFER);
				}

				progress.setWidth(progressWidth);
				percent.setText((int) (100 * percentComplete) + "%");
			}

			public void setStatus(Status updatedStatus, String message) {
				if (updatedStatus != Status.Working) {
					progress.setFill(updatedStatus.color());
					progress.setWidth(width - 2 * BUFFER);
					progress.setHeight(INNER);
					progress.setTranslateY(BUFFER);
					percent.setText(updatedStatus.percent());
				}
				FadeTransition fade = new FadeTransition(Duration.millis(50), status);
				fade.setToValue(0.0);
				fade.setOnFinished((finished) -> {
					status.setText(message);
					fade.setToValue(1.0);
					fade.setOnFinished(null);
					fade.play();
				});
				fade.play();
			}
		}
	}