/*
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lwjgl.util.applet;

import org.lwjgl.LWJGLUtil;

import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.image.ImageObserver;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.SocketPermission;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedExceptionAction;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * <p>
 * The AppletLoader enables deployment of LWJGL to applets in an easy
 * and polished way. The loader will display a configurable logo and progressbar
 * while the relevant jars (generic and native) are downloaded from a specified source.
 * </p>
 * <p>
 * The downloaded jars are extracted to the users temporary directory - and if enabled, cached for
 * faster loading in future uses.
 * </p>
 * <p>
 * The following applet parameters are required:
 * <ul>
 * <li>al_main - [String] Full package and class the applet to instantiate and display when loaded.</li>
 * <li>al_jars - [String] Comma separated list of jars to download.</li>
 * <p>
 * <li>al_windows - [String] Jar containing native files for windows.</li>
 * <li>al_linux - [String] Jar containing native files for linux.</li>
 * <li>al_mac - [String] Jar containing native files for mac.</li>
 * <li>al_solaris - [String] Jar containing native files for solaris.</li>
 * <li>al_freebsd - [String] Jar containing native files for freebsd.</li>
 * <li>al_openbsd - [String] Jar containing native files for openbsd.</li>
 * </ul>
 * </p>
 * <p>
 * Additionally the following parameters can be supplied to tweak the behaviour of the AppletLoader.
 * <ul>
 * <li>al_cache - [boolean] Whether to use cache system. <i>Default: true</i>.</li>
 * <li>al_version - [int or float] Version of deployment. If this is specified, the jars will be cached and
 * reused if the version matches. If version doesn't match all of the files are reloaded.</li>
 * 
 * <li>al_debug - [boolean] Whether to enable debug mode. <i>Default: false</i>.</li>
 * <li>al_min_jre - [String] Specify the minimum jre version that the applet requires, should be in format like 1.6.0_24 or a subset like 1.6 <i>Default: 1.5</i>.</li>
 * <li>al_prepend_host - [boolean] Whether to limit caching to this domain, disable if your applet is hosted on multiple domains and needs to share the cache. <i>Default: true</i>.</li>
 * <li>al_lookup_threads - [int] Specify the number of concurrent threads to use to get file information before downloading. <i>Default: 1</i>.</li>
 * <p>
 * <li>al_windows64 - [String] If specified it will be used instead of al_windows on 64bit windows systems.</li>
 * <li>al_windows32 - [String] If specified it will be used instead of al_windows on 32bit windows systems.</li>
 * <li>al_linux64 - [String] If specified it will be used instead of al_linux on 64bit linux systems.</li>
 * <li>al_linux32 - [String] If specified it will be used instead of al_linux on 32bit linux systems.</li>
 * <li>al_mac32 - [String] If specified it will be used instead of al_mac on 64bit mac systems.</li>
 * <li>al_mac64 - [String] If specified it will be used instead of al_mac on 32bit mac systems.</li>
 * <li>al_macppc - [String] If specified it will be used instead of al_mac on PPC mac systems.</li>
 * <p>
 * <li>boxbgcolor - [String] any String AWT color ("red", "blue", etc), RGB (0-255) or hex formated color (#RRGGBB) to use as background. <i>Default: #ffffff</i>.</li>
 * <li>boxfgcolor - [String] any String AWT color ("red", "blue", etc), RGB (0-255) or hex formated color (#RRGGBB) to use as foreground. <i>Default: #000000</i>.</li>
 * <p>
 * <li>al_logo - [String Path of of the logo resource to paint while loading.<i>Default: "appletlogo.gif"</i>.</li>
 * <li>al_progressbar - [String] Path of the progressbar resource to paint on top of the logo, width clipped by percentage.<i>Default: "appletprogress.gif"</i>.</li>
 * <p>
 * <li>lwjgl_arguments - </li> [String] used to pass LWJGL parameters to LWJGL e.g. ("-Dorg.lwjgl.input.Mouse.allowNegativeMouseCoords=true -Dorg.lwjgl.util.Debug=true").</li>
 * </ul>
 * </p>
 * @author kappaOne <one.kappa@gmail.com>
 * @author Brian Matzon <brian@matzon.dk>
 * @version $Revision$
 * $Id$
 * 
 * Contributors:
 * <ul>
 * <li>Arielsan</li>
 * <li>Bobjob</li>
 * <li>Dashiva</li>
 * <li>Dr_evil</li>
 * <li>Elias Naur</li>
 * <li>Kevin Glass</li>
 * <li>Matthias Mann</li>
 * <li>Mickelukas</li>
 * <li>NateS</li>
 * <li>Pelle Johnsen</li>
 * <li>Riven</li>
 * <li>Ruben01</li>
 * <li>Shannon Smith</li>
 * </ul>
 * 
 */
public class AppletLoader extends Applet implements Runnable, AppletStub {

	/** initializing */
	public static final int STATE_INIT 						= 1;
	
	/** checking version of jre */
	public static final int STATE_CHECK_JRE_VERSION			= 2;

	/** determining which packages that are required */
	public static final int STATE_DETERMINING_PACKAGES 		= 3;

	/** checking for already downloaded files */
	public static final int STATE_CHECKING_CACHE 			= 4;
	
	/** checking if any updates are available for cache files */
	public static final int STATE_CHECKING_FOR_UPDATES		= 5;

	/** downloading packages */
	public static final int STATE_DOWNLOADING 				= 6;

	/** extracting packages */
	public static final int STATE_EXTRACTING_PACKAGES 		= 7;
	
	/** validating packages */
	public static final int STATE_VALIDATING_PACKAGES 		= 8;

	/** updating the classpath */
	public static final int STATE_UPDATING_CLASSPATH 		= 9;

	/** switching to real applet */
	public static final int STATE_SWITCHING_APPLET 			= 10;

	/** initializing real applet */
	public static final int STATE_INITIALIZE_REAL_APPLET	= 11;

	/** stating real applet */
	public static final int STATE_START_REAL_APPLET 		= 12;

	/** done */
	public static final int STATE_DONE 						= 13;

	/** used to calculate length of progress bar */
	protected volatile int percentage;

	/** total size of download in bytes */
	protected int		totalDownloadSize;

	/** current size of extracted in bytes */
	protected int		currentSizeExtract;

	/** total size of extracted in bytes */
	protected int		totalSizeExtract;

	/** logo to be shown while loading */
	protected Image		logo, logoBuffer;

	/** progressbar to render while loading */
	protected Image		progressbar, progressbarBuffer;

	/** offscreen image used */
	protected Image 	offscreen;

	/** set to true while painting is done */
	protected boolean 	painting;

	/** background color of applet */
	protected Color		bgColor 	= Color.white;

	/** color to write foreground in */
	protected Color		fgColor 	= Color.black;

	/** urls of the jars to download */
	protected URL[]		urlList;

	/** classLoader used to add downloaded jars to the classpath */
	protected ClassLoader classLoader;

	/** actual thread that does the loading */
	protected Thread	loaderThread;

	/** animation thread that renders our load screen while loading */
	protected Thread 	animationThread;

	/** applet to load after all downloads are complete */
	protected Applet	lwjglApplet;

	/** whether we're running in debug mode */
	protected boolean 	debugMode;

	/** whether to prepend host to cache path */
	protected boolean 	prependHost;

	/** Used to store file names with lastModified time */
	protected HashMap<String, Long> 	filesLastModified;

	/** Sizes of files to download */
	protected int[] 	fileSizes;
	
	/** Number of native jars */
	protected int		nativeJarCount;

	/** whether to use caching system, only download files that have changed */
	protected boolean 	cacheEnabled;

	/** String to display as a subtask */
	protected String	subtaskMessage = "";

	/** state of applet loader */
	protected volatile int state = STATE_INIT;

	/** whether lzma is supported */
	protected boolean 	lzmaSupported;

	/** whether pack200 is supported */
	protected boolean 	pack200Supported;
	
	/** whether to run in headless mode */
	protected boolean 	headless = false;
	
	/** whether to switch applets in headless mode or wait longer */
	protected boolean 	headlessWaiting = true;

	/** messages to be passed via liveconnect in headless mode */
	protected String[] 	headlessMessage;
	
