package com.jolbox.utils;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;

/** Finds a class somewhere on disk. Searches recursively and inside jars too.
 * @author wallacew
 *
 */
@Component
@ManagedResource(description = "FindClass utils", objectName = "com.jolbox.utils:name=FindClass")
public class FindClass {

	
	@SuppressWarnings("unchecked")
	@ManagedOperation( description="Returns the jar/path of all the currently loaded resources")
	public Set<String> dumpAllLoadedOrigins() throws IllegalAccessException{
		Set<String> results = new HashSet<String>();

		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		Class<?> loaderClass = cl.getClass();
		while (loaderClass != java.lang.ClassLoader.class){
			loaderClass = loaderClass.getSuperclass();
		}
		
		Field classesField;
		try {
			classesField = loaderClass.getDeclaredField("classes");
		} catch (SecurityException e) {
			throw new IllegalAccessException("Security Exception");
		} catch (NoSuchFieldException e) {
			throw new IllegalAccessException("No such field");
		}
		classesField.setAccessible(true);
		Vector<Class<?>> classes = (Vector<Class<?>>) classesField.get(cl);
		
		for (Class<?> clazz: classes){
			results.add(getLoadOrigin(clazz));
		}
		return results;
	}

	@ManagedOperation( description="Returns the jar/path of all the currently loaded resources")
	public String showAllLoadedResources() throws IllegalAccessException{
		return dumpAllLoadedOrigins().toString();
	}
	
	public void printAllLoadedClassOrigins() throws IllegalAccessException{
		for (String location: this.dumpAllLoadedOrigins()){
			System.out.println(location);
		}
	}

	/** Given a resource, returns the place where this was loaded from (which jar or file path). */
	@ManagedOperation(description="Returns the jar/path where the given resource can be found")
	 @ManagedOperationParameters({
    @ManagedOperationParameter(name = "resource", description = "The resource to lookup")
	 })
	public String findLoadOrigin(String resource){
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		Enumeration<?>[] e;
		try {
			e = new Enumeration[] {cl.getResources(resource)};

			URL url;
			URLConnection conn;
			JarFile jarFile = null;

			if (e[0].hasMoreElements()) {
				url = (URL) e[0].nextElement();
				conn = url.openConnection();
				conn.setUseCaches(false);
				conn.setDefaultUseCaches(false);
				if (conn instanceof JarURLConnection) {
					jarFile = ((JarURLConnection) conn).getJarFile();
					return jarFile.getName();
				} 

				if (conn instanceof HttpURLConnection) {
					return ((HttpURLConnection)conn).getURL().toString();
				}

				return new File(URLDecoder.decode(url.getFile(), "UTF-8")).getAbsolutePath();
			}

		} catch (IOException e1) {
			// no match
		}

		return null;
	}

	/** Given a class, returns the place where this class was loaded from (which jar or file path). */
	public String getLoadOrigin(Class<?> clazz){
		return findLoadOrigin(clazz.getName().replaceAll("\\.", "/")+".class");
	}
	
	/** Given an object, returns the place where this instance was loaded from (which jar or file path). */
	public String getLoadOrigin(Object obj){
		return getLoadOrigin(obj.getClass());
	}
	
	@ManagedOperation(description="Returns the jar/path where the given class can be found")
	 @ManagedOperationParameters({
   @ManagedOperationParameter(name = "fullyQualifiedClassName", description = "The classname to locate eg com.foo.bar.Whatever")
	 })
	public String getLoadOriginClass(String fullyQualifiedClassName){
		return findLoadOrigin(fullyQualifiedClassName.replaceAll("\\.", "/")+".class");
	}

	/**
	 * Search for a class in a given dir.
	 *
	 * @param aStartingDir
	 * @param match
	 * @param suffix
	 * @throws IOException
	 */
	public static void searchForClass(File aStartingDir, String match, String suffix)
	throws IOException {
		validateDirectory(aStartingDir);

		for (File file : aStartingDir.listFiles()) {
			String name = file.getName().toUpperCase();
			if ((name.indexOf(match) > -1) && name.endsWith(suffix.toUpperCase())) {
				System.out.println(file.getAbsoluteFile());
			}
			if (file.getAbsolutePath().toUpperCase().endsWith(".JAR")) {
				checkInJar(file, match, suffix);
			}
			if (!file.isFile()) {
				// must be a directory recursive call!
				searchForClass(file, match, suffix);
			}

		}
	}

	/**
	 * Checks to see if match is in the given jar file.
	 *
	 * @param file to search into
	 * @param match matching string
	 * @param suffix
	 * @throws IOException
	 */
	private static void checkInJar(File file, String match, String suffix)
	throws IOException {
		ZipInputStream zw;
		ZipEntry ze;

		FileInputStream fis = new FileInputStream(file);
		zw = new ZipInputStream(new BufferedInputStream(fis));


		ze = zw.getNextEntry();

		while (ze != null) {
			String zeName = ze.getName().toUpperCase();
			if ((zeName.indexOf(match) > -1) && zeName.endsWith(suffix.toUpperCase())) {
				System.out.println("Found in jar: " + file.getAbsolutePath() + " " + ze.getName());
			}
			ze = zw.getNextEntry();
		}
		zw.close();
	}


