package com.divergencebot.bootstrap;

import java.awt.Font;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.FileChannel;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import javax.swing.JFrame;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;

import LZMA.LzmaInputStream;

public class Bootstrapper extends JFrame {

	private static final long serialVersionUID = 2303420226333746027L;

	private static final Font MONOSPACED = new Font("Monospaced", 0, 12);
	public static final String LAUNCHER_URL = "http://cdn.divergencebot.com/bot.pack.lzma";
	public static final String CHECKSUM_URL = "http://cdn.divergencebot.com/bot.pack.lzma.sha1";

	private static Bootstrapper instance;

	private static final Integer BOOTSTRAP_VERSION = 1;

	private final File workDir;
	private final File launcherJar;
	private final File packedLauncherJar;
	private final File packedLauncherJarNew;

	private final File librariesDir;

	private static boolean forceUpdate;

	static ProgressSplashScreen splash;

	private final JTextArea textArea;
	private final JScrollPane scrollPane;


	public Bootstrapper(File workingDir) {

		super("Divergence Bootstrap Launcher");
		instance = this;

		splash = new ProgressSplashScreen();

		this.workDir = workingDir;
		this.launcherJar = new File(workDir, "bot.jar");
		this.packedLauncherJar = new File(workDir, "bot.pack.lzma");
		this.packedLauncherJarNew = new File(workDir, "bot.pack.lzma.new");

		this.librariesDir = new File(workDir, "libraries");

		setSize(854, 480);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		this.textArea = new JTextArea();
		this.textArea.setLineWrap(true);
		this.textArea.setEditable(false);
		this.textArea.setFont(MONOSPACED);
		((DefaultCaret)this.textArea.getCaret()).setUpdatePolicy(1);

		this.scrollPane = new JScrollPane(this.textArea);
		this.scrollPane.setBorder(null);
		this.scrollPane.setVerticalScrollBarPolicy(22);

		add(this.scrollPane);
		setLocationRelativeTo(null);


		println("Bootstrap started", 5, true, true);

	}

	public static Bootstrapper getInstance() {
		return instance;
	}

	public void execute(boolean force) {

		System.out.println(this.workDir.getPath());
		if (this.packedLauncherJarNew.isFile()) {
			println("Found cached update", 10, true, true);
			renameNew();
		}

		Downloader.Controller controller = new Downloader.Controller();

		String sha1 = getSHA1(this.packedLauncherJar);

		if ((force) || (!this.packedLauncherJar.exists())) {
			println("Forced Update", 15, true, true);
			sha1 = "N/A (Forced Update)";
		}


		Thread thread = new Thread(new Downloader(controller, this, sha1, this.packedLauncherJarNew));
		thread.setName("Launcher downloader");
		thread.start();
		try {
			println("Looking for update", 20, true, true);
			boolean wasInTime = controller.foundUpdateLatch.await(3L, TimeUnit.SECONDS);

			if (controller.foundUpdate.get()) {
				println("Found update in time, waiting to download", 35, true, true);
				controller.hasDownloadedLatch.await();
				renameNew();
			} else if (!wasInTime) {
				println("Didn't find an update in time.", 35, true, true);
			}
		} catch (InterruptedException e) {
			throw new FatalError("Got interrupted: " + e.toString());
		}


		unpack();


		startLauncher(this.launcherJar);
	}

	public static void main(String[] args) throws IOException {
		System.setProperty("java.net.preferIPv4Stack", "true");

		parseArgs(args);


		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (Exception e) {
			System.out.println("Fallback");
			try {
				UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
			} catch (Exception ex) {

			}
		}

		File workingDirectory = Util.getWorkingDirectory();
		if ((workingDirectory.exists()) && (!workingDirectory.isDirectory())) {
			throw new FatalError("Invalid working directory: " + workingDirectory);
		}
		if ((!workingDirectory.exists()) && (!workingDirectory.mkdirs())) {
			throw new FatalError("Unable to create directory: " + workingDirectory);
		}

		Bootstrapper frame = new Bootstrapper(workingDirectory);

		try {
			frame.execute(forceUpdate);
		} catch (Throwable t) {
			splashDispose();
			t.printStackTrace();
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			t.printStackTrace(new PrintStream(outputStream));
			frame.setVisible(true);
			frame.println("FATAL ERROR: " + outputStream.toString());
			frame.println("\nPlease fix the error and restart.");
		}

	}