	/** threads to use when fetching information of files to be downloaded */
	protected int 		concurrentLookupThreads;
	
	/** whether a fatal error occurred */
	protected boolean	fatalError;
	
	/** whether a certificate refused error occurred */
	protected boolean	certificateRefused;
	
	/** whether the minimum required JRE version is not found */
	protected boolean	minimumJreNotFound;

	/** generic error message to display on error */
	protected String[] 	genericErrorMessage = {	"An error occured while loading the applet.",
												"Please contact support to resolve this issue.",
												"<placeholder for error message>"};

	/** error message to display if user refuses to accept certificate*/
	protected String[] 	certificateRefusedMessage = { "Permissions for Applet Refused.",
												      "Please accept the permissions dialog to allow",
												      "the applet to continue the loading process."};
	
	/** error message to display if minimum JRE version is not met */
	protected String[] 	minimumJREMessage = { "Your version of Java is out of date.",
											  "Visit java.com to get the latest version.",
											  "Java <al_min_jre> or greater is required."};
	
	/** fatal error message to display */
	protected String[]	errorMessage;
	
	/** have natives been loaded by another instance of this applet */
	protected static boolean natives_loaded;
	
	/*
	 * @see java.applet.Applet#init()
	 */
	public void init() {
		setState(STATE_INIT);
		
		// sanity check
		String[] requiredArgs = {"al_main", "al_jars"};
		for ( String requiredArg : requiredArgs ) {
			if ( getParameter(requiredArg) == null ) {
				fatalErrorOccured("missing required applet parameter: " + requiredArg, null);
				return;
			}
		}
		
		// whether to use cache system
		cacheEnabled	= getBooleanParameter("al_cache", true);

		// whether to run in debug mode
		debugMode 		= getBooleanParameter("al_debug", false);

		// whether to prepend host to cache path
		prependHost 	= getBooleanParameter("al_prepend_host", true);

		// whether to run in headless mode
		headless		= getBooleanParameter("al_headless", false);
		
		// obtain the number of concurrent lookup threads to use
		concurrentLookupThreads = getIntParameter("al_lookup_threads", 1); // defaults to 1
		
		// get colors of applet
		bgColor 		= getColor("boxbgcolor", Color.white);
		setBackground(bgColor);
		fgColor 		= getColor("boxfgcolor", Color.black);

		if (!headless) {
			// load logos
			logo 		= getImage(getStringParameter("al_logo", "appletlogo.gif"));
			progressbar = getImage(getStringParameter("al_progressbar", "appletprogress.gif"));
		}

		// check for lzma support
		try {
			Class.forName("LZMA.LzmaInputStream");
			lzmaSupported = true;
		} catch (Throwable e) {
			/* no lzma support */
		}

		// check pack200 support
		try {
			java.util.jar.Pack200.class.getSimpleName();
			pack200Supported = true;
		} catch (Throwable e) {
			/* no pack200 support */
		}
	}

	/**
	 * Generates a stacktrace in the form of a string
	 * @param exception Exception to make stacktrace of
	 * @return Stacktrace of exception in the form of a string
	 */
	private static String generateStacktrace(Exception exception) {
		Writer result = new StringWriter();
		PrintWriter printWriter = new PrintWriter(result);
		exception.printStackTrace(printWriter);
		return result.toString();
	}

	/*
	 * @see java.applet.Applet#start()
	 */
	public void start() {
		if (lwjglApplet != null) {
			lwjglApplet.start();
		}
		else {
			if(loaderThread == null && !fatalError) {
				loaderThread = new Thread(this);
				loaderThread.setName("AppletLoader.loaderThread");
				loaderThread.start();

				if (!headless) {
					animationThread = new Thread() {
						public void run() {
							while(loaderThread != null) {
								repaint();
								AppletLoader.this.sleep(100);
							}
							animationThread = null;
						}
					};
					animationThread.setName("AppletLoader.animationthread");
					animationThread.start();
				}
			}
		}
	}

	/*
	 * @see java.applet.Applet#stop()
	 */
	public void stop() {
		if (lwjglApplet != null) {
			lwjglApplet.stop();
		}
	}

	/*
	 * @see java.applet.Applet#destroy()
	 */
	public void destroy() {
		if (lwjglApplet != null) {
			lwjglApplet.destroy();
		}
	}

	/**
	 * Clean up resources
	 */
	protected void cleanUp() {
		progressbar = null;
		logo 		= null;

		logoBuffer = null;
		progressbarBuffer = null;

		offscreen = null;
	}

	/**
	 * Retrieves the applet that has been loaded. Useful for liveconnect.
	 */
	public Applet getApplet() {
		return lwjglApplet;
	}

	/**
	 * Retrieves the current status of the AppletLoader and is 
	 * used by liveconnect when running in headless mode.
	 * 
	 * This method will return the current progress of the AppletLoader
	 * as a value from 0-100. In the case of a fatal error it will
	 * return -1. If the certificate is refused it will return -2.
	 * If the minimum jre requirement is not met will return -3.
	 * 
	 * When method returns 100 the AppletLoader will sleep until the 
	 * method is called again. When called again it will switch to the
	 * LWJGL Applet. This is a useful trigger to start the LWJGL applet 
	 * when needed.
	 */
	public int getStatus() {
		if (fatalError) {
			headlessMessage = errorMessage;
			
			if (certificateRefused) return -2;
			if (minimumJreNotFound) return -3;
			return -1;
		}
		
		if (percentage == 100 && headlessWaiting) {
			headlessWaiting = false;
		}
		
		if (percentage == 95) {
			percentage = 100; // ready to switch applet
		}
		
		String[] message = {getDescriptionForState(), subtaskMessage};
		headlessMessage = message;
		
		return percentage;
	}
	
	/**
	 * Retrieves the current message for the current status. 
	 * Used by liveconnect when running in headless mode.
	 */
	public String[] getMessages() {
		return headlessMessage;
	}
	
	/**
	 * Transfers the call of AppletResize from the stub to the lwjglApplet.
	 */
	public void appletResize(int width, int height) {
		resize(width, height);
	}

	/*
	 * @see java.awt.Container#update(java.awt.Graphics)
	 */
	public final void update(Graphics g) {
		paint(g);
	}

	/*
	 * @see java.awt.Container#paint(java.awt.Graphics)
	 */
	public void paint(Graphics g) {
		// don't paint loader if applet loaded
		if(state == STATE_DONE) {
			cleanUp(); // clean up resources
			return;
		}
		
		// no drawing in headless mode
		if (headless) return;

		// create offscreen if missing
		if (offscreen == null) {
			offscreen = createImage(getWidth(), getHeight());

			// create buffers for animated gifs
			if (logo != null) {
				logoBuffer = createImage(logo.getWidth(null), logo.getHeight(null));
				// add image observer, it will notify when next animated gif frame is ready
				offscreen.getGraphics().drawImage(logo, 0, 0, this);
				// in case image is not animated fill image buffer once
				imageUpdate(logo, ImageObserver.FRAMEBITS, 0, 0, 0, 0);
			}

			if (progressbar != null) {
				progressbarBuffer = createImage(progressbar.getWidth(null), progressbar.getHeight(null));
				// add image observer, it will notify when next animated gif frame is ready
				offscreen.getGraphics().drawImage(progressbar, 0, 0, this);
				// in case image is not animated fill image buffer once
				imageUpdate(progressbar, ImageObserver.FRAMEBITS, 0, 0, 0, 0);
			}
		}

		// draw everything onto an image before drawing to avoid flicker
		Graphics og = offscreen.getGraphics();
		FontMetrics fm = og.getFontMetrics();

		// clear background color
		og.setColor(bgColor);
		og.fillRect(0, 0, offscreen.getWidth(null), offscreen.getHeight(null));

		og.setColor(fgColor);
		
		// if we had a failure of some sort, notify the user
		if (fatalError) {
			for(int i=0; i<errorMessage.length; i++) {
				if(errorMessage[i] != null) {
					int messageX = (offscreen.getWidth(null) - fm.stringWidth(errorMessage[i])) / 2;
					int messageY = (offscreen.getHeight(null) - (fm.getHeight() * errorMessage.length)) / 2;

					og.drawString(errorMessage[i], messageX, messageY + i*fm.getHeight());
				}
			}
		} else {
			og.setColor(fgColor);

			painting = true;

			// get position at the middle of the offscreen buffer
			int x = offscreen.getWidth(null)/2;
			int y = offscreen.getHeight(null)/2;

			// draw logo
			if (logo != null) {
				og.drawImage(logoBuffer, x-logo.getWidth(null)/2, y-logo.getHeight(null)/2, this);
			}

			// draw message
			String message = getDescriptionForState();
			
			int messageX = (offscreen.getWidth(null) - fm.stringWidth(message)) / 2;
			int messageY = y + 20;

			if (logo != null) messageY += logo.getHeight(null)/2;
			else if (progressbar != null) messageY += progressbar.getHeight(null)/2;

			og.drawString(message, messageX, messageY);

			// draw subtaskmessage, if any
			if(subtaskMessage.length() > 0) {
				messageX = (offscreen.getWidth(null) - fm.stringWidth(subtaskMessage)) / 2;
				og.drawString(subtaskMessage, messageX, messageY+20);
			}

			// draw loading progress bar, clipping it depending on percentage done
			if (progressbar != null) {
				int barSize = (progressbar.getWidth(null) * percentage) / 100;
				og.clipRect(x-progressbar.getWidth(null)/2, 0, barSize, offscreen.getHeight(null));
				og.drawImage(progressbarBuffer, x-progressbar.getWidth(null)/2, y-progressbar.getHeight(null)/2, this);
			}

			painting = false;
		}

		og.dispose();

		// finally draw it all centred
		g.drawImage(offscreen, (getWidth() - offscreen.getWidth(null))/2, (getHeight() - offscreen.getHeight(null))/2, null);
	}