	/**
	 * Directory is valid if it exists, does not represent a file, and can be read.
	 *
	 * @param aDirectory directory to validate
	 * @throws FileNotFoundException on error
	 */
	private static void validateDirectory(File aDirectory)
	throws FileNotFoundException {
		if (aDirectory == null) {
			throw new IllegalArgumentException("Directory should not be null.");
		}
		if (!aDirectory.exists()) {
			throw new FileNotFoundException("Directory does not exist: " + aDirectory);
		}
		if (!aDirectory.isDirectory()) {
			throw new IllegalArgumentException("Is not a directory: " + aDirectory);
		}
		if (!aDirectory.canRead()) {
			throw new IllegalArgumentException("Directory cannot be read: " + aDirectory);
		}
	}

	/**
	 * Search for given class in the classpath.
	 *
	 * @param match A string to search for.
	 * @param suffix
	 * @throws IOException on error
	 */
	public static void searchInClassPath(String match, String suffix)
	throws IOException {
		String classPath = System.getProperty("java.class.path");
		String find = match.toUpperCase();

		for (String path : classPath.split(";")) {
			File file = new File(path);
			if (file.getAbsolutePath().toUpperCase().endsWith(".JAR")) {
				checkInJar(file, find, suffix);
			}
			else {
				searchForClass(new File(path), find, suffix);
			}
		}
	}



	/**
	 * Attempts to list all the classes in the specified package as determined by the context class
	 * loader
	 *
	 * @param pckgname the package name to search
	 * @return a list of classes that exist within that package
	 * @throws ClassNotFoundException if something went wrong
	 * @throws IOException
	 */
	public static List<Class> getClassesForPackage(String pckgname)
	throws ClassNotFoundException, IOException {
		// This will hold a list of directories matching the pckgname. There may be more than one if
		// a package is split over multiple jars/paths
		ArrayList<File> directories = new ArrayList<File>();
		try {
			ClassLoader cld = Thread.currentThread().getContextClassLoader();
			if (cld == null) {
				throw new ClassNotFoundException("Can't get class loader.");
			}
			String path = pckgname.replace('.', '/');
			// Ask for all resources for the path
			Enumeration<URL> resources = cld.getResources(path);
			while (resources.hasMoreElements()) {
				directories.add(new File(URLDecoder.decode(resources.nextElement().getPath(), "UTF-8")));
			}
		}
		catch (NullPointerException x) {
			throw new ClassNotFoundException(pckgname + " does not appear to be a valid package (Null pointer exception)");
		}
		catch (UnsupportedEncodingException encex) {
			throw new ClassNotFoundException(pckgname + " does not appear to be a valid package (Unsupported encoding)");
		}
		catch (IOException ioex) {
			throw new ClassNotFoundException("IOException was thrown when trying to get all resources for " + pckgname);
		}

		ArrayList<Class> classes = new ArrayList<Class>();
		// For every directory identified capture all the .class files
		for (File directory : directories) {
			searchInDir(pckgname, classes, directory);
		}
		return classes;
	}



	/**
	 *
	 *
	 * @param pckgname
	 * @param classes
	 * @param directory
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	private static void searchInDir(String pckgname, ArrayList<Class> classes, File directory)
	throws ClassNotFoundException, IOException {

		if (directory.isDirectory()) {
			for (File dir : directory.listFiles()) {
				if (dir.isDirectory()) {
					searchInDir(pckgname + "." + dir.getName(), classes, dir);
				} else {
					// Get the list of the files contained in the package
					String[] files = directory.list();
					for (String file : files) {
						// we are only interested in .class files

						if (file.endsWith(".class")) {
							// removes the .class extension
							classes.add(Class.forName(pckgname + '.' + file.substring(0, file.length() - 6)));
						}
					}
				}


			}
		}

	}



	/**
	 * Main app
	 *
	 * @param args
	 * @throws IOException
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException 
	 * @throws NoSuchFieldException 
	 * @throws IllegalArgumentException 
	 * @throws SecurityException 
	 */
	public static void main(String[] args)
	throws IOException, ClassNotFoundException, SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
//		FindClass findClass = new FindClass();
//		System.out.println("Long class has been loaded from : "+findClass.getLoadOrigin(1L));
//		System.out.println("FindClass class has been loaded from : " + findClass.getLoadOrigin(FindClass.class));
//		System.exit(1);
		//    	System.out.println(dumpAllLoadedOrigins());
		if (args.length < 3) {
			System.err.println("Usage: FindClass <startdir> <match> <extension>");
			System.err.println("Eg: FindClass d:\\workspace FooBar .class");
			System.err.println("matches *FooClass*.class (case is not important)");
		}
		else {
			System.out.println("Searching. Grab a coffee....");
			searchForClass(new File(args[0]), args[1].toUpperCase(), args[2].toUpperCase());
		}
		System.out.println("End of search");
	}
}