	@SuppressWarnings("resource")
	public void startLauncher(File launcherJar) {

		CountDownLatch foundUpdateLatch = new CountDownLatch(1);
		try {
			foundUpdateLatch.await(500L, TimeUnit.MILLISECONDS);
		} catch (InterruptedException ignored) {

		}

		List<Dependency> depends = null;
		InputStream depStream = null;
		try (JarFile jar = new JarFile(launcherJar)) {
			JarEntry entry = jar.getJarEntry("bot.depends");
			if (entry == null) {
		
			}
			depStream = jar.getInputStream(entry);
			depends = DependencyDownloader.getDependencies(depStream);

		} catch (IOException e) {
			e.printStackTrace();
			throw new FatalError("Could not find bot.depends in Jar: " + e);
		}

		Class<?> aClass;
		Constructor<?> constructor;
		try {

			if (depends != null) {

				println("Downloading Dependencies", 65, true, true);
				int inc = 30 / depends.size();
				int pos = 70;
				for (Dependency dep : depends) {

					println("Downloading Dependency: " + dep.getArtifactId(), pos, true, true);

					DependencyDownloader.downloadArtifact(librariesDir, dep);

					File file = DependencyDownloader.getFilePath(librariesDir, dep);
					if (file.exists() && file.isFile()) {
						ClassPathUtil.addFile(file);
					}
					pos += inc;
				}
			}

			println("Starting launcher.", 100, true, true);

			//TODO: Change to new class
			aClass = new URLClassLoader(new URL[] { launcherJar.toURI().toURL() })
					.loadClass("com.divergencebot.bot.BotEntrance");
			constructor = aClass.getConstructor(new Class[] { JFrame.class, File.class, Integer.class});
			splashDispose();

			constructor.newInstance(new Object[] { this, this.workDir, BOOTSTRAP_VERSION});
		} catch (Exception e) {
			e.printStackTrace();
			throw new FatalError("Unable to start: " + e);

		}
	}

	protected static File makeSubDirPath(File basepath, String subdir) {

		File path = new File(basepath, subdir);
		if (!path.exists() && !path.mkdirs()) {
			throw new RuntimeException((new StringBuilder()).append("The working directory could not be created: ")
					.append(path).toString());
		}
		return path;
	}

	public void println(String string) {
		print(string + "\n");
	}

	public void println(String string, int percent, boolean splashShow, boolean logshow) {
		if (splashShow && splash != null && splash.isDisplayable()) {
			splash.updateText(string);
			splash.updateProgress(percent);
		}
		if (logshow) {
			print(string + "\n");
		}
	}

