public class Deobfuscator {
	
	public interface ProgressListener {
		void init(int totalWork, String title);
		void onProgress(int numDone, String message);
	}
	
	private JarFile m_jar;
	private DecompilerSettings m_settings;
	private JarIndex m_jarIndex;
	private Mappings m_mappings;
	private MappingsRenamer m_renamer;
	private Map<TranslationDirection,Translator> m_translatorCache;
	
	public Deobfuscator(JarFile jar) throws IOException {
		m_jar = jar;
		
		// build the jar index
		m_jarIndex = new JarIndex();
		m_jarIndex.indexJar(m_jar, true);
		
		// config the decompiler
		m_settings = DecompilerSettings.javaDefaults();
		m_settings.setMergeVariables(true);
		m_settings.setForceExplicitImports(true);
		m_settings.setForceExplicitTypeArguments(true);
		m_settings.setShowDebugLineNumbers(true);
		// DEBUG
		//m_settings.setShowSyntheticMembers(true);
		
		// init defaults
		m_translatorCache = Maps.newTreeMap();
		
		// init mappings
		setMappings(new Mappings());
		markWordAsDeObfuscate();
	}

	public void markWordAsDeObfuscate(){
		try{
			Map<Entry,Access> access = m_jarIndex.getAllAccess();
			Set<ClassEntry> classEntries = m_jarIndex.getObfClassEntries();
			for (ClassEntry classEntry:classEntries){
				try {
					String simpleName =classEntry.getSimpleName();
					if (checkNameValid(simpleName)) {
						this.markAsDeobfuscated(classEntry);
					}
				}catch (Exception e){
					System.out.println("mark word As De Obfuscate error:"+e);
				}
			}

			for (Entry entry:access.keySet()){
				if (entry instanceof MethodEntry){
					MethodEntry methodEntry = (MethodEntry)entry;
					if (checkNameValid(methodEntry.getName())){
						this.markAsDeobfuscated(entry);
					}
				}else if (entry instanceof FieldEntry){

				}

			}
		}catch (Exception e){
			System.out.println("mark word As De Obfuscate error:"+e);
		}

	}

	public boolean checkNameValid(String name){
		if (name!=null && name.length()>=5 && name.matches("[a-zA-Z0-9]+")){
			return true;
		}
		return false;
	}
	
	public JarFile getJar() {
		return m_jar;
	}
	
	public String getJarName() {
		return m_jar.getName();
	}
	
	public JarIndex getJarIndex() {
		return m_jarIndex;
	}
	
	public Mappings getMappings() {
		return m_mappings;
	}
	
	public void setMappings(Mappings val) {
		setMappings(val, true);
	}
	
	public void setMappings(Mappings val, boolean warnAboutDrops) {
		if (val == null) {
			val = new Mappings();
		}
		
		// drop mappings that don't match the jar
		MappingsChecker checker = new MappingsChecker(m_jarIndex);
		checker.dropBrokenMappings(val);
		if (warnAboutDrops) {
			for (java.util.Map.Entry<ClassEntry,ClassMapping> mapping : checker.getDroppedClassMappings().entrySet()) {
				System.out.println("WARNING: Couldn't find class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
			}
			for (java.util.Map.Entry<ClassEntry,ClassMapping> mapping : checker.getDroppedInnerClassMappings().entrySet()) {
				System.out.println("WARNING: Couldn't find inner class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
			}
			for (java.util.Map.Entry<FieldEntry,FieldMapping> mapping : checker.getDroppedFieldMappings().entrySet()) {
				System.out.println("WARNING: Couldn't find field entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
			}
			for (java.util.Map.Entry<BehaviorEntry,MethodMapping> mapping : checker.getDroppedMethodMappings().entrySet()) {
				System.out.println("WARNING: Couldn't find behavior entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
			}
		}
		
		// check for related method inconsistencies
		if (checker.getRelatedMethodChecker().hasProblems()) {
			throw new Error("Related methods are inconsistent! Need to fix the mappings manually.\n" + checker.getRelatedMethodChecker().getReport());
		}
		
		m_mappings = val;
		m_renamer = new MappingsRenamer(m_jarIndex, val);
		m_translatorCache.clear();
	}
	
	public Translator getTranslator(TranslationDirection direction) {
		Translator translator = m_translatorCache.get(direction);
		if (translator == null) {
			translator = m_mappings.getTranslator(direction, m_jarIndex.getTranslationIndex());
			m_translatorCache.put(direction, translator);
		}
		return translator;
	}
	public void getSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> deobfClasses,List<ClassEntry> nonePkgClass,List<ClassEntry> innerClass) {
		Set<ClassEntry> all = m_jarIndex.getObfClassEntries();
		for (ClassEntry obfClassEntry : all) {
			// skip inner classes
			if (obfClassEntry.isInnerClass()) {
				innerClass.add(obfClassEntry);
			}
			
			ClassEntry deobfClassEntry = deobfuscateEntry(obfClassEntry);
			// separate the classes
			if (isInDeObfuscatTrans(obfClassEntry) || !deobfClassEntry.equals(obfClassEntry)) {
				deobfClasses.add(deobfClassEntry);
			}else if (obfClassEntry.getPackageName().equals(Constants.NonePackage)){
				nonePkgClass.add(obfClassEntry);
			}else{
				// otherwise, assume it's still obfuscated
				obfClasses.add(obfClassEntry);
			}
		}
	}
	
	public void getSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> deobfClasses) {
		for (ClassEntry obfClassEntry : m_jarIndex.getObfClassEntries()) {
			// skip inner classes
			if (obfClassEntry.isInnerClass()) {
				continue;
			}
			
			// separate the classes
			ClassEntry deobfClassEntry = deobfuscateEntry(obfClassEntry);
			if (!deobfClassEntry.equals(obfClassEntry)) {
				// if the class has a mapping, clearly it's deobfuscated
				deobfClasses.add(deobfClassEntry);
			} else if (!obfClassEntry.getPackageName().equals(Constants.NonePackage)) {
				// also call it deobufscated if it's not in the none package
				deobfClasses.add(obfClassEntry);
			} else {
				// otherwise, assume it's still obfuscated
				obfClasses.add(obfClassEntry);
			}
		}
	}
	
	public CompilationUnit getSourceTree(String className) {
		
		// we don't know if this class name is obfuscated or deobfuscated
		// we need to tell the decompiler the deobfuscated name so it doesn't get freaked out
		// the decompiler only sees classes after deobfuscation, so we need to load it by the deobfuscated name if there is one
		
		// first, assume class name is deobf
		String deobfClassName = className;
		
		// if it wasn't actually deobf, then we can find a mapping for it and get the deobf name
		ClassMapping classMapping = m_mappings.getClassByObf(className);
		if (classMapping != null && classMapping.getDeobfName() != null) {
			deobfClassName = classMapping.getDeobfName();
		}
		
		// set the type loader
		TranslatingTypeLoader loader = new TranslatingTypeLoader(
			m_jar,
			m_jarIndex,
			getTranslator(TranslationDirection.Obfuscating),
			getTranslator(TranslationDirection.Deobfuscating)
		); 
		m_settings.setTypeLoader(loader);

		// see if procyon can find the type
		TypeReference type = new MetadataSystem(loader).lookupType(deobfClassName);
		if (type == null) {
			throw new Error(String.format("Unable to find type: %s (deobf: %s)\nTried class names: %s",
				className, deobfClassName, loader.getClassNamesToTry(deobfClassName)
			));
		}
		TypeDefinition resolvedType = type.resolve();
		
		// decompile it!
		DecompilerContext context = new DecompilerContext();
		context.setCurrentType(resolvedType);
		context.setSettings(m_settings);
		AstBuilder builder = new AstBuilder(context);
		builder.addType(resolvedType);
		builder.runTransformations(null);
		return builder.getCompilationUnit();
	}
	
	public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source) {
		return getSourceIndex(sourceTree, source, null);
	}
	
	public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source, Boolean ignoreBadTokens) {
		
		// build the source index
		SourceIndex index;
		if (ignoreBadTokens != null) {
			index = new SourceIndex(source, ignoreBadTokens);
		} else {
			index = new SourceIndex(source);
		}
		sourceTree.acceptVisitor(new SourceIndexVisitor(), index);
		
		// DEBUG
		// sourceTree.acceptVisitor( new TreeDumpVisitor( new File( "tree.txt" ) ), null );
		
		// resolve all the classes in the source references
		for (Token token : index.referenceTokens()) {
			EntryReference<Entry,Entry> deobfReference = index.getDeobfReference(token);
			
			// get the obfuscated entry
			Entry obfEntry = obfuscateEntry(deobfReference.entry);
			
			// try to resolve the class
			ClassEntry resolvedObfClassEntry = m_jarIndex.getTranslationIndex().resolveEntryClass(obfEntry);
			if (resolvedObfClassEntry != null && !resolvedObfClassEntry.equals(obfEntry.getClassEntry())) {
				// change the class of the entry
				obfEntry = obfEntry.cloneToNewClass(resolvedObfClassEntry);
				
				// save the new deobfuscated reference
				deobfReference.entry = deobfuscateEntry(obfEntry);
				index.replaceDeobfReference(token, deobfReference);
			}
			
			// DEBUG
			// System.out.println( token + " -> " + reference + " -> " + index.getReferenceToken( reference ) );
		}
		
		return index;
	}
	