	/**
	 * When an animated gif frame is ready to be drawn the ImageObserver
	 * will call this method.
	 *
	 * The Image frame is copied into a buffer, which is then drawn.
	 * This is done to prevent image tearing on gif animations.
	 */
	public boolean imageUpdate(Image img, int flag, int x, int y, int width, int height) {

		// finish with this ImageObserver
		if (state == STATE_DONE) return false;

		// if image frame is ready to be drawn and is currently not being painted
		if (flag == ImageObserver.FRAMEBITS && !painting) {
			Image buffer;

			// select which buffer to fill
			if (img == logo) buffer = logoBuffer;
			else buffer = progressbarBuffer;

			Graphics g = buffer.getGraphics();

			// clear background on buffer
			g.setColor(bgColor);
			g.fillRect(0, 0, buffer.getWidth(null), buffer.getHeight(null));

			// buffer background is cleared, so draw logo under progressbar
			if (img == progressbar && logo != null) {
				g.drawImage(logoBuffer, progressbar.getWidth(null)/2-logo.getWidth(null)/2,
										progressbar.getHeight(null)/2-logo.getHeight(null)/2, null);
			}

			g.drawImage(img, 0, 0, this);
			g.dispose();

			repaint();
		}

		return true;
	}

	/**
	 * @return string describing the state of the loader
	 */
	protected String getDescriptionForState() {
		switch (state) {
			case STATE_INIT:
				return "Initializing loader";
			case STATE_CHECK_JRE_VERSION:
				return "Checking version";
			case STATE_DETERMINING_PACKAGES:
				return "Determining packages to load";
			case STATE_CHECKING_CACHE:
				return "Calculating download size";
			case STATE_CHECKING_FOR_UPDATES:
				return "Checking for updates";
			case STATE_DOWNLOADING:
				return "Downloading packages";
			case STATE_EXTRACTING_PACKAGES:
				return "Extracting downloaded packages";
			case STATE_VALIDATING_PACKAGES:
				return "Validating packages";
			case STATE_UPDATING_CLASSPATH:
				return "Updating classpath";
			case STATE_SWITCHING_APPLET:
				return "Switching applet";
			case STATE_INITIALIZE_REAL_APPLET:
				return "Initializing real applet";
			case STATE_START_REAL_APPLET:
				return "Starting real applet";
			case STATE_DONE:
				return "Done loading";
			default:
				return "unknown state";
		}
	}

	/**
	 * Trims the passed file string based on the available capabilities
	 * @param file string of files to be trimmed
	 * @return trimmed string based on capabilities of client
	 */
	protected String trimExtensionByCapabilities(String file) {
		if (!pack200Supported) {
			file = file.replace(".pack", "");
		}

		if (!lzmaSupported && file.endsWith(".lzma")) {
			file = file.replace(".lzma", "");
			System.out.println("LZMA decoder (lzma.jar) not found, trying " + file + " without lzma extension.");
		}
		return file;
	}

