package net.fybertech.meddle;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.lang.reflect.Field;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.minecraft.launchwrapper.IClassTransformer;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;


public class Meddle implements ITweaker 
{
	public static final Logger LOGGER = LogManager.getLogger("Meddle");
	
	/** Command-line arguments received from LaunchWrapper */
	public final List<String> args = new ArrayList<String>();

	/** Containers for all jars discovered in the Meddle folder */
	public static final List<ModContainer> discoveredModsList = new ArrayList<ModContainer>();
	
	/** Discovered mod IDs and their associated containers */ 
	public static final Map<String, ModContainer> loadedModsList = new HashMap<String, ModContainer>();
	
	public static final List<String> blacklistedTweaks = new ArrayList<String>();

	/** LaunchClassLoader's exception list, obtained via reflection */
	static Set<String> classloaderExceptions = null;	

	/** Meddle's directory in your instance directory */
	private static File meddleDir = null;
	
	/** Meddle's config directory */
	private static File configDir = null;
	
	/** Custom main class specified from arguments.
	 *  Only needed for < 1.6, or unique situations. */	  
	private static String customMainClass = null;
	
	public static boolean isBootstrapped = false;
	
	

	@SuppressWarnings("unchecked")
	public Meddle()
	{
		// Prevent classloader collisions, mostly due to keeping
		// the deprecated DynamicMappings class.
		Launch.classLoader.addClassLoaderExclusion("org.objectweb.asm.");

		blacklistedTweaks.add(Meddle.class.getName());

		// Launchwrapper adds the package without a period on the end, which
		// covers any similarly-named packages.  We could solve by putting
		// Meddle's tweak class in a deeper package, but this works too
		// while maintaining backwards compatibility.

		try {
			Field exceptionsField = LaunchClassLoader.class.getDeclaredField("classLoaderExceptions");
			exceptionsField.setAccessible(true);
			classloaderExceptions = (Set<String>) exceptionsField.get(Launch.classLoader);
		} catch (Exception e) { e.printStackTrace(); }

		classloaderExceptions.remove("net.fybertech.meddle");
		classloaderExceptions.add("net.fybertech.meddle.");
	}


	/** Get the current Meddle version. */
	public static String getVersion()
	{
		return "1.3.1";
	}

	
	/** 
	 * Gets the main Meddle directory, where mods are stored.
	 * By default, this is "meddle/"
	 * 
	 * Can be specified via --meddleDir argument. 
	 */
	public static File getMeddleDir()
	{
		return meddleDir;
	}
	
	
	/**
	 * Gets the recommended Meddle config file directory.
	 * By default, this is "meddle/config/"
	 * 	 
	 * Can be specified via --meddleConfigDir argument.
	 */
	public static File getConfigDir()
	{
		return configDir;
	}
	

	/** Determines if the specified mod ID has already been loaded */
	public static boolean isModLoaded(String modID)
	{
		return loadedModsList.get(modID) != null;
	}


	public static class ModContainer
	{
		public File jar;
		public Class<? extends ITweaker> tweakClass;
		public String transformerClass;
		public String id;

		public MeddleMod meta;

		public ModContainer(File f)
		{
			jar = f;
		}
	}


	/** Checks if the mod container contains any tweak or transformer class mods. */
	@SuppressWarnings("unchecked")
	private void checkJar(ModContainer mod)
	{
		Manifest manifest = null;
		try {
			manifest = new JarFile(mod.jar).getManifest();
		} catch (IOException e) {}
		if (manifest == null) return;

		Attributes attr = manifest.getMainAttributes();
		if (attr == null) return;

		String tweakClassName = attr.getValue("TweakClass");
		if (tweakClassName != null && tweakClassName.length() > 0 && !Meddle.blacklistedTweaks.contains(tweakClassName))
		{
			LOGGER.info("[Meddle] Found tweak class in " + mod.jar.getName() + " (" + tweakClassName + ")");

			// We delay loading the tweaks until Stage Two because:
			// 1) Adding it to Launchwrapper's TweakClasses list adds the package to the
			// classloader exception list, resulting in class not found.
			// 2) You can't add it to the other Tweaks list because it's currently being iterated.
			// 3) We want to finish initializing everything before they're instantiated
			// 4) We still need to sort them

			Class<? extends ITweaker> tweakClass = null;
			try {
				tweakClass = (Class<? extends ITweaker>) Class.forName(tweakClassName, false, Launch.classLoader);
			} catch (Exception e) {}

			if (tweakClass != null) {
				mod.tweakClass = tweakClass;
				mod.meta = tweakClass.getAnnotation(MeddleMod.class);
				mod.id = mod.meta != null ? mod.meta.id() : mod.jar.getName();
				loadedModsList.put(mod.id, mod);
			}
			else Meddle.LOGGER.error("[Meddle] Couldn't load tweak class " + tweakClassName);
			return;
		}


		// NOTE: Transformer classes aren't sorted for dependencies!

		String transformerClassName = attr.getValue("TransformerClass");
		if (transformerClassName != null && transformerClassName.length() > 0)
		{
			LOGGER.info("[Meddle] Found transformer class in " + mod.jar.getName() + " (" + transformerClassName + ")");

			Class<? extends IClassTransformer> transformerClass = null;
			try {
				transformerClass = (Class<? extends IClassTransformer>) Class.forName(transformerClassName, false, Launch.classLoader);
			} catch (Exception e) {}

			if (transformerClass != null) {
				mod.transformerClass = transformerClassName;
				mod.meta = transformerClass.getAnnotation(MeddleMod.class);
				mod.id = mod.meta != null ? mod.meta.id() : mod.jar.getName();
				loadedModsList.put(mod.id, mod);
			}
			else Meddle.LOGGER.error("[Meddle] Couldn't load transformer class " + tweakClassName);
			return;
		}
	}