	public String getSource(CompilationUnit sourceTree) {
		// render the AST into source
		StringWriter buf = new StringWriter();
		sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null);
		sourceTree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(buf), m_settings), null);
		return buf.toString();
	}
	
	public void writeSources(File dirOut, ProgressListener progress) throws IOException {
		// get the classes to decompile
		Set<ClassEntry> classEntries = Sets.newHashSet();
		for (ClassEntry obfClassEntry : m_jarIndex.getObfClassEntries()) {
			// skip inner classes
			if (obfClassEntry.isInnerClass()) {
				continue;
			}
			
			classEntries.add(obfClassEntry);
		}
		
		if (progress != null) {
			progress.init(classEntries.size(), "Decompiling classes...");
		}
		
		// DEOBFUSCATE ALL THE THINGS!! @_@
		int i = 0;
		for (ClassEntry obfClassEntry : classEntries) {
			ClassEntry deobfClassEntry = deobfuscateEntry(new ClassEntry(obfClassEntry));
			if (progress != null) {
				progress.onProgress(i++, deobfClassEntry.toString());
			}
			
			try {
				// get the source
				String source = getSource(getSourceTree(obfClassEntry.getName()));
				
				// write the file
				File file = new File(dirOut, deobfClassEntry.getName().replace('.', '/') + ".java");
				file.getParentFile().mkdirs();
				try (FileWriter out = new FileWriter(file)) {
					out.write(source);
				}
			} catch (Throwable t) {
				// don't crash the whole world here, just log the error and keep going
				// TODO: set up logback via log4j
				System.err.println("Unable to deobfuscate class " + deobfClassEntry.toString() + " (" + obfClassEntry.toString() + ")");
				t.printStackTrace(System.err);
			}
		}
		if (progress != null) {
			progress.onProgress(i, "Done!");
		}
	}
	
	public void writeJar(File out, ProgressListener progress) {
		final TranslatingTypeLoader loader = new TranslatingTypeLoader(
			m_jar,
			m_jarIndex,
			getTranslator(TranslationDirection.Obfuscating),
			getTranslator(TranslationDirection.Deobfuscating)
		);
		transformJar(out, progress, new ClassTransformer() {

			@Override
			public CtClass transform(CtClass c) throws Exception {
				return loader.transformClass(c);
			}
		});
	}
	
	public void protectifyJar(File out, ProgressListener progress) {
		transformJar(out, progress, new ClassTransformer() {

			@Override
			public CtClass transform(CtClass c) throws Exception {
				return ClassProtectifier.protectify(c);
			}
		});
	}
	
	public void publifyJar(File out, ProgressListener progress) {
		transformJar(out, progress, new ClassTransformer() {

			@Override
			public CtClass transform(CtClass c) throws Exception {
				return ClassPublifier.publify(c);
			}
		});
	}
	
	private interface ClassTransformer {
		public CtClass transform(CtClass c) throws Exception;
	}
	private void transformJar(File out, ProgressListener progress, ClassTransformer transformer) {
		try (JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) {
			if (progress != null) {
				progress.init(JarClassIterator.getClassEntries(m_jar).size(), "Transforming classes...");
			}
			
			int i = 0;
			for (CtClass c : JarClassIterator.classes(m_jar)) {
				if (progress != null) {
					progress.onProgress(i++, c.getName());
				}
				
				try {
					c = transformer.transform(c);
					outJar.putNextEntry(new JarEntry(c.getName().replace('.', '/') + ".class"));
					outJar.write(c.toBytecode());
					outJar.closeEntry();
				} catch (Throwable t) {
					throw new Error("Unable to transform class " + c.getName(), t);
				}
			}
			if (progress != null) {
				progress.onProgress(i, "Done!");
			}
			
			outJar.close();
		} catch (IOException ex) {
			throw new Error("Unable to write to Jar file!");
		}
	}
	
	public <T extends Entry> T obfuscateEntry(T deobfEntry) {
		if (deobfEntry == null) {
			return null;
		}
		return getTranslator(TranslationDirection.Obfuscating).translateEntry(deobfEntry);
	}
	
	public <T extends Entry> T deobfuscateEntry(T obfEntry) {
		if (obfEntry == null) {
			return null;
		}
		return getTranslator(TranslationDirection.Deobfuscating).translateEntry(obfEntry);
	}

	public boolean isInDeObfuscatTrans(Entry obfEntry){
		if (obfEntry == null) {
			return false;
		}
		Translator translator = getTranslator(TranslationDirection.Deobfuscating);
		return translator.contains(obfEntry);
	}
	
	public <E extends Entry,C extends Entry> EntryReference<E,C> obfuscateReference(EntryReference<E,C> deobfReference) {
		if (deobfReference == null) {
			return null;
		}
		return new EntryReference<E,C>(
			obfuscateEntry(deobfReference.entry),
			obfuscateEntry(deobfReference.context),
			deobfReference
		);
	}
	
	public <E extends Entry,C extends Entry> EntryReference<E,C> deobfuscateReference(EntryReference<E,C> obfReference) {
		if (obfReference == null) {
			return null;
		}
		return new EntryReference<E,C>(
			deobfuscateEntry(obfReference.entry),
			deobfuscateEntry(obfReference.context),
			obfReference
		);
	}
	
	public boolean isObfuscatedIdentifier(Entry obfEntry) {
		
		if (obfEntry instanceof MethodEntry) {
			
			// HACKHACK: Object methods are not obfuscated identifiers
			MethodEntry obfMethodEntry = (MethodEntry)obfEntry;
			String name = obfMethodEntry.getName();
			String sig = obfMethodEntry.getSignature().toString();
			if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) {
				return false;
			} else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) {
				return false;
			} else if (name.equals("finalize") && sig.equals("()V")) {
				return false;
			} else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) {
				return false;
			} else if (name.equals("hashCode") && sig.equals("()I")) {
				return false;
			} else if (name.equals("notify") && sig.equals("()V")) {
				return false;
			} else if (name.equals("notifyAll") && sig.equals("()V")) {
				return false;
			} else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) {
				return false;
			} else if (name.equals("wait") && sig.equals("()V")) {
				return false;
			} else if (name.equals("wait") && sig.equals("(J)V")) {
				return false;
			} else if (name.equals("wait") && sig.equals("(JI)V")) {
				return false;
			}
		}
		
		return m_jarIndex.containsObfEntry(obfEntry);
	}
	
	public boolean isRenameable(EntryReference<Entry,Entry> obfReference) {
		return obfReference.isNamed() && isObfuscatedIdentifier(obfReference.getNameableEntry());
	}
	
	// NOTE: these methods are a bit messy... oh well
	
	public boolean hasDeobfuscatedName(Entry obfEntry) {
		Translator translator = getTranslator(TranslationDirection.Deobfuscating);
		if (obfEntry instanceof ClassEntry) {
			ClassEntry obfClass = (ClassEntry)obfEntry;
			List<ClassMapping> mappingChain = m_mappings.getClassMappingChain(obfClass);
			ClassMapping classMapping = mappingChain.get(mappingChain.size() - 1);
			return classMapping != null && classMapping.getDeobfName() != null;
		} else if (obfEntry instanceof FieldEntry) {
			return translator.translate((FieldEntry)obfEntry) != null;
		} else if (obfEntry instanceof MethodEntry) {
			return translator.translate((MethodEntry)obfEntry) != null;
		} else if (obfEntry instanceof ConstructorEntry) {
			// constructors have no names
			return false;
		} else if (obfEntry instanceof ArgumentEntry) {
			return translator.translate((ArgumentEntry)obfEntry) != null;
		} else {
			throw new Error("Unknown entry type: " + obfEntry.getClass().getName());
		}
	}
	
	public void rename(Entry obfEntry, String newName) {
		if (obfEntry instanceof ClassEntry) {
			m_renamer.setClassName((ClassEntry)obfEntry, Descriptor.toJvmName(newName));
		} else if (obfEntry instanceof FieldEntry) {
			m_renamer.setFieldName((FieldEntry)obfEntry, newName);
		} else if (obfEntry instanceof MethodEntry) {
			m_renamer.setMethodTreeName((MethodEntry)obfEntry, newName);
		} else if (obfEntry instanceof ConstructorEntry) {
			throw new IllegalArgumentException("Cannot rename constructors");
		} else if (obfEntry instanceof ArgumentEntry) {
			m_renamer.setArgumentName((ArgumentEntry)obfEntry, newName);
		} else {
			throw new Error("Unknown entry type: " + obfEntry.getClass().getName());
		}
		
		// clear caches
		m_translatorCache.clear();
	}
	
	public void removeMapping(Entry obfEntry) {
		if (obfEntry instanceof ClassEntry) {
			m_renamer.removeClassMapping((ClassEntry)obfEntry);
		} else if (obfEntry instanceof FieldEntry) {
			m_renamer.removeFieldMapping((FieldEntry)obfEntry);
		} else if (obfEntry instanceof MethodEntry) {
			m_renamer.removeMethodTreeMapping((MethodEntry)obfEntry);
		} else if (obfEntry instanceof ConstructorEntry) {
			throw new IllegalArgumentException("Cannot rename constructors");
		} else if (obfEntry instanceof ArgumentEntry) {
			m_renamer.removeArgumentMapping((ArgumentEntry)obfEntry);
		} else {
			throw new Error("Unknown entry type: " + obfEntry);
		}
		
		// clear caches
		m_translatorCache.clear();
	}
	
	public void markAsDeobfuscated(Entry obfEntry) {
		if (obfEntry instanceof ClassEntry) {
			m_renamer.markClassAsDeobfuscated((ClassEntry)obfEntry);
		} else if (obfEntry instanceof FieldEntry) {
			m_renamer.markFieldAsDeobfuscated((FieldEntry)obfEntry);
		} else if (obfEntry instanceof MethodEntry) {
			m_renamer.markMethodTreeAsDeobfuscated((MethodEntry)obfEntry);
		} else if (obfEntry instanceof ConstructorEntry) {
			throw new IllegalArgumentException("Cannot rename constructors");
		} else if (obfEntry instanceof ArgumentEntry) {
			m_renamer.markArgumentAsDeobfuscated((ArgumentEntry)obfEntry);
		} else {
			throw new Error("Unknown entry type: " + obfEntry);
		}
		
		// clear caches
		m_translatorCache.clear();
	}
} public interface ProgressListener {
	void init(int totalWork, String title);
	void onProgress(int numDone, String message);
} public class BehaviorReferenceTreeNode extends DefaultMutableTreeNode implements ReferenceTreeNode<BehaviorEntry,BehaviorEntry> {
	
	private static final long serialVersionUID = -3658163700783307520L;
	
	private Translator m_deobfuscatingTranslator;
	private BehaviorEntry m_entry;
	private EntryReference<BehaviorEntry,BehaviorEntry> m_reference;
	private Access m_access;
	
	public BehaviorReferenceTreeNode(Translator deobfuscatingTranslator, BehaviorEntry entry) {
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_entry = entry;
		m_reference = null;
	}
	
	public BehaviorReferenceTreeNode(Translator deobfuscatingTranslator, EntryReference<BehaviorEntry,BehaviorEntry> reference, Access access) {
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_entry = reference.entry;
		m_reference = reference;
		m_access = access;
	}
	
	@Override
	public BehaviorEntry getEntry() {
		return m_entry;
	}
	
	@Override
	public EntryReference<BehaviorEntry,BehaviorEntry> getReference() {
		return m_reference;
	}
	
	@Override
	public String toString() {
		if (m_reference != null) {
			return String.format("%s (%s)", m_deobfuscatingTranslator.translateEntry(m_reference.context), m_access);
		}
		return m_deobfuscatingTranslator.translateEntry(m_entry).toString();
	}
	
	public void load(JarIndex index, boolean recurse) {
		// get all the child nodes
		for (EntryReference<BehaviorEntry,BehaviorEntry> reference : index.getBehaviorReferences(m_entry)) {
			add(new BehaviorReferenceTreeNode(m_deobfuscatingTranslator, reference, index.getAccess(m_entry)));
		}
		
		if (recurse && children != null) {
			for (Object child : children) {
				if (child instanceof BehaviorReferenceTreeNode) {
					BehaviorReferenceTreeNode node = (BehaviorReferenceTreeNode)child;
					
					// don't recurse into ancestor
					Set<Entry> ancestors = Sets.newHashSet();
					TreeNode n = (TreeNode)node;
					while (n.getParent() != null) {
						n = n.getParent();
						if (n instanceof BehaviorReferenceTreeNode) {
							ancestors.add( ((BehaviorReferenceTreeNode)n).getEntry());
						}
					}
					if (ancestors.contains(node.getEntry())) {
						continue;
					}
					
					node.load(index, true);
				}
			}
		}
	}
} public class ClassInheritanceTreeNode extends DefaultMutableTreeNode {
	
	private static final long serialVersionUID = 4432367405826178490L;
	
	private Translator m_deobfuscatingTranslator;
	private String m_obfClassName;
	
	public ClassInheritanceTreeNode(Translator deobfuscatingTranslator, String obfClassName) {
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_obfClassName = obfClassName;
	}
	
	public String getObfClassName() {
		return m_obfClassName;
	}
	
	public String getDeobfClassName() {
		return m_deobfuscatingTranslator.translateClass(m_obfClassName);
	}
	
	@Override
	public String toString() {
		String deobfClassName = getDeobfClassName();
		if (deobfClassName != null) {
			return deobfClassName;
		}
		return m_obfClassName;
	}
	
	public void load(TranslationIndex ancestries, boolean recurse) {
		// get all the child nodes
		List<ClassInheritanceTreeNode> nodes = Lists.newArrayList();
		for (ClassEntry subclassEntry : ancestries.getSubclass(new ClassEntry(m_obfClassName))) {
			nodes.add(new ClassInheritanceTreeNode(m_deobfuscatingTranslator, subclassEntry.getName()));
		}
		
		// add them to this node
		for (ClassInheritanceTreeNode node : nodes) {
			this.add(node);
		}
		
		if (recurse) {
			for (ClassInheritanceTreeNode node : nodes) {
				node.load(ancestries, true);
			}
		}
	}
	
	public static ClassInheritanceTreeNode findNode(ClassInheritanceTreeNode node, ClassEntry entry) {
		// is this the node?
		if (node.getObfClassName().equals(entry.getName())) {
			return node;
		}
		
		// recurse
		for (int i = 0; i < node.getChildCount(); i++) {
			ClassInheritanceTreeNode foundNode = findNode((ClassInheritanceTreeNode)node.getChildAt(i), entry);
			if (foundNode != null) {
				return foundNode;
			}
		}
		return null;
	}
} public class EntryReference<E extends Entry,C extends Entry> {
	
	private static final List<String> ConstructorNonNames = Arrays.asList("this", "super", "static");
	public E entry;
	public C context;
	
	private boolean m_isNamed;
	
	public EntryReference(E entry, String sourceName) {
		this(entry, sourceName, null);
	}
	
	public EntryReference(E entry, String sourceName, C context) {
		if (entry == null) {
			throw new IllegalArgumentException("Entry cannot be null!");
		}
		
		this.entry = entry;
		this.context = context;
		
		m_isNamed = sourceName != null && sourceName.length() > 0;
		if (entry instanceof ConstructorEntry && ConstructorNonNames.contains(sourceName)) {
			m_isNamed = false;
		}
	}
	
	public EntryReference(E entry, C context, EntryReference<E,C> other) {
		this.entry = entry;
		this.context = context;
		m_isNamed = other.m_isNamed;
	}
	
	public ClassEntry getLocationClassEntry() {
		if (context != null) {
			return context.getClassEntry();
		}
		return entry.getClassEntry();
	}
	
	public boolean isNamed() {
		return m_isNamed;
	}
	
	public Entry getNameableEntry() {
		if (entry instanceof ConstructorEntry) {
			// renaming a constructor really means renaming the class
			return entry.getClassEntry();
		}
		return entry;
	}
	
	public String getNamableName() {
		if (getNameableEntry() instanceof ClassEntry) {
			ClassEntry classEntry = (ClassEntry)getNameableEntry();
			if (classEntry.isInnerClass()) {
				// make sure we only rename the inner class name
				return classEntry.getInnermostClassName();
			}
		}
		
		return getNameableEntry().getName();
	}
	
	@Override
	public int hashCode() {
		if (context != null) {
			return Util.combineHashesOrdered(entry.hashCode(), context.hashCode());
		}
		return entry.hashCode();
	}
	
	@Override
	public boolean equals(Object other) {
		if (other instanceof EntryReference) {
			return equals((EntryReference<?,?>)other);
		}
		return false;
	}
	
	public boolean equals(EntryReference<?,?> other) {
		// check entry first
		boolean isEntrySame = entry.equals(other.entry);
		if (!isEntrySame) {
			return false;
		}
		
		// check caller
		if (context == null && other.context == null) {
			return true;
		} else if (context != null && other.context != null) {
			return context.equals(other.context);
		}
		return false;
	}
	
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append(entry);
		if (context != null) {
			buf.append(" called from ");
			buf.append(context);
		}
		return buf.toString();
	}
} public class FieldReferenceTreeNode extends DefaultMutableTreeNode implements ReferenceTreeNode<FieldEntry,BehaviorEntry> {
	
	private static final long serialVersionUID = -7934108091928699835L;
	
	private Translator m_deobfuscatingTranslator;
	private FieldEntry m_entry;
	private EntryReference<FieldEntry,BehaviorEntry> m_reference;
	private Access m_access;
	
	public FieldReferenceTreeNode(Translator deobfuscatingTranslator, FieldEntry entry) {
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_entry = entry;
		m_reference = null;
	}
	
	private FieldReferenceTreeNode(Translator deobfuscatingTranslator, EntryReference<FieldEntry,BehaviorEntry> reference, Access access) {
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_entry = reference.entry;
		m_reference = reference;
		m_access = access;
	}
	
	@Override
	public FieldEntry getEntry() {
		return m_entry;
	}
	
	@Override
	public EntryReference<FieldEntry,BehaviorEntry> getReference() {
		return m_reference;
	}
	
	@Override
	public String toString() {
		if (m_reference != null) {
			return String.format("%s (%s)", m_deobfuscatingTranslator.translateEntry(m_reference.context), m_access);
		}
		return m_deobfuscatingTranslator.translateEntry(m_entry).toString();
	}
	
	public void load(JarIndex index, boolean recurse) {
		// get all the child nodes
		if (m_reference == null) {
			for (EntryReference<FieldEntry,BehaviorEntry> reference : index.getFieldReferences(m_entry)) {
				add(new FieldReferenceTreeNode(m_deobfuscatingTranslator, reference, index.getAccess(m_entry)));
			}
		} else {
			for (EntryReference<BehaviorEntry,BehaviorEntry> reference : index.getBehaviorReferences(m_reference.context)) {
				add(new BehaviorReferenceTreeNode(m_deobfuscatingTranslator, reference, index.getAccess(m_reference.context)));
			}
		}
		
		if (recurse && children != null) {
			for (Object node : children) {
				if (node instanceof BehaviorReferenceTreeNode) {
					((BehaviorReferenceTreeNode)node).load(index, true);
				} else if (node instanceof FieldReferenceTreeNode) {
					((FieldReferenceTreeNode)node).load(index, true);
				}
			}
		}
	}
} public class MethodImplementationsTreeNode extends DefaultMutableTreeNode {
	
	private static final long serialVersionUID = 3781080657461899915L;
	
	private Translator m_deobfuscatingTranslator;
	private MethodEntry m_entry;
	
	public MethodImplementationsTreeNode(Translator deobfuscatingTranslator, MethodEntry entry) {
		if (entry == null) {
			throw new IllegalArgumentException("entry cannot be null!");
		}
		
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_entry = entry;
	}
	
	public MethodEntry getMethodEntry() {
		return m_entry;
	}
	
	public String getDeobfClassName() {
		return m_deobfuscatingTranslator.translateClass(m_entry.getClassName());
	}
	
	public String getDeobfMethodName() {
		return m_deobfuscatingTranslator.translate(m_entry);
	}
	
	@Override
	public String toString() {
		String className = getDeobfClassName();
		if (className == null) {
			className = m_entry.getClassName();
		}
		
		String methodName = getDeobfMethodName();
		if (methodName == null) {
			methodName = m_entry.getName();
		}
		return className + "." + methodName + "()";
	}
	
	public void load(JarIndex index) {
		
		// get all method implementations
		List<MethodImplementationsTreeNode> nodes = Lists.newArrayList();
		for (String implementingClassName : index.getImplementingClasses(m_entry.getClassName())) {
			MethodEntry methodEntry = new MethodEntry(
				new ClassEntry(implementingClassName),
				m_entry.getName(),
				m_entry.getSignature()
			);
			if (index.containsObfBehavior(methodEntry)) {
				nodes.add(new MethodImplementationsTreeNode(m_deobfuscatingTranslator, methodEntry));
			}
		}
		
		// add them to this node
		for (MethodImplementationsTreeNode node : nodes) {
			this.add(node);
		}
	}
	
	public static MethodImplementationsTreeNode findNode(MethodImplementationsTreeNode node, MethodEntry entry) {
		// is this the node?
		if (node.getMethodEntry().equals(entry)) {
			return node;
		}
		
		// recurse
		for (int i = 0; i < node.getChildCount(); i++) {
			MethodImplementationsTreeNode foundNode = findNode((MethodImplementationsTreeNode)node.getChildAt(i), entry);
			if (foundNode != null) {
				return foundNode;
			}
		}
		return null;
	}
} public class MethodInheritanceTreeNode extends DefaultMutableTreeNode {
	
	private static final long serialVersionUID = 1096677030991810007L;
	
	private Translator m_deobfuscatingTranslator;
	private MethodEntry m_entry;
	private boolean m_isImplemented;
	
	public MethodInheritanceTreeNode(Translator deobfuscatingTranslator, MethodEntry entry, boolean isImplemented) {
		m_deobfuscatingTranslator = deobfuscatingTranslator;
		m_entry = entry;
		m_isImplemented = isImplemented;
	}
	
	public MethodEntry getMethodEntry() {
		return m_entry;
	}
	
	public String getDeobfClassName() {
		return m_deobfuscatingTranslator.translateClass(m_entry.getClassName());
	}
	
	public String getDeobfMethodName() {
		return m_deobfuscatingTranslator.translate(m_entry);
	}
	
	public boolean isImplemented() {
		return m_isImplemented;
	}
	
	@Override
	public String toString() {
		String className = getDeobfClassName();
		if (className == null) {
			className = m_entry.getClassName();
		}
		
		if (!m_isImplemented) {
			return className;
		} else {
			String methodName = getDeobfMethodName();
			if (methodName == null) {
				methodName = m_entry.getName();
			}
			return className + "." + methodName + "()";
		}
	}
	
	public void load(JarIndex index, boolean recurse) {
		// get all the child nodes
		List<MethodInheritanceTreeNode> nodes = Lists.newArrayList();
		for (ClassEntry subclassEntry : index.getTranslationIndex().getSubclass(m_entry.getClassEntry())) {
			MethodEntry methodEntry = new MethodEntry(
				subclassEntry,
				m_entry.getName(),
				m_entry.getSignature()
			);
			nodes.add(new MethodInheritanceTreeNode(
				m_deobfuscatingTranslator,
				methodEntry,
				index.containsObfBehavior(methodEntry)
			));
		}
		
		// add them to this node
		for (MethodInheritanceTreeNode node : nodes) {
			this.add(node);
		}
		
		if (recurse) {
			for (MethodInheritanceTreeNode node : nodes) {
				node.load(index, true);
			}
		}
	}
	
	public static MethodInheritanceTreeNode findNode(MethodInheritanceTreeNode node, MethodEntry entry) {
		// is this the node?
		if (node.getMethodEntry().equals(entry)) {
			return node;
		}
		
		// recurse
		for (int i = 0; i < node.getChildCount(); i++) {
			MethodInheritanceTreeNode foundNode = findNode((MethodInheritanceTreeNode)node.getChildAt(i), entry);
			if (foundNode != null) {
				return foundNode;
			}
		}
		return null;
	}
} public class SourceIndex {
	
	private String m_source;
	private TreeMap<Token,EntryReference<Entry,Entry>> m_tokenToReference;
	private Multimap<EntryReference<Entry,Entry>,Token> m_referenceToTokens;
	private Map<Entry,Token> m_declarationToToken;
	private List<Integer> m_lineOffsets;
	private boolean m_ignoreBadTokens;
	
	public SourceIndex(String source) {
		this(source, false);
	}
	
	public SourceIndex(String source, boolean ignoreBadTokens) {
		m_source = source;
		m_ignoreBadTokens = ignoreBadTokens;
		m_tokenToReference = Maps.newTreeMap();
		m_referenceToTokens = HashMultimap.create();
		m_declarationToToken = Maps.newHashMap();
		m_lineOffsets = Lists.newArrayList();
		
		// count the lines
		m_lineOffsets.add(0);
		for (int i = 0; i < source.length(); i++) {
			if (source.charAt(i) == '\n') {
				m_lineOffsets.add(i + 1);
			}
		}
	}
	
	public String getSource() {
		return m_source;
	}
	
	public Token getToken(AstNode node) {
		
		// get the text of the node
		String name = "";
		if (node instanceof Identifier) {
			name = ((Identifier)node).getName();
		}
		
		// get a token for this node's region
		Region region = node.getRegion();
		if (region.getBeginLine() == 0 || region.getEndLine() == 0) {
			// DEBUG
			System.err.println(String.format("WARNING: %s \"%s\" has invalid region: %s", node.getNodeType(), name, region));
			return null;
		}
		Token token = new Token(
			toPos(region.getBeginLine(), region.getBeginColumn()),
			toPos(region.getEndLine(), region.getEndColumn()),
			m_source
		);
		if (token.start == 0) {
			// DEBUG
			System.err.println(String.format("WARNING: %s \"%s\" has invalid start: %s", node.getNodeType(), name, region));
			return null;
		}
		
		// DEBUG
		// System.out.println( String.format( "%s \"%s\" region: %s", node.getNodeType(), name, region ) );
		
		// if the token has a $ in it, something's wrong. Ignore this token
		if (name.lastIndexOf('$') >= 0 && m_ignoreBadTokens) {
			// DEBUG
			System.err.println(String.format("WARNING: %s \"%s\" is probably a bad token. It was ignored", node.getNodeType(), name));
			return null;
		}
		
		return token;
	}
	
	public void addReference(AstNode node, Entry deobfEntry, Entry deobfContext) {
		Token token = getToken(node);
		if (token != null) {
			EntryReference<Entry,Entry> deobfReference = new EntryReference<Entry,Entry>(deobfEntry, token.text, deobfContext);
			m_tokenToReference.put(token, deobfReference);
			m_referenceToTokens.put(deobfReference, token);
		}
	}
	
	public void addDeclaration(AstNode node, Entry deobfEntry) {
		Token token = getToken(node);
		if (token != null) {
			EntryReference<Entry,Entry> reference = new EntryReference<Entry,Entry>(deobfEntry, token.text);
			m_tokenToReference.put(token, reference);
			m_referenceToTokens.put(reference, token);
			m_declarationToToken.put(deobfEntry, token);
		}
	}
	
	public Token getReferenceToken(int pos) {
		Token token = m_tokenToReference.floorKey(new Token(pos, pos, null));
		if (token != null && token.contains(pos)) {
			return token;
		}
		return null;
	}
	
	public Collection<Token> getReferenceTokens(EntryReference<Entry,Entry> deobfReference) {
		return m_referenceToTokens.get(deobfReference);
	}
	
	public EntryReference<Entry,Entry> getDeobfReference(Token token) {
		if (token == null) {
			return null;
		}
		return m_tokenToReference.get(token);
	}
	
	public void replaceDeobfReference(Token token, EntryReference<Entry,Entry> newDeobfReference) {
		EntryReference<Entry,Entry> oldDeobfReference = m_tokenToReference.get(token);
		m_tokenToReference.put(token, newDeobfReference);
		Collection<Token> tokens = m_referenceToTokens.get(oldDeobfReference);
		m_referenceToTokens.removeAll(oldDeobfReference);
		m_referenceToTokens.putAll(newDeobfReference, tokens);
	}
	
	public Iterable<Token> referenceTokens() {
		return m_tokenToReference.keySet();
	}
	
	public Iterable<Token> declarationTokens() {
		return m_declarationToToken.values();
	}
	
	public Iterable<Entry> declarations() {
		return m_declarationToToken.keySet();
	}
	
	public Token getDeclarationToken(Entry deobfEntry) {
		return m_declarationToToken.get(deobfEntry);
	}
	
	public int getLineNumber(int pos) {
		// line number is 1-based
		int line = 0;
		for (Integer offset : m_lineOffsets) {
			if (offset > pos) {
				break;
			}
			line++;
		}
		return line;
	}
	
	public int getColumnNumber(int pos) {
		// column number is 1-based
		return pos - m_lineOffsets.get(getLineNumber(pos) - 1) + 1;
	}
	
	private int toPos(int line, int col) {
		// line and col are 1-based
		return m_lineOffsets.get(line - 1) + col - 1;
	}
} public interface BehaviorEntry extends Entry {
	Signature getSignature();
} public class ClassEntry implements Entry, Serializable {
	
	private static final long serialVersionUID = 4235460580973955811L;
	
	private String m_name;
	
	public ClassEntry(String className) {
		if (className == null) {
			throw new IllegalArgumentException("Class name cannot be null!");
		}
		if (className.indexOf('.') >= 0) {
			throw new IllegalArgumentException("Class name must be in JVM format. ie, path/to/package/class$inner : " + className);
		}
		
		m_name = className;
		
		if (isInnerClass() && getInnermostClassName().indexOf('/') >= 0) {
			throw new IllegalArgumentException("Inner class must not have a package: " + className);
		}
	}
	
	public ClassEntry(ClassEntry other) {
		m_name = other.m_name;
	}
	
	@Override
	public String getName() {
		return m_name;
	}
	
	@Override
	public String getClassName() {
		return m_name;
	}
	
	@Override
	public ClassEntry getClassEntry() {
		return this;
	}
	
	@Override
	public ClassEntry cloneToNewClass(ClassEntry classEntry) {
		return classEntry;
	}
	
	@Override
	public int hashCode() {
		return m_name.hashCode();
	}
	
	@Override
	public boolean equals(Object other) {
		if (other instanceof ClassEntry) {
			return equals((ClassEntry)other);
		}
		return false;
	}
	
	public boolean equals(ClassEntry other) {
		return m_name.equals(other.m_name);
	}
	
	@Override
	public String toString() {
		return m_name;
	}
	
	public boolean isInnerClass() {
		return m_name.lastIndexOf('$') >= 0;
	}
	
	public List<String> getClassChainNames() {
		return Lists.newArrayList(m_name.split("\\$"));
	}
	
	public List<ClassEntry> getClassChain() {
		List<ClassEntry> entries = Lists.newArrayList();
		StringBuilder buf = new StringBuilder();
		for (String name : getClassChainNames()) {
			if (buf.length() > 0) {
				buf.append("$");
			}
			buf.append(name);
			entries.add(new ClassEntry(buf.toString()));
		}
		return entries;
	}
	
	public String getOutermostClassName() {
		if (isInnerClass()) {
			return m_name.substring(0, m_name.indexOf('$'));
		}
		return m_name;
	}

	public String getOutermostClassInnerName() {
		if (isInnerClass()) {
			String className = m_name.substring(m_name.indexOf('$')+1, m_name.length());
			String pkgname = getPackageName();
			return pkgname+"/"+className;
		}
		return m_name;
	}
	
	public ClassEntry getOutermostClassEntry() {
		return new ClassEntry(getOutermostClassName());
	}

	public ClassEntry getOutermostInnerClassEntry(){
		return new ClassEntry(getOutermostClassInnerName());
	}
	
	public String getOuterClassName() {
		if (!isInnerClass()) {
			throw new Error("This is not an inner class!");
		}
		return m_name.substring(0, m_name.lastIndexOf('$'));
	}
	
	public ClassEntry getOuterClassEntry() {
		return new ClassEntry(getOuterClassName());
	}
	
	public String getInnermostClassName() {
		if (!isInnerClass()) {
			throw new Error("This is not an inner class!");
		}
		return m_name.substring(m_name.lastIndexOf('$') + 1);
	}
	
	public boolean isInDefaultPackage() {
		return m_name.indexOf('/') < 0;
	}
	
	public String getPackageName() {
		int pos = m_name.lastIndexOf('/');
		if (pos > 0) {
			return m_name.substring(0, pos);
		}
		return null;
	}
	
	public String getSimpleName() {
		int pos = m_name.lastIndexOf('/');
		if (pos > 0) {
			return m_name.substring(pos + 1);
		}
		return m_name;
	}
	
	public ClassEntry buildClassEntry(List<ClassEntry> classChain) {
		assert(classChain.contains(this));
		StringBuilder buf = new StringBuilder();
		for (ClassEntry chainEntry : classChain) {
			if (buf.length() == 0) {
				buf.append(chainEntry.getName());
			} else {
				buf.append("$");
				buf.append(chainEntry.isInnerClass() ? chainEntry.getInnermostClassName() : chainEntry.getSimpleName());
			}
			
			if (chainEntry == this) {
				break;
			}
		}
		return new ClassEntry(buf.toString());
	}
} public class FieldEntry implements Entry, Serializable {
	
	private static final long serialVersionUID = 3004663582802885451L;
	
	private ClassEntry m_classEntry;
	private String m_name;
	private Type m_type;
	
	// NOTE: this argument order is important for the MethodReader/MethodWriter
	public FieldEntry(ClassEntry classEntry, String name, Type type) {
		if (classEntry == null) {
			throw new IllegalArgumentException("Class cannot be null!");
		}
		if (name == null) {
			throw new IllegalArgumentException("Field name cannot be null!");
		}
		if (type == null) {
			throw new IllegalArgumentException("Field type cannot be null!");
		}
		
		m_classEntry = classEntry;
		m_name = name;
		m_type = type;
	}
	
	public FieldEntry(FieldEntry other) {
		this(other, new ClassEntry(other.m_classEntry));
	}
	
	public FieldEntry(FieldEntry other, ClassEntry newClassEntry) {
		m_classEntry = newClassEntry;
		m_name = other.m_name;
		m_type = other.m_type;
	}
	
	@Override
	public ClassEntry getClassEntry() {
		return m_classEntry;
	}
	
	@Override
	public String getName() {
		return m_name;
	}
	
	@Override
	public String getClassName() {
		return m_classEntry.getName();
	}
	
	public Type getType() {
		return m_type;
	}
	
	@Override
	public FieldEntry cloneToNewClass(ClassEntry classEntry) {
		return new FieldEntry(this, classEntry);
	}
	
	@Override
	public int hashCode() {
		return Util.combineHashesOrdered(m_classEntry, m_name, m_type);
	}
	
	@Override
	public boolean equals(Object other) {
		if (other instanceof FieldEntry) {
			return equals((FieldEntry)other);
		}
		return false;
	}
	
	public boolean equals(FieldEntry other) {
		return m_classEntry.equals(other.m_classEntry)
			&& m_name.equals(other.m_name)
			&& m_type.equals(other.m_type);
	}
	
	@Override
	public String toString() {
		return m_classEntry.getName() + "." + m_name + ":" + m_type;
	}
} public class MappingsReader {
	
	public Mappings read(Reader in)
	throws IOException, MappingParseException {
		return read(new BufferedReader(in));
	}
	
	public Mappings read(BufferedReader in)
	throws IOException, MappingParseException {
		Mappings mappings = new Mappings();
		Deque<Object> mappingStack = Queues.newArrayDeque();
		
		int lineNumber = 0;
		String line = null;
		while ( (line = in.readLine()) != null) {
			lineNumber++;
			
			// strip comments
			int commentPos = line.indexOf('#');
			if (commentPos >= 0) {
				line = line.substring(0, commentPos);
			}
			
			// skip blank lines
			if (line.trim().length() <= 0) {
				continue;
			}
			
			// get the indent of this line
			int indent = 0;
			for (int i = 0; i < line.length(); i++) {
				if (line.charAt(i) != '\t') {
					break;
				}
				indent++;
			}
			
			// handle stack pops
			while (indent < mappingStack.size()) {
				mappingStack.pop();
			}
			
			String[] parts = line.trim().split("\\s");
			try {
				// read the first token
				String token = parts[0];
				
				if (token.equalsIgnoreCase("CLASS")) {
					ClassMapping classMapping;
					if (indent <= 0) {
						// outer class
						classMapping = readClass(parts, false);
						mappings.addClassMapping(classMapping);
					} else {
						
						// inner class
						if (!(mappingStack.peek() instanceof ClassMapping)) {
							throw new MappingParseException(lineNumber, "Unexpected CLASS entry here!");
						}
						
						classMapping = readClass(parts, true);
						((ClassMapping)mappingStack.peek()).addInnerClassMapping(classMapping);
					}
					mappingStack.push(classMapping);
				} else if (token.equalsIgnoreCase("FIELD")) {
					if (mappingStack.isEmpty() || ! (mappingStack.peek() instanceof ClassMapping)) {
						throw new MappingParseException(lineNumber, "Unexpected FIELD entry here!");
					}
					((ClassMapping)mappingStack.peek()).addFieldMapping(readField(parts));
				} else if (token.equalsIgnoreCase("METHOD")) {
					if (mappingStack.isEmpty() || ! (mappingStack.peek() instanceof ClassMapping)) {
						throw new MappingParseException(lineNumber, "Unexpected METHOD entry here!");
					}
					MethodMapping methodMapping = readMethod(parts);
					((ClassMapping)mappingStack.peek()).addMethodMapping(methodMapping);
					mappingStack.push(methodMapping);
				} else if (token.equalsIgnoreCase("ARG")) {
					if (mappingStack.isEmpty() || ! (mappingStack.peek() instanceof MethodMapping)) {
						throw new MappingParseException(lineNumber, "Unexpected ARG entry here!");
					}
					((MethodMapping)mappingStack.peek()).addArgumentMapping(readArgument(parts));
				}
			} catch (ArrayIndexOutOfBoundsException | IllegalArgumentException ex) {
				throw new MappingParseException(lineNumber, "Malformed line:\n" + line);
			}
		}
		
		return mappings;
	}
	
	private ArgumentMapping readArgument(String[] parts) {
		return new ArgumentMapping(Integer.parseInt(parts[1]), parts[2]);
	}
	
	private ClassMapping readClass(String[] parts, boolean makeSimple) {
		if (parts.length == 2) {
			return new ClassMapping(parts[1]);
		} else {
			return new ClassMapping(parts[1], parts[2]);
		}
	}
	
	/* TEMP */
	protected FieldMapping readField(String[] parts) {
		return new FieldMapping(parts[1], new Type(parts[3]), parts[2]);
	}
	
	private MethodMapping readMethod(String[] parts) {
		if (parts.length == 3) {
			return new MethodMapping(parts[1], new Signature(parts[2]));
		} else {
			return new MethodMapping(parts[1], new Signature(parts[3]), parts[2]);
		}
	}
} public class MappingsWriter {
	
	public void write(Writer out, Mappings mappings) throws IOException {
		write(new PrintWriter(out), mappings);
	}
	
	public void write(PrintWriter out, Mappings mappings) throws IOException {
		for (ClassMapping classMapping : sorted(mappings.classes())) {
			write(out, classMapping, 0);
		}
	}
	
	private void write(PrintWriter out, ClassMapping classMapping, int depth) throws IOException {
		if (classMapping.getDeobfName() == null) {
			out.format("%sCLASS %s\n", getIndent(depth), classMapping.getObfFullName());
		} else {
			out.format("%sCLASS %s %s\n", getIndent(depth), classMapping.getObfFullName(), classMapping.getDeobfName());
		}
		
		for (ClassMapping innerClassMapping : sorted(classMapping.innerClasses())) {
			write(out, innerClassMapping, depth + 1);
		}
		
		for (FieldMapping fieldMapping : sorted(classMapping.fields())) {
			write(out, fieldMapping, depth + 1);
		}
		
		for (MethodMapping methodMapping : sorted(classMapping.methods())) {
			write(out, methodMapping, depth + 1);
		}
	}
	
	private void write(PrintWriter out, FieldMapping fieldMapping, int depth) throws IOException {
		out.format("%sFIELD %s %s %s\n", getIndent(depth), fieldMapping.getObfName(), fieldMapping.getDeobfName(), fieldMapping.getObfType().toString());
	}
	
	private void write(PrintWriter out, MethodMapping methodMapping, int depth) throws IOException {
		if (methodMapping.getDeobfName() == null) {
			out.format("%sMETHOD %s %s\n", getIndent(depth), methodMapping.getObfName(), methodMapping.getObfSignature());
		} else {
			out.format("%sMETHOD %s %s %s\n", getIndent(depth), methodMapping.getObfName(), methodMapping.getDeobfName(), methodMapping.getObfSignature());
		}
		
		for (ArgumentMapping argumentMapping : sorted(methodMapping.arguments())) {
			write(out, argumentMapping, depth + 1);
		}
	}
	
	private void write(PrintWriter out, ArgumentMapping argumentMapping, int depth) throws IOException {
		out.format("%sARG %d %s\n", getIndent(depth), argumentMapping.getIndex(), argumentMapping.getName());
	}
	
	private <T extends Comparable<T>> List<T> sorted(Iterable<T> classes) {
		List<T> out = new ArrayList<T>();
		for (T t : classes) {
			out.add(t);
		}
		Collections.sort(out);
		return out;
	}
	
	private String getIndent(int depth) {
		StringBuilder buf = new StringBuilder();
		for (int i = 0; i < depth; i++) {
			buf.append("\t");
		}
		return buf.toString();
	}
} public class MethodEntry implements BehaviorEntry, Serializable {
	
	private static final long serialVersionUID = 4770915224467247458L;
	
	private ClassEntry m_classEntry;
	private String m_name;
	private Signature m_signature;
	
	public MethodEntry(ClassEntry classEntry, String name, Signature signature) {
		if (classEntry == null) {
			throw new IllegalArgumentException("Class cannot be null!");
		}
		if (name == null) {
			throw new IllegalArgumentException("Method name cannot be null!");
		}
		if (signature == null) {
			throw new IllegalArgumentException("Method signature cannot be null!");
		}
		if (name.startsWith("<")) {
			throw new IllegalArgumentException("Don't use MethodEntry for a constructor!");
		}
		
		m_classEntry = classEntry;
		m_name = name;
		m_signature = signature;
	}
	
	public MethodEntry(MethodEntry other) {
		m_classEntry = new ClassEntry(other.m_classEntry);
		m_name = other.m_name;
		m_signature = other.m_signature;
	}
	
	public MethodEntry(MethodEntry other, String newClassName) {
		m_classEntry = new ClassEntry(newClassName);
		m_name = other.m_name;
		m_signature = other.m_signature;
	}
	
	@Override
	public ClassEntry getClassEntry() {
		return m_classEntry;
	}
	
	@Override
	public String getName() {
		return m_name;
	}
	
	@Override
	public Signature getSignature() {
		return m_signature;
	}
	
	@Override
	public String getClassName() {
		return m_classEntry.getName();
	}
	
	@Override
	public MethodEntry cloneToNewClass(ClassEntry classEntry) {
		return new MethodEntry(this, classEntry.getName());
	}
	
	@Override
	public int hashCode() {
		return Util.combineHashesOrdered(m_classEntry, m_name, m_signature);
	}
	
	@Override
	public boolean equals(Object other) {
		if (other instanceof MethodEntry) {
			return equals((MethodEntry)other);
		}
		return false;
	}
	
	public boolean equals(MethodEntry other) {
		return m_classEntry.equals(other.m_classEntry)
			&& m_name.equals(other.m_name)
			&& m_signature.equals(other.m_signature);
	}
	
	@Override
	public String toString() {
		return m_classEntry.getName() + "." + m_name + m_signature;
	}
} public enum TranslationDirection {
	
	Deobfuscating {
		@Override
		public <T> T choose(T deobfChoice, T obfChoice) {
			return deobfChoice;
		}
	},
	Obfuscating {
		@Override
		public <T> T choose(T deobfChoice, T obfChoice) {
			return obfChoice;
		}
	};
	
	public abstract <T> T choose(T deobfChoice, T obfChoice);
}
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.jar.JarFile;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.strobel.decompiler.languages.java.ast.CompilationUnit;
import cuchaz.enigma.Deobfuscator;
import cuchaz.enigma.Deobfuscator.ProgressListener;
import cuchaz.enigma.analysis.BehaviorReferenceTreeNode;
import cuchaz.enigma.analysis.ClassImplementationsTreeNode;
import cuchaz.enigma.analysis.ClassInheritanceTreeNode;
import cuchaz.enigma.analysis.EntryReference;
import cuchaz.enigma.analysis.FieldReferenceTreeNode;
import cuchaz.enigma.analysis.MethodImplementationsTreeNode;
import cuchaz.enigma.analysis.MethodInheritanceTreeNode;
import cuchaz.enigma.analysis.SourceIndex;
import cuchaz.enigma.analysis.Token;
import cuchaz.enigma.gui.ProgressDialog.ProgressRunnable;
import cuchaz.enigma.mapping.BehaviorEntry;
import cuchaz.enigma.mapping.ClassEntry;
import cuchaz.enigma.mapping.Entry;
import cuchaz.enigma.mapping.FieldEntry;
import cuchaz.enigma.mapping.MappingParseException;
import cuchaz.enigma.mapping.MappingsReader;
import cuchaz.enigma.mapping.MappingsWriter;
import cuchaz.enigma.mapping.MethodEntry;
import cuchaz.enigma.mapping.TranslationDirection;
/*******************************************************************************
 * Copyright (c) 2015 Jeff Martin.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public
 * License v3.0 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 * 
 * Contributors:
 *     Jeff Martin - initial API and implementation
 ******************************************************************************/