	/**
	 * Reads list of jars to download and adds the urls to urlList
	 * also finds out which OS you are on and adds appropriate native
	 * jar to the urlList
	 */
	protected void loadJarURLs() throws Exception {
		setState(STATE_DETERMINING_PACKAGES);

		// jars to load
		String jarList = getParameter("al_jars");
		String nativeJarList = null;
		
		String osName = System.getProperty("os.name");
		
		if (osName.startsWith("Win")) {

			// check if arch specific natives have been specified
			if (System.getProperty("os.arch").endsWith("64")) {
				nativeJarList = getParameter("al_windows64");
			} else {
				nativeJarList = getParameter("al_windows32");
			}

			if (nativeJarList == null) {
				nativeJarList = getParameter("al_windows");
			}

		} else if (osName.startsWith("Linux") || osName.startsWith("Unix")) {

			// check if arch specific natives have been specified
			if (System.getProperty("os.arch").endsWith("64")) {
				nativeJarList = getParameter("al_linux64");
			} else {
				nativeJarList = getParameter("al_linux32");
			}

			if (nativeJarList == null) {
				nativeJarList = getParameter("al_linux");
			}

		} else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) {
			
			// check if arch specific natives have been specified
			if (System.getProperty("os.arch").endsWith("64")) {
				nativeJarList = getParameter("al_mac64");
			} else if (System.getProperty("os.arch").contains("ppc")) {
				nativeJarList = getParameter("al_macppc");
			} else {
				nativeJarList = getParameter("al_mac32");
			}

			if (nativeJarList == null) {
				nativeJarList = getParameter("al_mac");
			}
			
		} else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) {
			nativeJarList = getParameter("al_solaris");
		} else if (osName.startsWith("FreeBSD")) {
			nativeJarList = getParameter("al_freebsd");
		} else if (osName.startsWith("OpenBSD")) {
			nativeJarList = getParameter("al_openbsd");
		} else {
			fatalErrorOccured("OS (" + osName + ") not supported", null);
			return;
		}
		
		if (nativeJarList == null) {
			fatalErrorOccured("no lwjgl natives files found", null);
			return;
		} 
		
		jarList = trimExtensionByCapabilities(jarList);
		StringTokenizer jars = new StringTokenizer(jarList, ", ");

		nativeJarList = trimExtensionByCapabilities(nativeJarList);
		StringTokenizer nativeJars = new StringTokenizer(nativeJarList, ", ");
		
		int jarCount = jars.countTokens();
		nativeJarCount = nativeJars.countTokens();
		
		urlList = new URL[jarCount+nativeJarCount];

		URL path = getCodeBase();

		// set jars urls
		for (int i = 0; i < jarCount; i++) {
			urlList[i] = new URL(path, jars.nextToken());
		}

		for (int i = jarCount; i < jarCount+nativeJarCount; i++) {
			urlList[i] = new URL(path, nativeJars.nextToken());
		}
	}

	/**
	 * 9 steps
	 *
	 * 1) check jre version meets minimum requirements
	 * 2) check applet cache and decide which jars to download
	 * 3) download the jars
	 * 4) extract native files
	 * 5) validate jars for any corruption
	 * 6) save applet cache information
	 * 7) add jars to class path
	 * 8) set any lwjgl properties
	 * 9) switch to loaded applet
	 */
	public void run() {
		percentage = 5;

 		try {
			debug_sleep(2000);
			
			// check JRE version meets minimum requirements
			if (!isMinJREVersionAvailable()) {
				minimumJreNotFound = true;
				fatalErrorOccured("Java " + getStringParameter("al_min_jre", "1.5") + " or greater is required.", null);
				return;
			}

			// parse the urls for the jars into the url list
			loadJarURLs();

			// get path where applet files will be stored
			String path = getCacheDirectory();

			File dir = new File(path);

			// create directory
			if (!dir.exists()) {
				dir.mkdirs();
			}

			File versionFile = new File(dir, "version");

			// if specified applet version already available don't download anything
			boolean versionAvailable = false;

			// version string of applet
			String version = getParameter("al_version");

			// if applet version specifed, compare with version in the cache
			if (version != null) {
				versionAvailable = compareVersion(versionFile, version.toLowerCase());
			}

			// if jars not available or need updating download them
			if (!versionAvailable) {
				// get jars file sizes and check cache
				getJarInfo(dir);		// 5-15%

				// downloads jars from the server
				downloadJars(path);		// 15-55%

				// Extract Pack and LZMA files
				extractJars(path);		// 55-65%

				// Extracts Native Files
				extractNatives(path);	// 65-80%
				
				// Validate Jars		// 80-90%
				validateJars(path);

				// save version information once jars downloaded successfully
				if (version != null) {
					percentage = 90;
					writeObjectFile(versionFile, version.toLowerCase());
				}

				// save file names with last modified info once downloaded successfully
				writeObjectFile(new File(dir, "timestamps"), filesLastModified);
			}

			// add the downloaded jars and natives to classpath
			updateClassPath(path);
			
			// set lwjgl properties
			setLWJGLProperties();

			// if headless mode then sleep, until told to continue
			if (headless) {
				while(headlessWaiting) {
					Thread.sleep(100);
				}
			}
			
			// make applet switch on the EDT as an AWT/Swing permission dialog could be called
			EventQueue.invokeAndWait(new Runnable() {
	            public void run() {
					try {
						switchApplet();
					} catch (Exception e) {
						fatalErrorOccured("This occurred while '" + getDescriptionForState() + "'", e);
					}
					setState(STATE_DONE);
					repaint();
	            }
			});
			
		} catch (Exception e) {
			certificateRefused = e instanceof AccessControlException;
			fatalErrorOccured("This occurred while '" + getDescriptionForState() + "'", e);
		} finally {
			loaderThread = null;
		}
	}
	
	/**
	 * When this method is supplied with a JRE version it will compare it to the
	 * current JRE version.
	 * 
	 * minimum requried JRE version is set using al_min_jre parameter, if not 
	 * this is not set then the value will default to version 1.5
	 * 
	 * The minimumVersion should follow a structure such as x.x.x_x
	 * Example values would include 1.6.0_10 or a subset like 1.6.0 or 1.6
	 * 
	 * @return returns true if the available version is greater or equal to the 
	 * minimum version required
	 * 
	 * @throws Exception a NumberFormatException is thrown if the string is not valid
	 */
	public boolean isMinJREVersionAvailable() throws Exception {
		setState(STATE_CHECK_JRE_VERSION);
		
		String minimumVersion = getStringParameter("al_min_jre", "1.5");
		String javaVersion = System.getProperty("java.version");
		
		// remove dash and anything after it (letters) from version string e.g. 1.5.0_01-ea
		minimumVersion = javaVersion.split("-")[0];
		javaVersion = minimumVersion.split("-")[0];
		
		// split version string into a string arrays
		String[] jvmVersionData = javaVersion.split("[_\\.]");
		String[] minVersionData = minimumVersion.split("[_\\.]");
		
		int maxLength = Math.max(jvmVersionData.length, minVersionData.length);
		
		// convert string arrays into int arrays
		int[] jvmVersion = new int[maxLength];
		int[] minVersion = new int[maxLength];
		
		for (int i = 0; i < jvmVersionData.length; i++) {
			jvmVersion[i] = Integer.parseInt(jvmVersionData[i]);
		}
		
		for (int i = 0; i < minVersionData.length; i++) {
			minVersion[i] = Integer.parseInt(minVersionData[i]);
		}
		
		// compare versions
		for (int i = 0; i < maxLength; i++) {
			if (jvmVersion[i] < minVersion[i]) return false; // minVersion is greater then jvmVersion
		}
		
		return true;
	}
	
	/**
	 * This method will return true if the version stored in the file
	 * matches the supplied String version.
	 * 
	 * @param versionFile - location to file containing version information
	 * @param version - String version that needs to be compared
	 * @return returns true if the version in file matches specified version
	 */
	protected boolean compareVersion(File versionFile, String version) {
		// if version file exists
		if (versionFile.exists()) {
			String s = readStringFile(versionFile);
			
			// compare to version with file
			if (s != null && s.equals(version)) {
				percentage = 90; // not need to download cache files again

				if(debugMode) {
					System.out.println("Loading Cached Applet Version: " + version);
				}
				debug_sleep(2000);
				
				return true; // version matches file
			}
		}
		
		return false;
	}
	
	/**
	 * Parses the java_arguments list and sets lwjgl specific 
	 * properties accordingly, before the launch.
	 */
	protected void setLWJGLProperties() {
		String lwjglArguments = getParameter("lwjgl_arguments");

		if(lwjglArguments != null && lwjglArguments.length() > 0) {
			int start = lwjglArguments.indexOf("-Dorg.lwjgl");
			while(start != -1) {
				int end = lwjglArguments.indexOf(" ", start);
				if(end == -1) {
					end = lwjglArguments.length();
				}
				String[] keyValue = lwjglArguments.substring(start+2, end).split("=");
				System.setProperty(keyValue[0], keyValue[1]);
				if(debugMode) {
					System.out.println("Setting property " + keyValue[0] + " to " + keyValue[1]);
				}
				start = lwjglArguments.indexOf("-Dorg.lwjgl", end);
			}
		}
	}

	/**
	 * This method will return the location of the cache directory. All the
	 * applet files will be downloaded and stored here. A folder will be
	 * created inside the LWJGL cache directory from the al_title parameter.
	 * This folder will also be prepended by the host name of the codebase 
	 * to avoid conflict with same named applets on other hosts.
	 * 
	 * @return path to applets cache directory
	 * @throws Exception if access is denied
	 */
	protected String getCacheDirectory() throws Exception {
		
		String path = AccessController.doPrivileged(new PrivilegedExceptionAction<String>() {
			public String run() throws Exception {

				// we append the code base to avoid naming collisions with al_title
				String codebase = "";
				if(prependHost) {
					codebase = getCodeBase().getHost();
					if(codebase == null || codebase.length() == 0) {
						codebase = "localhost";
					}
					codebase += File.separator;
				}
				return getLWJGLCacheDir() + File.separator + codebase + getParameter("al_title") + File.separator;
			}
		});
		
		return path;
	}
	
	/**
	 * Get path to the lwjgl cache directory. This location will be where
	 * the OS keeps temporary files.
	 * 
	 * @return path to the lwjgl cache directory
	 */
	protected String getLWJGLCacheDir() {
		String cacheDir = System.getProperty("deployment.user.cachedir");
		
		if (cacheDir == null || System.getProperty("os.name").startsWith("Win")) {
			cacheDir = System.getProperty("java.io.tmpdir");
		}
		
		return cacheDir + File.separator + "lwjglcache";
	}

	/**
	 * read String object from File
	 *
	 * @param file to be read
	 * @return the String stored in the file or null if it fails
	 */
	protected String readStringFile(File file) {
		try {
			return (String)readObjectFile(file);
		} catch (Exception e) {
			// failed to read version file
			e.printStackTrace();
		}
		
		// return null if failed to read file
		return null;
	}
	
	/**
	 * read the HashMap from File
	 *
	 * @param file the file to read
	 * @return the hashmap stored in the file or an empty hashmap if it fails
	 */
	@SuppressWarnings("unchecked")
	protected HashMap<String, Long> readHashMapFile(File file) {
		
		try {
			return (HashMap<String, Long>) readObjectFile(file);
		} catch (Exception e) {
			// failed to read hashmap from file
			e.printStackTrace();  
		}
		
		// return an empty map if failed to read file
		return new HashMap<String, Long>();
	}
	
	/**
	 * read the object from the File
	 * 
	 * @param file the file to read
	 * @return the object contained in the file or null if it fails
	 * @throws Exception if it fails to read object from file
	 */
	protected Object readObjectFile(File file) throws Exception {
		FileInputStream fis = new FileInputStream(file);
		
		try {
			ObjectInputStream dis = new ObjectInputStream(fis);
			Object object = dis.readObject();
			dis.close();
			return object;
		} catch (Exception e) {
			// failed to read file
			throw e;  
		} finally {
			fis.close();
		}
	}
	
	/**
	 * write object to specified File
	 *
	 * @param file the file to write out to
	 * @param object the contents of the file
	 * @throws Exception if it fails to write file
	 */
	protected void writeObjectFile(File file, Object object) throws Exception {
		FileOutputStream fos = new FileOutputStream(file);
		try {
			ObjectOutputStream dos = new ObjectOutputStream(fos);
			dos.writeObject(object);
			dos.close();
		} finally {
			fos.close();
		}
	}

	/**
	 * Edits the ClassPath at runtime to include the jars
	 * that have just been downloaded and then adds the
	 * lwjgl natives folder property.
	 *
	 * @param path location where applet is stored
	 * @throws Exception if it fails to add classpath
	 */
	protected void updateClassPath(final String path) throws Exception {

		setState(STATE_UPDATING_CLASSPATH);

		percentage = 95;

		URL[] urls = new URL[urlList.length];

		for (int i = 0; i < urlList.length; i++) {
			String file = new File(path, getJarName(urlList[i])).toURI().toString();
			// fix JVM bug where ! is not escaped
			file = file.replace("!", "%21");
			urls[i] = new URL(file);
		}
		
		// get AppletLoader certificates
		final Certificate[] certs = getCurrentCertificates();
		
		// detect if we are running on a mac and save result as boolean
		String osName = System.getProperty("os.name");
		final boolean isMacOS = (osName.startsWith("Mac") || osName.startsWith("Darwin"));
		
		// add downloaded jars to the classpath with required permissions
		classLoader = new URLClassLoader(urls) {
			protected PermissionCollection getPermissions (CodeSource codesource) {
				PermissionCollection perms = null;

				try {
                    // no permissions
                    perms = new Permissions();

                    // if certificates match the AppletLoader certificates then we should be all set
                    if (certificatesMatch(certs, codesource.getCertificates())) {
                        perms.add(new AllPermission());
                        return perms;
                    }

					String host = getCodeBase().getHost();
			        if (host != null && (host.length() > 0)) {
			        	// add permission for downloaded jars to access host they were from
			        	perms.add(new SocketPermission(host, "connect,accept"));
			        }
			        else if ( "file".equals(codesource.getLocation().getProtocol()) ) {
			        	// if running locally add file permission
			        	String path = codesource.getLocation().getFile().replace('/', File.separatorChar);
			            perms.add(new FilePermission(path, "read"));
			        }

		        } catch (Exception e) {
					e.printStackTrace();
				}

		        return perms;
		    }
			
			// allow non lwjgl native to be found from cache directory
			protected String findLibrary (String libname) {
				String libPath = path + "natives" + File.separator + LWJGLUtil.mapLibraryName(libname);
				
				if (new File(libPath).exists()) {
					return libPath;
				}
				
				return super.findLibrary(libname);
			}
		};

		debug_sleep(2000);

		// unload natives loaded by a previous instance of this lwjgl applet
		unloadNatives(path);

		// add natives files path to native class path
		System.setProperty("org.lwjgl.librarypath", path + "natives");

		// Make sure jinput knows about the new path too
		System.setProperty("net.java.games.input.librarypath", path + "natives");

		// set the library path, useful for non lwjgl natives
		System.setProperty("java.library.path", path + "natives");

		// mark natives as loaded
		natives_loaded = true;
	}

	/**
	 * Unload natives loaded by a different classloader.
	 *
	 * Due to limitations of the jvm, native files can only
	 * be loaded once and only be used by the classloader
	 * they were loaded from.
	 *
	 * Due to the way applets on plugin1 work, one jvm must
	 * be used for all applets. We need to use multiple
	 * classloaders in the same jvm due to LWJGL's static
	 * nature. In order to solve this we simply remove the
	 * natives from a previous classloader allowing a new
	 * classloader to use those natives in the same jvm.
	 *
	 * This method will only attempt to unload natives from a
	 * previous classloader if it detects that the natives have
	 * been loaded in the same jvm.
	 *
	 * @param nativePath directory where natives are stored
	 */
	private void unloadNatives(String nativePath) {

		// check whether natives have been loaded into this jvm
		if (!natives_loaded) {
			return;
		}

		try {
			Field field = ClassLoader.class.getDeclaredField("loadedLibraryNames");
			field.setAccessible(true);
			Vector libs = (Vector) field.get(getClass().getClassLoader());

			String path = new File(nativePath).getCanonicalPath();

			for (int i = 0; i < libs.size(); i++) {
				String s = (String) libs.get(i);

				// if a native from the nativePath directory is loaded, unload it
				if (s.startsWith(path)) {
					libs.remove(i);
					i--;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * replace the current applet with the lwjgl applet
	 * using AppletStub and initialise and start it
	 */
	protected void switchApplet() throws Exception {

		setState(STATE_SWITCHING_APPLET);
		percentage = 100;

		debug_sleep(2000);
		
		// set correct context classloader for lwjgl applet
		Thread.currentThread().setContextClassLoader(classLoader);
		
		Class appletClass = classLoader.loadClass(getParameter("al_main"));
		lwjglApplet = (Applet) appletClass.newInstance();

		lwjglApplet.setStub(this);
		lwjglApplet.setSize(getWidth(), getHeight());

		setLayout(new BorderLayout());
		add(lwjglApplet);
		validate();

		setState(STATE_INITIALIZE_REAL_APPLET);
		lwjglApplet.init();

		setState(STATE_START_REAL_APPLET);
		lwjglApplet.start();
	}

	/**
	 * This method will get the files sizes of the files to download.
	 * It wil further get the lastModified time of files
	 * and save it in a hashmap, if cache is enabled it will mark
	 * those files that have not changed since last download to not
	 * redownloaded.
	 *
	 * @param dir - location to read cache file from
	 * @throws Exception - if fails to get infomation
	 */
	protected void getJarInfo(File dir) throws Exception {
		setState(STATE_CHECKING_CACHE);
		
		filesLastModified = new HashMap<String, Long>();

		// store file sizes and mark which files not to download
		fileSizes = new int[urlList.length];

		File timestampsFile = new File(dir, "timestamps");

		// if timestamps file exists, load it
		if (timestampsFile.exists()) {
			setState(STATE_CHECKING_FOR_UPDATES);
			filesLastModified = readHashMapFile(timestampsFile);
		}

		// calculate total size of jars to download
		
		ExecutorService executorService = Executors.newFixedThreadPool(concurrentLookupThreads);
		Queue<Future> requests = new LinkedList<Future>();
		
		// create unique object to sync code in requests 
		final Object sync = new Integer(1);
		
		for (int j = 0; j < urlList.length; j++) {
			final int i = j;
			
			Future request = executorService.submit(new Runnable() {
				
				public void run() {
					
					try {
						
						URLConnection urlconnection = urlList[i].openConnection();
						urlconnection.setDefaultUseCaches(false);
						if (urlconnection instanceof HttpURLConnection) {
							((HttpURLConnection) urlconnection).setRequestMethod("HEAD");
						}
		 
						fileSizes[i] = urlconnection.getContentLength();

						long lastModified = urlconnection.getLastModified();
						String fileName = getFileName(urlList[i]);

						if (cacheEnabled && lastModified != 0 && filesLastModified.containsKey(fileName)) {
							long savedLastModified = filesLastModified.get(fileName);

							// if lastModifed time is the same, don't redownload
							if (savedLastModified == lastModified) {
								fileSizes[i] = -2; // mark it to not redownload
							}
						}
			 
						if (fileSizes[i] >= 0) {
							synchronized (sync) {
								totalDownloadSize += fileSizes[i];
							}
						}

						// put key and value in the hashmap
						filesLastModified.put(fileName, lastModified);
									
					} catch (Exception e) {
						throw new RuntimeException("Failed to fetch information for " + urlList[i], e);
					}				
				}});
			
			requests.add(request);
		}
			
		while (!requests.isEmpty()) {
			Iterator<Future> iterator = requests.iterator();
			while (iterator.hasNext()) {
				Future request = iterator.next();
					if (request.isDone()) {
						request.get(); // will throw an exception if request thrown an exception.
						iterator.remove();
						
						// update progress bar
						percentage = 5 + (int) (10 * (urlList.length - requests.size()) / (float) urlList.length);
					}
			}
		
			Thread.sleep(10);
		}
		
		executorService.shutdown();
	}

	/**
	 * Will download the jars from the server using the list of urls
	 * in urlList, while at the same time updating progress bar
	 *
	 * @param path location of the directory to save to
	 * @throws Exception if download fails
	 */
	protected void downloadJars(String path) throws Exception {
		setState(STATE_DOWNLOADING);

		URLConnection urlconnection;

		int initialPercentage = percentage = 15;
		int amountDownloaded = 0;

		// download each jar
		byte buffer[] = new byte[65536];
		for (int i = 0; i < urlList.length; i++) {

			// skip file if marked as -2 (already downloaded and not changed)
			if (fileSizes[i] == -2) continue;

			int unsuccessfulAttempts = 0;
			int maxUnsuccessfulAttempts = 3;
			boolean downloadFile = true;
			
			String currentFile = getFileName(urlList[i]);

			// download the jar a max of 3 times
			while(downloadFile) {
				downloadFile = false;

				debug_sleep(2000);
				
				try {
					urlconnection = urlList[i].openConnection();
					urlconnection.setUseCaches(false);
	
					if (urlconnection instanceof HttpURLConnection) {
						urlconnection.setRequestProperty("Cache-Control", "no-store,max-age=0,no-cache");
						urlconnection.connect();
			        }
	
					
					InputStream inputstream = getJarInputStream(currentFile, urlconnection);
					FileOutputStream fos = new FileOutputStream(path + currentFile);
					
					
					int bufferSize;
					int currentDownload = 0;
					
					long downloadStartTime = System.currentTimeMillis();
					int downloadedAmount = 0;
					String downloadSpeedMessage = "";
					
					try {	
						while ((bufferSize = inputstream.read(buffer, 0, buffer.length)) != -1) {
							debug_sleep(10);
							fos.write(buffer, 0, bufferSize);
							currentDownload += bufferSize;
							
							int totalDownloaded = amountDownloaded + currentDownload;
							percentage = initialPercentage + ((totalDownloaded * 45) / totalDownloadSize);
							subtaskMessage = "Retrieving: " + currentFile + " " + ((totalDownloaded * 100) / totalDownloadSize) + "%";
		
							downloadedAmount += bufferSize;
							long timeLapse = System.currentTimeMillis() - downloadStartTime;
							// update only if a second or more has passed
							if (timeLapse >= 1000) {
								// get kb/s, nice that bytes/millis is same as kilobytes/seconds
								float downloadSpeed = (float) downloadedAmount / timeLapse;
								// round to two decimal places
								downloadSpeed = ((int)(downloadSpeed*100))/100f;
								// set current speed message
								downloadSpeedMessage = " - " + downloadSpeed + " KB/sec";
								// reset downloaded amount
								downloadedAmount = 0;
								// reset start time
								downloadStartTime = System.currentTimeMillis();
							}
		
							subtaskMessage += downloadSpeedMessage;
						}
						
					} finally {
						inputstream.close();
						fos.close();
					}
					
					// download complete, verify if it was successful
					if (urlconnection instanceof HttpURLConnection) {
		                if (currentDownload == fileSizes[i]) {
		                	// successful download
		                }
		                else if (fileSizes[i] <= 0 && currentDownload != 0) {
	                        // If contentLength for fileSizes[i] <= 0, we don't know if the download
	                        // is complete. We're going to guess the download is complete.
	                    }
	                    else {
	                    	throw new Exception("size mismatch on download of " + currentFile + 
	                    						" expected " + fileSizes[i] + " got " + currentDownload);
	                    }
		            }
					
					// successful file download, update total amount downloaded
					amountDownloaded += fileSizes[i];
					
				} catch (Exception e) {
					e.printStackTrace(); // output exception to console
					
					// Failed to download the file
					unsuccessfulAttempts++;
	        		
					// download failed try again
	            	if (unsuccessfulAttempts < maxUnsuccessfulAttempts) {
	            		downloadFile = true;
	            		Thread.sleep(100); // wait a bit before retrying
	            	}
	            	else {
	            		// retry attempts exhasted, download failed
	            		throw new Exception("failed to download " + currentFile + 
	            							" after " + maxUnsuccessfulAttempts + " attempts");
	            	}
				}
			}
		}
		subtaskMessage = "";
	}

	/**
	 * Retrieves a jar files input stream. This method exists primarily to fix an Opera hang in getInputStream
	 * @param urlconnection connection to get input stream from
	 * @return InputStream or null if not possible
	 */
	protected InputStream getJarInputStream(final String currentFile, final URLConnection urlconnection) throws Exception {
		final InputStream[] is = new InputStream[1];

		// try to get the input stream 3 times.
		// Wait at most 5 seconds before interrupting the thread
		for (int j = 0; j < 3 && is[0] == null; j++) {
			Thread t = new Thread() {
				public void run() {
					try {
						is[0] = urlconnection.getInputStream();
					} catch (IOException e) {
						/* ignored */
					}
				}
			};
			t.setName("JarInputStreamThread");
			t.start();

			int iterationCount = 0;
			while(is[0] == null && iterationCount++ < 5) {
				try {
					t.join(1000);
				} catch (InterruptedException inte) {
					/* ignored */
				}
			}

			if(is[0] == null) {
				try {
					t.interrupt();
					t.join();
				} catch (InterruptedException inte) {
					/* ignored */
				}
			}
		}

		if(is[0] == null) {
			throw new Exception("Unable to get input stream for " + currentFile);
		}


		return is[0];
	}

	/**
	 *  Extract LZMA File
	 *  @param in Input path to pack file
	 *  @param out output path to resulting file
	 *  @throws Exception if any errors occur
	 */
	protected void extractLZMA(String in, String out) throws Exception {

		File f = new File(in);
		FileInputStream fileInputHandle = new FileInputStream(f);

		// use reflection to avoid hard dependency
		Class<?> clazz = Class.forName( "LZMA.LzmaInputStream" );
		Constructor constructor = clazz.getDeclaredConstructor(InputStream.class);
		InputStream inputHandle = (InputStream) constructor.newInstance(fileInputHandle);

		OutputStream outputHandle = new FileOutputStream(out);

		byte [] buffer = new byte [1<<14];

		try {
			int ret = inputHandle.read(buffer);
			while (ret >= 1) {
				outputHandle.write(buffer,0,ret);
				ret = inputHandle.read(buffer);
			}
		} finally {
			inputHandle.close();
			outputHandle.close();
		}

		// delete LZMA file, as it is no longer needed
		f.delete();
	}
	
	/**
	 *  Extract GZip File
	 *  @param in Input path to pack file
	 *  @param out output path to resulting file
	 *  @throws Exception if any errors occur
	 */
	protected void extractGZip(String in, String out) throws Exception {

		File f = new File(in);
		FileInputStream fileInputHandle = new FileInputStream(f);

		InputStream inputHandle = new GZIPInputStream(fileInputHandle);

		OutputStream outputHandle = new FileOutputStream(out);

		try {
			byte [] buffer = new byte [1<<14];
	
			int ret = inputHandle.read(buffer);
			while (ret >= 1) {
				outputHandle.write(buffer,0,ret);
				ret = inputHandle.read(buffer);
			}
		} finally {
			inputHandle.close();
			outputHandle.close();
		}

		// delete GZip file, as it is no longer needed
		f.delete();
	}
	
	/**
	 *  Extract Pack File
	 *  @param in Input path to pack file
	 *  @param out output path to resulting file
	 *  @throws Exception if any errors occur
	 */
	protected void extractPack(String in, String out) throws Exception {
		File f = new File(in);
	    FileOutputStream fostream = new FileOutputStream(out);
	    JarOutputStream jostream = new JarOutputStream(fostream);
	    
	    try {
	    	Pack200.Unpacker unpacker = Pack200.newUnpacker();
	    	unpacker.unpack(f, jostream);
	    } finally {
	    	jostream.close();
	    	fostream.close();
	    }

	    // delete pack file as its no longer needed
	    f.delete();
	}

	/**
	 *  Extract all jars from any lzma/gz/pack files
	 *
	 *  @param path output path
	 *  @throws Exception if any errors occur
	 */
	protected void extractJars(String path) throws Exception {
		setState(STATE_EXTRACTING_PACKAGES);

		float increment = (float) 10.0 / urlList.length;
		// extract all gz, lzma, pack.gz and pack.lzma files
		for (int i = 0; i < urlList.length; i++) {

			// if file has not changed, skip it
			if (fileSizes[i] == -2) continue;

			percentage = 55 + (int) (increment * (i+1));
			String filename = getFileName(urlList[i]);
			
			if (filename.endsWith(".pack.lzma")) {
				subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".lzma", "");
				debug_sleep(1000);
				extractLZMA(path + filename, path + replaceLast(filename, ".lzma", ""));

				subtaskMessage = "Extracting: " + replaceLast(filename, ".lzma", "") + " to " + replaceLast(filename, ".pack.lzma", "");
				debug_sleep(1000);
				extractPack(path + replaceLast(filename, ".lzma", ""), path + replaceLast(filename, ".pack.lzma", ""));
			}
			else if (filename.endsWith(".pack.gz")) {
				subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".gz", "");
				debug_sleep(1000);
				extractGZip(path + filename, path + replaceLast(filename, ".gz", ""));

				subtaskMessage = "Extracting: " + replaceLast(filename, ".gz", "") + " to " + replaceLast(filename, ".pack.gz", "");
				debug_sleep(1000);
				extractPack(path + replaceLast(filename, ".gz", ""), path + replaceLast(filename, ".pack.gz", ""));
			}
			else if (filename.endsWith(".pack")) {
				subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".pack", "");
				debug_sleep(1000);
				extractPack(path + filename, path + replaceLast(filename, ".pack", ""));
			}
			else if (filename.endsWith(".lzma")) {
				subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".lzma", "");
				debug_sleep(1000);
				extractLZMA(path + filename, path + replaceLast(filename, ".lzma", ""));
			}
			else if (filename.endsWith(".gz")) {
				subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".gz", "");
				debug_sleep(1000);
				extractGZip(path + filename, path + replaceLast(filename, ".gz", ""));
			}
		}
	}

	/**
	 * This method will extract all file from the native jar and extract them
	 * to the subdirectory called "natives" in the local path, will also check
	 * to see if the native jar files is signed properly
	 *
	 * @param path base folder containing all downloaded jars
	 * @throws Exception if it fails to extract files
	 */
	protected void extractNatives(String path) throws Exception {
		
		setState(STATE_EXTRACTING_PACKAGES);
		
		float percentageParts = 15f/nativeJarCount; // parts for each native jar from 15%
		
		// create native folder
		File nativeFolder = new File(path + "natives");
		if (!nativeFolder.exists()) {
			nativeFolder.mkdir();
		}
		
		// get the current AppletLoader certificates to compare against certificates of the native files
		Certificate[] certificate = getCurrentCertificates();
		
		for (int i = urlList.length - nativeJarCount; i < urlList.length; i++) {
			
			// if a new native jar was not downloaded, no extracting needed
			if (fileSizes[i] == -2) {
				continue;
			}
			
			// get name of jar file with natives from urlList
			String nativeJar = getJarName(urlList[i]);
	
			// open jar file
			JarFile jarFile = new JarFile(path + nativeJar, true);
	
			// get list of files in jar
			Enumeration entities = jarFile.entries();
	
			totalSizeExtract = 0;
			int jarNum = i - (urlList.length - nativeJarCount); // used for progressbar
		
			// calculate the size of the files to extract for progress bar
			while (entities.hasMoreElements()) {
				JarEntry entry = (JarEntry) entities.nextElement();
	
				// skip directories and anything in directories
				// conveniently ignores the manifest
				if (entry.isDirectory() || entry.getName().indexOf('/') != -1) {
					continue;
				}
				totalSizeExtract += entry.getSize();
			}
	
			currentSizeExtract = 0;
	
			// reset point to begining by getting list of file again
			entities = jarFile.entries();
	
			// extract all files from the jar
			while (entities.hasMoreElements()) {
				JarEntry entry = (JarEntry) entities.nextElement();
	
				// skip directories and anything in directories
				// conveniently ignores the manifest
				if (entry.isDirectory() || entry.getName().indexOf('/') != -1) {
					continue;
				}
	
				// check if native file already exists if so delete it to make room for new one
				// useful when using the reload button on the browser
				File f = new File(path + "natives" + File.separator + entry.getName());
				if (f.exists()) {
					if (!f.delete()) {
						continue; // unable to delete file, it is in use, skip extracting it
					}
				}
	
				debug_sleep(1000);
	
				InputStream in = jarFile.getInputStream(jarFile.getEntry(entry.getName()));
				OutputStream out = new FileOutputStream(path + "natives" + File.separator + entry.getName());
	
				try {
					int bufferSize;
					byte buffer[] = new byte[65536];
		
					while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1) {
						debug_sleep(10);
						out.write(buffer, 0, bufferSize);
						currentSizeExtract += bufferSize;
		
						// update progress bar
						percentage = 65 + (int)(percentageParts * (jarNum + currentSizeExtract/(float)totalSizeExtract));
						subtaskMessage = "Extracting: " + entry.getName() + " " + ((currentSizeExtract * 100) / totalSizeExtract) + "%";
					}
				} finally {
					in.close();
					out.close();
				}
				
				// validate the certificate for the native file being extracted
				if (!certificatesMatch(certificate, entry.getCertificates())) {
					f.delete(); // delete extracted native as its certificates doesn't match
					throw new Exception("The certificate(s) in " + nativeJar + " do not match the AppletLoader!");
				}
			}
			subtaskMessage = "";
	
			jarFile.close();
	
			// delete native jar as it is no longer needed
			File f = new File(path + nativeJar);
			f.delete();
		
		}
	}

	/**
	 * Compare two certificate chains to see if they match
	 *
	 * @param certs1 first chain of certificates
	 * @param certs2 second chain of certificates
	 * 
	 * @return true if the certificate chains are the same
	 */
	protected static boolean certificatesMatch(Certificate[] certs1, Certificate[] certs2) throws Exception {
		if (certs1 == null || certs2 == null) {
			return false;
		}
		
		if (certs1.length != certs2.length) {
			System.out.println("Certificate chain differs in length [" + certs1.length + " vs " + certs2.length + "]!");
			return false;
		}
		
		for (int i = 0; i < certs1.length; i++) {
			if (!certs1[i].equals(certs2[i])) {
				System.out.println("Certificate mismatch found!");
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Returns the current certificate chain of the AppletLoader
	 * 
	 * @return - certificate chain of AppletLoader
	 */
	protected static Certificate[] getCurrentCertificates() throws Exception {
		// get the current certificate to compare against native files
		Certificate[] certificate = AppletLoader.class.getProtectionDomain().getCodeSource().getCertificates();
		
		// workaround for bug where cached applet loader does not have certificates!?
		if (certificate == null) {
			URL location = AppletLoader.class.getProtectionDomain().getCodeSource().getLocation();

			// manually load the certificate
			JarURLConnection jurl = (JarURLConnection) (new URL("jar:" + location.toString() + "!/org/lwjgl/util/applet/AppletLoader.class").openConnection());
			jurl.setDefaultUseCaches(true);
			certificate = jurl.getCertificates();
			jurl.setDefaultUseCaches(false);
		}
		
		return certificate;
	}
	
	/**
	 *  Check and validate jars which will be loaded into the classloader to make 
	 *  sure that they are not corrupt. This ensures corrupt files are never marked
	 *  as successful downloadeds by the cache system.
	 *  
	 *  @param path - where the jars are stored
	 *  @throws Exception if a corrupt jar is found
	 */
	protected void validateJars(String path) throws Exception {
		
		setState(STATE_VALIDATING_PACKAGES);
		
		percentage = 80;
		
		float percentageParts = 10f / urlList.length; // percentage for each file out of 10%
		
		for (int i = 0; i < urlList.length - nativeJarCount; i++) {
			
			debug_sleep(1000);
			
			// if file not downloaded, no need to validate again
			if (fileSizes[i] == -2) continue;
			
			subtaskMessage = "Validating: " + getJarName(urlList[i]);
			
			File file = new File(path, getJarName(urlList[i])); 
			if (!isZipValid(file)) {
				throw new Exception("The file " + getJarName(urlList[i]) + " is corrupt!");
			}
			
			percentage = 80 + (int)(percentageParts * i);
		}
		
		subtaskMessage = "";
	}
	
	/**
	 * This method will check if a zip file is valid by running through it
	 * and checking for any corruption and CRC failures
	 * 
	 * @param file - zip file to test
	 * @return boolean - runs false if the file is corrupt
	 */
	protected boolean isZipValid(File file) {
		
		try {
			ZipFile zipFile = new ZipFile(file);
			
			try {
				Enumeration e = zipFile.entries();
		        
				byte[] buffer = new byte[4096];
				
		        while(e.hasMoreElements()) {
		        	ZipEntry zipEntry = (ZipEntry) e.nextElement();
		        	
		        	CRC32 crc = new CRC32();
		        	
		        	BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(zipEntry));
		        	CheckedInputStream cis = new CheckedInputStream(bis, crc);
		        	
		        	while(cis.read(buffer, 0, buffer.length) != -1) {
		        		// scroll through zip entry
		        	}
		        	
		        	if (crc.getValue() != zipEntry.getCrc()) {
		        		return false; // CRC match failed, corrupt zip
		        	}
		        }
		        
		        return true; // valid zip file
			} finally {
				zipFile.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * Get Image from path provided
	 *
	 * @param s location of the image
	 * @return the Image file
	 */
	protected Image getImage(String s) {
		
		// if s is "" then don't load an image
		if (s.length() == 0) return null;
		
		Image image = null;
		
		try {
			image = getImage(new URL(getCodeBase(), s));
		} catch (Exception e) {
			/* */
		}
		
		// if image failed to load, try another method
		if (image == null) {
			image = getImage(Thread.currentThread().getContextClassLoader().getResource(s));
		}
		
		// if image loaded sucessfully return it
		if (image != null) {
			return image;
		}

		// show error as image could not be loaded
		fatalErrorOccured("Unable to load the logo/progressbar image: " + s, null);
		return null;
	}
	
	/**
	 * Get Image from path provided
	 *
	 * @param url location of the image
	 * @return the Image file
	 */
	public Image getImage(URL url) {
		try {
			MediaTracker tracker = new MediaTracker(this);
			
			Image image = super.getImage(url);

			// wait for image to load
			tracker.addImage(image, 0);
	        tracker.waitForAll();

	        // if no errors return image
	        if (!tracker.isErrorAny()) {
	        	return image;
	        }
		} catch (Exception e) {
			/* */
		}
		
		return null;
	}

	/**
	 * Get jar name from URL.
	 *
	 * @param url Get jar file name from this url
	 * @return file name as string
	 */
	protected String getJarName(URL url) {
		String fileName = url.getFile();

		if (fileName.endsWith(".pack.lzma")) {
			fileName = replaceLast(fileName, ".pack.lzma", "");
		} else if (fileName.endsWith(".pack.gz")) {
			fileName = replaceLast(fileName, ".pack.gz", "");
		} else if (fileName.endsWith(".pack")) {
			fileName = replaceLast(fileName, ".pack", "");
		} else if (fileName.endsWith(".lzma")) {
			fileName = replaceLast(fileName, ".lzma", "");
		} else if (fileName.endsWith(".gz")) {
			fileName = replaceLast(fileName, ".gz", "");
		}

		return fileName.substring(fileName.lastIndexOf('/') + 1);
	}

	/**
	 * Get file name portion of URL.
	 *
	 * @param url Get file name from this url
	 * @return file name as string
	 */
	protected String getFileName(URL url) {
		String fileName = url.getFile();
		return fileName.substring(fileName.lastIndexOf('/') + 1);
	}

	/**
	 * Retrieves the color
	 *
	 * @param param Color to load
	 * @param defaultColor Default color to use if no color to load
	 * @return Color to use
	 */
	protected Color getColor(String param, Color defaultColor) {
		String color = getParameter(param);

		if (color == null) return defaultColor;

		// Check if RGB format
        if (color.indexOf(",") != -1) {
            StringTokenizer st = new StringTokenizer(color, ",");

            // We've got three components for the color
            try {
            	return new Color(Integer.parseInt(st.nextToken().trim()),
    							 Integer.parseInt(st.nextToken().trim()),
    							 Integer.parseInt(st.nextToken().trim()));
            } catch (Exception e) {
                // failed to parse
            	return defaultColor;
            }
        }

        // Check & decode if the color is in hexadecimal color format (i.e. #808000)
        try {
        	return Color.decode(color);
        } catch (NumberFormatException e) {
        	// ignore exception
        }

        // Get the color by name if it exists
        try {
        	return (Color)Color.class.getField(color).get(null);
        } catch (Exception e) {
        	return defaultColor;
        }
	}
	
	/**
	 * Replaces the last occurrence of the specified target substring with
	 * the specified replacement string in a string.
	 * 
	 * @param original - String to search
	 * @param target - substring to find
	 * @param replacement - what to replace target substring with
	 * @return - return the modified string, if target substring not found return original string
	 */
	public String replaceLast(String original, String target, String replacement) {
		int index = original.lastIndexOf(target); 
		
		if(index == -1) {
			return original;
		}
		
		return original.substring(0, index) + replacement + original.substring(index + target.length());
	}
	
	/**
	 * Retrieves the String value for the parameter
	 * @param name Name of parameter
	 * @param defaultValue default value to return if no such parameter
	 * @return value of parameter or defaultValue
	 */
	protected String getStringParameter(String name, String defaultValue) {
		String parameter = getParameter(name);
		if (parameter != null) {
			return parameter;
		}
		return defaultValue;
	}

	/**
	 * Retrieves the boolean value for the parameter
	 * @param name Name of parameter
	 * @param defaultValue default value to return if no such parameter
	 * @return value of parameter or defaultValue
	 */
	protected boolean getBooleanParameter(String name, boolean defaultValue) {
		String parameter = getParameter(name);
		if (parameter != null) {
			return Boolean.parseBoolean(parameter);
		}
		return defaultValue;
	}
	
	/**
	 * Retrieves the int value for the applet
	 * @param name Name of parameter
	 * @param defaultValue default value to return if no such parameter
	 * @return value of parameter or defaultValue
	 */
	protected int getIntParameter(String name, int defaultValue) {
		String parameter = getParameter(name);
		if (parameter != null) {
			return Integer.parseInt(parameter);
		}
		return defaultValue;
	}

	/**
	 * Sets the error message and print debug information
	 *
	 * @param error Error message to print
	 */
	protected void fatalErrorOccured(String error, Exception e) {
		fatalError = true;
		
		if (minimumJreNotFound) {
			errorMessage = minimumJREMessage;
			errorMessage[errorMessage.length-1] = error;
		}
		else if (certificateRefused) {
			errorMessage = certificateRefusedMessage;
		}
		else {
			errorMessage = genericErrorMessage;
			errorMessage[errorMessage.length-1] = error;
		}
		
		System.out.println(error);
		if(e != null) {
			System.out.println(e.getMessage());
			System.out.println(generateStacktrace(e));
		}
		repaint();
	}

	/** 
	 * set the state of applet loader 
	 * @param state new state of applet loader
	 * */
	protected void setState(int state) {
		this.state = state;
		if(debugMode) {
			System.out.println(getDescriptionForState());
		}
	}
	
	/**
	 * Utility method for sleeping
	 * Will only really sleep if debug has been enabled
	 * @param ms milliseconds to sleep
	 */
	protected void debug_sleep(long ms) {
		if(debugMode) {
			sleep(ms);
		}
	}

	/**
	 * Utility method for sleeping
	 * @param ms milliseconds to sleep
	 */
	protected void sleep(long ms) {
		try {
			Thread.sleep(ms);
		} catch (Exception e) {
			/* ignored */
		}
	}

}