	@Override
	public void acceptOptions(List<String> inArgs, File gameDir, File assetsDir, String profile)
	{
		String version = MeddleUtil.findMinecraftVersion();
		LOGGER.info("[Meddle] Minecraft version detected: " + version + " (" + (MeddleUtil.isClientJar() ? "client)" : "server)"));

		
		// Insert "stage two" tweak into list
		@SuppressWarnings("unchecked")
		List<String> tweakClasses = (List<String>)Launch.blackboard.get("TweakClasses");
		tweakClasses.add(StageTwo.class.getName());
		
		
		this.args.clear();
		meddleDir = null;
		configDir = null;	
		
		// Process arguments
		for (Iterator<String> it = inArgs.iterator(); it.hasNext();) {
			String arg = it.next();
			
			String argLower = arg.toLowerCase();
			if (argLower.equals("--meddledir")) {
				if (it.hasNext()) meddleDir = new File(it.next());
			}
			else if (argLower.equals("--meddlemain")) {
				if (it.hasNext()) customMainClass = it.next();
			}
			else if (argLower.equals("--meddleconfigdir")) {
				if (it.hasNext()) configDir = new File(it.next());
			}			
			else args.add(arg);
		}
		
		// If they specified a custom version name, pass it to Minecraft
		if (profile != null) {
			this.args.add("--version");
			this.args.add(profile);
		}

		// If they specified an assets dir, pass it to Minecraft
		if (assetsDir != null) {
			this.args.add("--assetsDir");
			this.args.add(assetsDir.getPath());
		}
		

		// If not specified, current directory is game directory
		if (gameDir == null) gameDir = new File(".");
		
		// If not specified, use "meddle/" for the mods directory
		if (meddleDir == null) meddleDir = new File(gameDir, "meddle/");
		if (!meddleDir.exists()) meddleDir.mkdirs();
		
		// If not specified, use "meddle/config/" for the mod config directory
		if (configDir == null) configDir = new File(meddleDir, "config/");
		if (!configDir.exists()) configDir.mkdirs();
				

		// Process Meddle directory for mods
		File[] files = meddleDir.listFiles();
		Arrays.sort(files);
		for (File f : files) {
			if (!f.getName().toLowerCase().endsWith(".jar")) continue;

			// Add it to the classloader even if it's not a mod.  Allows for
			// libraries, resources, etc.
			try {
				Launch.classLoader.addURL(f.toURI().toURL());
			} catch (MalformedURLException e) {
				e.printStackTrace();
			}

			ModContainer mod = new ModContainer(f);
			discoveredModsList.add(mod);
			checkJar(mod);
		}		
	}


	@Override
	public String[] getLaunchArguments() 
	{
		return args.toArray(new String[args.size()]);
	}


	@Override
	public String getLaunchTarget() 
	{
		if (customMainClass != null) return customMainClass;
		
		if (MeddleUtil.isClientJar()) return "net.minecraft.client.main.Main";
		else return "net.minecraft.server.MinecraftServer";
	}


	@Override
	public void injectIntoClassLoader(LaunchClassLoader classLoader)
	{
		// Register any transformer classes specified in mod manifests
		for (ModContainer mod : discoveredModsList)
		{
			if (mod.transformerClass != null) {
				try {
					Class.forName(mod.transformerClass, true, classLoader);
				} catch (ClassNotFoundException e) {}
				classLoader.registerTransformer(mod.transformerClass);
			}
		}
	}

}