	public void print(String string) {
		System.out.print(string);


		Document document = this.textArea.getDocument();
		final JScrollBar scrollBar = this.scrollPane.getVerticalScrollBar();

		boolean shouldScroll = (scrollBar.getValue() + scrollBar.getSize().getHeight() +
				MONOSPACED.getSize() * 2 > scrollBar.getMaximum());
		try {
			document.insertString(document.getLength(), string, null);
		} catch (BadLocationException ignored) {

		}

		if (shouldScroll) {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					scrollBar.setValue(2147483647);
				}
			});
		}
	}

	public void renameNew() {
		if ((this.packedLauncherJar.exists()) && (!this.packedLauncherJar.isFile()) &&
				(!this.packedLauncherJar.delete())) {
			throw new FatalError("while renaming, target path: " +
					this.packedLauncherJar.getAbsolutePath() + " is not a file and we failed to delete it");
		}

		if (this.packedLauncherJarNew.isFile()) {
			println("Renaming " + this.packedLauncherJarNew.getName() + " to " + this.packedLauncherJar.getName());


			if (this.packedLauncherJarNew.renameTo(this.packedLauncherJar)) {
				println("Renamed successfully.");
			} else {
				if ((this.packedLauncherJar.exists()) && (!this.packedLauncherJar.canWrite())) {
					throw new FatalError("unable to rename: target" +
							this.packedLauncherJar.getAbsolutePath() + " not writable");
				}

				println("Unable to rename - could be on another filesystem, trying copy & delete.");

				if ((this.packedLauncherJarNew.exists()) && (this.packedLauncherJarNew.isFile())) {
					try {
						copyFile(this.packedLauncherJarNew, this.packedLauncherJar);
						if (this.packedLauncherJarNew.delete()) {
							println("Copy & delete succeeded.");
						} else {
							println("Unable to remove " + this.packedLauncherJarNew.getAbsolutePath() +
									" after copy.");
						}
					} catch (IOException e) {
						throw new FatalError("unable to copy:" + e);
					}
				} else {
					println("Nevermind... file vanished?");
				}
			}

		}
	}

	@SuppressWarnings("resource")
	public static void copyFile(File source, File target) throws IOException {
		if (!target.exists()) {
			target.createNewFile();
		}

		FileChannel sourceChannel = null;
		FileChannel targetChannel = null;
		try {
			sourceChannel = new FileInputStream(source).getChannel();
			targetChannel = new FileOutputStream(target).getChannel();
			targetChannel.transferFrom(sourceChannel, 0L, sourceChannel.size());
		} finally {
			if (sourceChannel != null) {
				sourceChannel.close();
			}

			if (targetChannel != null) {
				targetChannel.close();
			}
		}
	}

	public String getSHA1(File file) {
		DigestInputStream stream = null;
		try {
			stream = new DigestInputStream(new FileInputStream(file), MessageDigest.getInstance("SHA1"));
			byte[] buffer = new byte[65536];

			int read = stream.read(buffer);
			while (read >= 1) {
				read = stream.read(buffer);
			}

		} catch (Exception ignored) {
			return "";
		} finally {
			closeSilently(stream);
		}

		return String.format("%1$040x", new Object[] { new BigInteger(1, stream.getMessageDigest().digest()) });
	}


	public void unpack() {
		File lzmaUnpacked = getUnpackedLzmaFile(this.packedLauncherJar);
		InputStream inputHandle = null;
		OutputStream outputHandle = null;

		println("Reversing LZMA on " + this.packedLauncherJar.getName() + " to " + lzmaUnpacked.getName());
		println("Reversing LZMA", 50, true, false);
		try {
			inputHandle = new LzmaInputStream(new FileInputStream(this.packedLauncherJar));
			outputHandle = new FileOutputStream(lzmaUnpacked);

			byte[] buffer = new byte[65536];

			int read = inputHandle.read(buffer);
			while (read >= 1) {
				outputHandle.write(buffer, 0, read);
				read = inputHandle.read(buffer);
			}
		} catch (Exception e) {
			throw new FatalError("Unable to un-lzma: " + e);
		} finally {
			closeSilently(inputHandle);
			closeSilently(outputHandle);
		}

		println("Unpacking " + lzmaUnpacked.getName() + " to " + this.launcherJar.getName());
		println("Unpacking Jar", 55, true, false);

		JarOutputStream jarOutputStream = null;
		try {
			jarOutputStream = new JarOutputStream(new FileOutputStream(this.launcherJar));
			Pack200.newUnpacker().unpack(lzmaUnpacked, jarOutputStream);
		} catch (Exception e) {
			throw new FatalError("Unable to un-pack200: " + e);
		} finally {
			closeSilently(jarOutputStream);
		}

		println("Cleaning up " + lzmaUnpacked.getName());
		println("Cleaning up", 60, true, false);

		lzmaUnpacked.delete();
	}

	private File getUnpackedLzmaFile(File packedLauncherJar) {
		String filePath = packedLauncherJar.getAbsolutePath();
		if (filePath.endsWith(".lzma")) {
			filePath = filePath.substring(0, filePath.length() - 5);
		}
		return new File(filePath);
	}

	public static void closeSilently(Closeable closeable) {
		if (closeable != null) {
			try {
				closeable.close();
			} catch (IOException ignored) {

			}
		}
	}

	public static boolean stringHasValue(String string) {
		return (string != null) && (!string.isEmpty());
	}

	public static void parseArgs(String[] args) {

		/*
		 * --force
		 * --help
		 * --proxyhost <host>
		 * --proxyport <port>
		 * --proxyuser <user>
		 * --proxypass <pass>
		 */

		System.out.println("DivergenceBot Bootstrap Launcher");
		System.out.println();

		boolean error = false;
		int i = 0;
		for ( ; i < args.length; ) {

			if (args[i].equalsIgnoreCase("-help") || args[i].equalsIgnoreCase("--help")) {
				displayHelp();
			} else if (args[i].equalsIgnoreCase("-force") || args[i].equalsIgnoreCase("--force")) {
				forceUpdate = true;
				System.out.println("Forcing Launcher Update");
			} else {
				System.out.println("Unknown argument: " + args[i]);
				System.out.println("Displaying Help..");
				System.out.println();
				error = true;
				break;
			}

			i++;

		}

		if (error) {
			displayHelp();
		}

	}

	private static void displayHelp() {
		System.out.println("Option                  Description");
		System.out.println("------                  -----------");
		System.out.println("--force                 Force update");
		System.out.println("--help                  Display help");
		System.out.println();
		System.exit(0);
	}

	public static boolean isInteger(String input) {
		try {
			Integer.parseInt(input);
			return true;
		} catch (Exception ex) {
			return false;
		}
	}

	public static void splashDispose() {
		if (splash != null && splash.isDisplayable()) {
			splash.dispose();
			splash = null;
		}
	}

}