package cuchaz.enigma.gui;




public class GuiController {
	
	private Deobfuscator m_deobfuscator;
	private Gui m_gui;
	private SourceIndex m_index;
	private ClassEntry m_currentObfClass;
	private boolean m_isDirty;
	private Deque<EntryReference<Entry,Entry>> m_referenceStack;

	private Deque<EntryReference<Entry,Entry>> m_referenceNextStack;
	
	public GuiController(Gui gui) {
		m_gui = gui;
		m_deobfuscator = null;
		m_index = null;
		m_currentObfClass = null;
		m_isDirty = false;
		m_referenceStack = Queues.newArrayDeque();
		m_referenceNextStack = Queues.newArrayDeque();
	}
	
	public boolean isDirty() {
		return m_isDirty;
	}
	
	public void openJar(final JarFile jar) throws IOException {
		m_gui.onStartOpenJar();
		m_deobfuscator = new Deobfuscator(jar);
		m_gui.onFinishOpenJar(m_deobfuscator.getJarName());
		refreshClasses();
	}

	public void markWords(){
		m_deobfuscator.markWordAsDeObfuscate();
		refreshClasses();
	}
	
	public void closeJar() {
		m_deobfuscator = null;
		m_gui.onCloseJar();
	}
	
	public void openMappings(File file) throws IOException, MappingParseException {
		FileReader in = new FileReader(file);
		m_deobfuscator.setMappings(new MappingsReader().read(in));