public class MatchesWriter {
	
	public static void writeClasses(ClassMatches matches, File file)
	throws IOException {
		try (FileWriter out = new FileWriter(file)) {
			for (ClassMatch match : matches) {
				writeClassMatch(out, match);
			}
		}
	}

	private static void writeClassMatch(FileWriter out, ClassMatch match)
	throws IOException {
		writeClasses(out, match.sourceClasses);
		out.write(":");
		writeClasses(out, match.destClasses);
		out.write("\n");
	}
	
	private static void writeClasses(FileWriter out, Iterable<ClassEntry> classes)
	throws IOException {
		boolean isFirst = true;
		for (ClassEntry entry : classes) {
			if (isFirst) {
				isFirst = false;
			} else {
				out.write(",");
			}
			out.write(entry.toString());
		}
	}

	public static <T extends Entry> void writeMembers(MemberMatches<T> matches, File file)
	throws IOException {
		try (FileWriter out = new FileWriter(file)) {
			for (Map.Entry<T,T> match : matches.matches().entrySet()) {
				writeMemberMatch(out, match.getKey(), match.getValue());
			}
			for (T entry : matches.getUnmatchedSourceEntries()) {
				writeMemberMatch(out, entry, null);
			}
			for (T entry : matches.getUnmatchedDestEntries()) {
				writeMemberMatch(out, null, entry);
			}
			for (T entry : matches.getUnmatchableSourceEntries()) {
				writeUnmatchableEntry(out, entry);
			}
		}
	}

	private static <T extends Entry> void writeMemberMatch(FileWriter out, T source, T dest)
	throws IOException {
		if (source != null) {
			writeEntry(out, source);
		}
		out.write(":");
		if (dest != null) {
			writeEntry(out, dest);
		}
		out.write("\n");
	}
	
	private static <T extends Entry> void writeUnmatchableEntry(FileWriter out, T entry)
	throws IOException {
		out.write("!");
		writeEntry(out, entry);
		out.write("\n");
	}
	
	private static <T extends Entry> void writeEntry(FileWriter out, T entry)
	throws IOException {
		if (entry instanceof FieldEntry) {
			writeField(out, (FieldEntry)entry);
		} else if (entry instanceof BehaviorEntry) {
			writeBehavior(out, (BehaviorEntry)entry);
		}
	}
	
	private static void writeField(FileWriter out, FieldEntry fieldEntry)
	throws IOException {
		out.write(fieldEntry.getClassName());
		out.write(" ");
		out.write(fieldEntry.getName());
		out.write(" ");
		out.write(fieldEntry.getType().toString());
	}
	
	private static void writeBehavior(FileWriter out, BehaviorEntry behaviorEntry)
	throws IOException {
		out.write(behaviorEntry.getClassName());
		out.write(" ");
		out.write(behaviorEntry.getName());
		out.write(" ");
		if (behaviorEntry.getSignature() != null) {
			out.write(behaviorEntry.getSignature().toString());
		}
	}
} public class MemberMatchingGui<T extends Entry> {
	
	private static enum SourceType {
		Matched {
			
			@Override
			public <T extends Entry> Collection<ClassEntry> getObfSourceClasses(MemberMatches<T> matches) {
				return matches.getSourceClassesWithoutUnmatchedEntries();
			}
		},
		Unmatched {
			
			@Override
			public <T extends Entry> Collection<ClassEntry> getObfSourceClasses(MemberMatches<T> matches) {
				return matches.getSourceClassesWithUnmatchedEntries();
			}
		};
		
		public JRadioButton newRadio(ActionListener listener, ButtonGroup group) {
			JRadioButton button = new JRadioButton(name(), this == getDefault());
			button.setActionCommand(name());
			button.addActionListener(listener);
			group.add(button);
			return button;
		}
		
		public abstract <T extends Entry> Collection<ClassEntry> getObfSourceClasses(MemberMatches<T> matches);
		
		public static SourceType getDefault() {
			return values()[0];
		}
	}
	
	public static interface SaveListener<T extends Entry> {
		public void save(MemberMatches<T> matches);
	}
	
	// controls
	private JFrame m_frame;
	private Map<SourceType,JRadioButton> m_sourceTypeButtons;
	private ClassSelector m_sourceClasses;
	private CodeReader m_sourceReader;
	private CodeReader m_destReader;
	private JButton m_matchButton;
	private JButton m_unmatchableButton;
	private JLabel m_sourceLabel;
	private JLabel m_destLabel;
	private HighlightPainter m_unmatchedHighlightPainter;
	private HighlightPainter m_matchedHighlightPainter;

	private ClassMatches m_classMatches;
	private MemberMatches<T> m_memberMatches;
	private Deobfuscator m_sourceDeobfuscator;
	private Deobfuscator m_destDeobfuscator;
	private SaveListener<T> m_saveListener;
	private SourceType m_sourceType;
	private ClassEntry m_obfSourceClass;
	private ClassEntry m_obfDestClass;
	private T m_obfSourceEntry;
	private T m_obfDestEntry;

	public MemberMatchingGui(ClassMatches classMatches, MemberMatches<T> fieldMatches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) {
		
		m_classMatches = classMatches;
		m_memberMatches = fieldMatches;
		m_sourceDeobfuscator = sourceDeobfuscator;
		m_destDeobfuscator = destDeobfuscator;
		
		// init frame
		m_frame = new JFrame(Constants.Name + " - Member Matcher");
		final Container pane = m_frame.getContentPane();
		pane.setLayout(new BorderLayout());
		
		// init classes side
		JPanel classesPanel = new JPanel();
		classesPanel.setLayout(new BoxLayout(classesPanel, BoxLayout.PAGE_AXIS));
		classesPanel.setPreferredSize(new Dimension(200, 0));
		pane.add(classesPanel, BorderLayout.WEST);
		classesPanel.add(new JLabel("Classes"));
		
		// init source type radios
		JPanel sourceTypePanel = new JPanel();
		classesPanel.add(sourceTypePanel);
		sourceTypePanel.setLayout(new BoxLayout(sourceTypePanel, BoxLayout.PAGE_AXIS));
		ActionListener sourceTypeListener = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				setSourceType(SourceType.valueOf(event.getActionCommand()));
			}
		};
		ButtonGroup sourceTypeButtons = new ButtonGroup();
		m_sourceTypeButtons = Maps.newHashMap();
		for (SourceType sourceType : SourceType.values()) {
			JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons);
			m_sourceTypeButtons.put(sourceType, button);
			sourceTypePanel.add(button);
		}
		
		m_sourceClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator);
		m_sourceClasses.setListener(new ClassSelectionListener() {
			@Override
			public void onSelectClass(ClassEntry classEntry) {
				setSourceClass(classEntry);
			}
		});
		JScrollPane sourceScroller = new JScrollPane(m_sourceClasses);
		classesPanel.add(sourceScroller);

		// init readers
		DefaultSyntaxKit.initKit();
		m_sourceReader = new CodeReader();
		m_sourceReader.setSelectionListener(new CodeReader.SelectionListener() {
			@Override
			public void onSelect(EntryReference<Entry,Entry> reference) {
				if (reference != null) {
					onSelectSource(reference.entry);
				} else {
					onSelectSource(null);
				}
			}
		});
		m_destReader = new CodeReader();
		m_destReader.setSelectionListener(new CodeReader.SelectionListener() {
			@Override
			public void onSelect(EntryReference<Entry,Entry> reference) {
				if (reference != null) {
					onSelectDest(reference.entry);
				} else {
					onSelectDest(null);
				}
			}
		});
		
		// add key bindings
		KeyAdapter keyListener = new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent event) {
				switch (event.getKeyCode()) {
					case KeyEvent.VK_M:
						m_matchButton.doClick();
					break;
				}
			}
		};
		m_sourceReader.addKeyListener(keyListener);
		m_destReader.addKeyListener(keyListener);

		// init all the splits
		JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(m_sourceReader), new JScrollPane(m_destReader));
		splitRight.setResizeWeight(0.5); // resize 50:50
		JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, classesPanel, splitRight);
		splitLeft.setResizeWeight(0); // let the right side take all the slack
		pane.add(splitLeft, BorderLayout.CENTER);
		splitLeft.resetToPreferredSizes();
		
		// init bottom panel
		JPanel bottomPanel = new JPanel();
		bottomPanel.setLayout(new FlowLayout());
		pane.add(bottomPanel, BorderLayout.SOUTH);
		
		m_matchButton = new JButton();
		m_unmatchableButton = new JButton();
		
		m_sourceLabel = new JLabel();
		bottomPanel.add(m_sourceLabel);
		bottomPanel.add(m_matchButton);
		bottomPanel.add(m_unmatchableButton);
		m_destLabel = new JLabel();
		bottomPanel.add(m_destLabel);

		// show the frame
		pane.doLayout();
		m_frame.setSize(1024, 576);
		m_frame.setMinimumSize(new Dimension(640, 480));
		m_frame.setVisible(true);
		m_frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
		
		m_unmatchedHighlightPainter = new ObfuscatedHighlightPainter();
		m_matchedHighlightPainter = new DeobfuscatedHighlightPainter();

		// init state
		m_saveListener = null;
		m_obfSourceClass = null;
		m_obfDestClass = null;
		m_obfSourceEntry = null;
		m_obfDestEntry = null;
		setSourceType(SourceType.getDefault());
		updateButtons();
	}

	protected void setSourceType(SourceType val) {
		m_sourceType = val;
		updateSourceClasses();
	}

	public void setSaveListener(SaveListener<T> val) {
		m_saveListener = val;
	}
	
	private void updateSourceClasses() {
		
		String selectedPackage = m_sourceClasses.getSelectedPackage();
		
		List<ClassEntry> deobfClassEntries = Lists.newArrayList();
		for (ClassEntry entry : m_sourceType.getObfSourceClasses(m_memberMatches)) {
			deobfClassEntries.add(m_sourceDeobfuscator.deobfuscateEntry(entry));
		}
		m_sourceClasses.setClasses(deobfClassEntries);
		
		if (selectedPackage != null) {
			m_sourceClasses.expandPackage(selectedPackage);
		}
		
		for (SourceType sourceType : SourceType.values()) {
			m_sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)",
				sourceType.name(), sourceType.getObfSourceClasses(m_memberMatches).size()
			));
		}
	}
	
	protected void setSourceClass(ClassEntry sourceClass) {
		
		m_obfSourceClass = m_sourceDeobfuscator.obfuscateEntry(sourceClass);
		m_obfDestClass = m_classMatches.getUniqueMatches().get(m_obfSourceClass);
		if (m_obfDestClass == null) {
			throw new Error("No matching dest class for source class: " + m_obfSourceClass);
		}
		
		m_sourceReader.decompileClass(m_obfSourceClass, m_sourceDeobfuscator, false, new Runnable() {
			@Override
			public void run() {
				updateSourceHighlights();
			}
		});
		m_destReader.decompileClass(m_obfDestClass, m_destDeobfuscator, false, new Runnable() {
			@Override
			public void run() {
				updateDestHighlights();
			}
		});
	}
	
	protected void updateSourceHighlights() {
		highlightEntries(m_sourceReader, m_sourceDeobfuscator, m_memberMatches.matches().keySet(), m_memberMatches.getUnmatchedSourceEntries());
	}

	protected void updateDestHighlights() {
		highlightEntries(m_destReader, m_destDeobfuscator, m_memberMatches.matches().values(), m_memberMatches.getUnmatchedDestEntries());
	}
	
	private void highlightEntries(CodeReader reader, Deobfuscator deobfuscator, Collection<T> obfMatchedEntries, Collection<T> obfUnmatchedEntries) {
		reader.clearHighlights();
		SourceIndex index = reader.getSourceIndex();
		
		// matched fields
		for (T obfT : obfMatchedEntries) {
			T deobfT = deobfuscator.deobfuscateEntry(obfT);
			Token token = index.getDeclarationToken(deobfT);
			if (token != null) {
				reader.setHighlightedToken(token, m_matchedHighlightPainter);
			}
		}
		
		// unmatched fields
		for (T obfT : obfUnmatchedEntries) {
			T deobfT = deobfuscator.deobfuscateEntry(obfT);
			Token token = index.getDeclarationToken(deobfT);
			if (token != null) {
				reader.setHighlightedToken(token, m_unmatchedHighlightPainter);
			}
		}
	}
	
	private boolean isSelectionMatched() {
		return m_obfSourceEntry != null && m_obfDestEntry != null
			&& m_memberMatches.isMatched(m_obfSourceEntry, m_obfDestEntry);
	}
	
	protected void onSelectSource(Entry source) {

		// start with no selection
		if (isSelectionMatched()) {
			setDest(null);
		}
		setSource(null);
		
		// then look for a valid source selection
		if (source != null) {
			
			// this looks really scary, but it's actually ok
			// Deobfuscator.obfuscateEntry can handle all implementations of Entry
			// and MemberMatches.hasSource() will only pass entries that actually match T
			@SuppressWarnings("unchecked")
			T sourceEntry = (T)source;

			T obfSourceEntry = m_sourceDeobfuscator.obfuscateEntry(sourceEntry);
			if (m_memberMatches.hasSource(obfSourceEntry)) {
				setSource(obfSourceEntry);
				
				// look for a matched dest too
				T obfDestEntry = m_memberMatches.matches().get(obfSourceEntry);
				if (obfDestEntry != null) {
					setDest(obfDestEntry);
				}
			}
		}
		
		updateButtons();
	}

	protected void onSelectDest(Entry dest) {
		
		// start with no selection
		if (isSelectionMatched()) {
			setSource(null);
		}
		setDest(null);

		// then look for a valid dest selection
		if (dest != null) {
			
			// this looks really scary, but it's actually ok
			// Deobfuscator.obfuscateEntry can handle all implementations of Entry
			// and MemberMatches.hasSource() will only pass entries that actually match T
			@SuppressWarnings("unchecked")
			T destEntry = (T)dest;
			
			T obfDestEntry = m_destDeobfuscator.obfuscateEntry(destEntry);
			if (m_memberMatches.hasDest(obfDestEntry)) {
				setDest(obfDestEntry);

				// look for a matched source too
				T obfSourceEntry = m_memberMatches.matches().inverse().get(obfDestEntry);
				if (obfSourceEntry != null) {
					setSource(obfSourceEntry);
				}
			}
		}
		
		updateButtons();
	}
	
	private void setSource(T obfEntry) {
		if (obfEntry == null) {
			m_obfSourceEntry = obfEntry;
			m_sourceLabel.setText("");
		} else {
			m_obfSourceEntry = obfEntry;
			m_sourceLabel.setText(getEntryLabel(obfEntry, m_sourceDeobfuscator));
		}
	}
	
	private void setDest(T obfEntry) {
		if (obfEntry == null) {
			m_obfDestEntry = obfEntry;
			m_destLabel.setText("");
		} else {
			m_obfDestEntry = obfEntry;
			m_destLabel.setText(getEntryLabel(obfEntry, m_destDeobfuscator));
		}
	}

	private String getEntryLabel(T obfEntry, Deobfuscator deobfuscator) {
		// show obfuscated and deobfuscated names, but no types/signatures
		T deobfEntry = deobfuscator.deobfuscateEntry(obfEntry);
		return String.format("%s (%s)", deobfEntry.getName(), obfEntry.getName());
	}

	private void updateButtons() {
		
		GuiTricks.deactivateButton(m_matchButton);
		GuiTricks.deactivateButton(m_unmatchableButton);
		
		if (m_obfSourceEntry != null && m_obfDestEntry != null) {
			if (m_memberMatches.isMatched(m_obfSourceEntry, m_obfDestEntry)) {
				GuiTricks.activateButton(m_matchButton, "Unmatch", new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent event) {
						unmatch();
					}
				});
			} else if (!m_memberMatches.isMatchedSourceEntry(m_obfSourceEntry) && !m_memberMatches.isMatchedDestEntry(m_obfDestEntry)) {
				GuiTricks.activateButton(m_matchButton, "Match", new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent event) {
						match();
					}
				});
			}
		} else if (m_obfSourceEntry != null) {
			GuiTricks.activateButton(m_unmatchableButton, "Set Unmatchable", new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent event) {
					unmatchable();
				}
			});
		}
	}
	
	protected void match() {
		
		// update the field matches
		m_memberMatches.makeMatch(m_obfSourceEntry, m_obfDestEntry);
		save();

		// update the ui
		onSelectSource(null);
		onSelectDest(null);
		updateSourceHighlights();
		updateDestHighlights();
		updateSourceClasses();
	}
	
	protected void unmatch() {
		
		// update the field matches
		m_memberMatches.unmakeMatch(m_obfSourceEntry, m_obfDestEntry);
		save();

		// update the ui
		onSelectSource(null);
		onSelectDest(null);
		updateSourceHighlights();
		updateDestHighlights();
		updateSourceClasses();
	}
	
	protected void unmatchable() {
		
		// update the field matches
		m_memberMatches.makeSourceUnmatchable(m_obfSourceEntry);
		save();
		
		// update the ui
		onSelectSource(null);
		onSelectDest(null);
		updateSourceHighlights();
		updateDestHighlights();
		updateSourceClasses();
	}

	private void save() {
		if (m_saveListener != null) {
			m_saveListener.save(m_memberMatches);
		}
	}
} 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 ClassMapping implements Serializable, Comparable<ClassMapping> {
	
	private static final long serialVersionUID = -5148491146902340107L;
	
	private String m_obfFullName;
	private String m_obfSimpleName;
	private String m_deobfName;
	private Map<String,ClassMapping> m_innerClassesByObfSimple;
	private Map<String,ClassMapping> m_innerClassesByDeobf;
	private Map<String,FieldMapping> m_fieldsByObf;
	private Map<String,FieldMapping> m_fieldsByDeobf;
	private Map<String,MethodMapping> m_methodsByObf;
	private Map<String,MethodMapping> m_methodsByDeobf;

	public boolean isMarkedAsDeObfs() {
		return markedAsDeObfs;
	}

	public void setMarkedAsDeObfs(boolean markedAsDeObfs) {
		this.markedAsDeObfs = markedAsDeObfs;
	}

	private boolean markedAsDeObfs;

	public ClassMapping(String obfFullName) {
		this(obfFullName, null);
	}
	
	public ClassMapping(String obfFullName, String deobfName) {
		m_obfFullName = obfFullName;
		ClassEntry classEntry = new ClassEntry(obfFullName);
		m_obfSimpleName = classEntry.isInnerClass() ? classEntry.getInnermostClassName() : classEntry.getSimpleName();
		m_deobfName = NameValidator.validateClassName(deobfName, false);
		m_innerClassesByObfSimple = Maps.newHashMap();
		m_innerClassesByDeobf = Maps.newHashMap();
		m_fieldsByObf = Maps.newHashMap();
		m_fieldsByDeobf = Maps.newHashMap();
		m_methodsByObf = Maps.newHashMap();
		m_methodsByDeobf = Maps.newHashMap();
	}
	
	public String getObfFullName() {
		return m_obfFullName;
	}
	
	public String getObfSimpleName() {
		return m_obfSimpleName;
	}
	
	public String getDeobfName() {
		return m_deobfName;
	}
	
	public void setDeobfName(String val) {
		m_deobfName = NameValidator.validateClassName(val, false);
	}
	
	//// INNER CLASSES ////////
	
	public Iterable<ClassMapping> innerClasses() {
		assert (m_innerClassesByObfSimple.size() >= m_innerClassesByDeobf.size());
		return m_innerClassesByObfSimple.values();
	}
	
	public void addInnerClassMapping(ClassMapping classMapping) {
		boolean obfWasAdded = m_innerClassesByObfSimple.put(classMapping.getObfSimpleName(), classMapping) == null;
		assert (obfWasAdded);
		if (classMapping.getDeobfName() != null) {
			assert (isSimpleClassName(classMapping.getDeobfName()));
			boolean deobfWasAdded = m_innerClassesByDeobf.put(classMapping.getDeobfName(), classMapping) == null;
			assert (deobfWasAdded);
		}
	}
	
	public void removeInnerClassMapping(ClassMapping classMapping) {
		boolean obfWasRemoved = m_innerClassesByObfSimple.remove(classMapping.getObfSimpleName()) != null;
		assert (obfWasRemoved);
		if (classMapping.getDeobfName() != null) {
			boolean deobfWasRemoved = m_innerClassesByDeobf.remove(classMapping.getDeobfName()) != null;
			assert (deobfWasRemoved);
		}
	}
	
	public ClassMapping getOrCreateInnerClass(ClassEntry obfInnerClass) {
		ClassMapping classMapping = m_innerClassesByObfSimple.get(obfInnerClass.getInnermostClassName());
		if (classMapping == null) {
			classMapping = new ClassMapping(obfInnerClass.getName());
			boolean wasAdded = m_innerClassesByObfSimple.put(classMapping.getObfSimpleName(), classMapping) == null;
			assert (wasAdded);
		}
		return classMapping;
	}
	
	public ClassMapping getInnerClassByObfSimple(String obfSimpleName) {
		assert (isSimpleClassName(obfSimpleName));
		return m_innerClassesByObfSimple.get(obfSimpleName);
	}
	
	public ClassMapping getInnerClassByDeobf(String deobfName) {
		assert (isSimpleClassName(deobfName));
		return m_innerClassesByDeobf.get(deobfName);
	}
	
	public ClassMapping getInnerClassByDeobfThenObfSimple(String name) {
		ClassMapping classMapping = getInnerClassByDeobf(name);
		if (classMapping == null) {
			classMapping = getInnerClassByObfSimple(name);
		}
		return classMapping;
	}
	
	public String getDeobfInnerClassName(String obfSimpleName) {
		assert (isSimpleClassName(obfSimpleName));
		ClassMapping classMapping = m_innerClassesByObfSimple.get(obfSimpleName);
		if (classMapping != null) {
			return classMapping.getDeobfName();
		}
		return null;
	}
	
	public void setInnerClassName(ClassEntry obfInnerClass, String deobfName) {
		ClassMapping classMapping = getOrCreateInnerClass(obfInnerClass);
		if (classMapping.getDeobfName() != null) {
			boolean wasRemoved = m_innerClassesByDeobf.remove(classMapping.getDeobfName()) != null;
			assert (wasRemoved);
		}
		classMapping.setDeobfName(deobfName);
		if (deobfName != null) {
			assert (isSimpleClassName(deobfName));
			boolean wasAdded = m_innerClassesByDeobf.put(deobfName, classMapping) == null;
			assert (wasAdded);
		}
	}
	
	public boolean hasInnerClassByObfSimple(String obfSimpleName) {
		return m_innerClassesByObfSimple.containsKey(obfSimpleName);
	}
	
	public boolean hasInnerClassByDeobf(String deobfName) {
		return m_innerClassesByDeobf.containsKey(deobfName);
	}
	
	
	//// FIELDS ////////
	
	public Iterable<FieldMapping> fields() {
		assert (m_fieldsByObf.size() == m_fieldsByDeobf.size());
		return m_fieldsByObf.values();
	}
	
	public boolean containsObfField(String obfName, Type obfType) {
		return m_fieldsByObf.containsKey(getFieldKey(obfName, obfType));
	}
	
	public boolean containsDeobfField(String deobfName, Type deobfType) {
		return m_fieldsByDeobf.containsKey(getFieldKey(deobfName, deobfType));
	}
	
	public void addFieldMapping(FieldMapping fieldMapping) {
		String obfKey = getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType());
		if (m_fieldsByObf.containsKey(obfKey)) {
			throw new Error("Already have mapping for " + m_obfFullName + "." + obfKey);
		}
		String deobfKey = getFieldKey(fieldMapping.getDeobfName(), fieldMapping.getObfType());
		if (m_fieldsByDeobf.containsKey(deobfKey)) {
			throw new Error("Already have mapping for " + m_deobfName + "." + deobfKey);
		}
		boolean obfWasAdded = m_fieldsByObf.put(obfKey, fieldMapping) == null;
		assert (obfWasAdded);
		boolean deobfWasAdded = m_fieldsByDeobf.put(deobfKey, fieldMapping) == null;
		assert (deobfWasAdded);
		assert (m_fieldsByObf.size() == m_fieldsByDeobf.size());
	}
	
	public void removeFieldMapping(FieldMapping fieldMapping) {
		boolean obfWasRemoved = m_fieldsByObf.remove(getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType())) != null;
		assert (obfWasRemoved);
		if (fieldMapping.getDeobfName() != null) {
			boolean deobfWasRemoved = m_fieldsByDeobf.remove(getFieldKey(fieldMapping.getDeobfName(), fieldMapping.getObfType())) != null;
			assert (deobfWasRemoved);
		}
	}
	
	public FieldMapping getFieldByObf(String obfName, Type obfType) {
		return m_fieldsByObf.get(getFieldKey(obfName, obfType));
	}
	
	public FieldMapping getFieldByDeobf(String deobfName, Type obfType) {
		return m_fieldsByDeobf.get(getFieldKey(deobfName, obfType));
	}
	
	public String getObfFieldName(String deobfName, Type obfType) {
		FieldMapping fieldMapping = m_fieldsByDeobf.get(getFieldKey(deobfName, obfType));
		if (fieldMapping != null) {
			return fieldMapping.getObfName();
		}
		return null;
	}
	
	public String getDeobfFieldName(String obfName, Type obfType) {
		FieldMapping fieldMapping = m_fieldsByObf.get(getFieldKey(obfName, obfType));
		if (fieldMapping != null) {
			return fieldMapping.getDeobfName();
		}
		return null;
	}
	
	private String getFieldKey(String name, Type type) {
		if (name == null) {
			throw new IllegalArgumentException("name cannot be null!");
		}
		if (type == null) {
			throw new IllegalArgumentException("type cannot be null!");
		}
		return name + ":" + type;
	}
	
	
	public void setFieldName(String obfName, Type obfType, String deobfName) {
		assert(deobfName != null);
		FieldMapping fieldMapping = m_fieldsByObf.get(getFieldKey(obfName, obfType));
		if (fieldMapping == null) {
			fieldMapping = new FieldMapping(obfName, obfType, deobfName);
			boolean obfWasAdded = m_fieldsByObf.put(getFieldKey(obfName, obfType), fieldMapping) == null;
			assert (obfWasAdded);
		} else {
			boolean wasRemoved = m_fieldsByDeobf.remove(getFieldKey(fieldMapping.getDeobfName(), obfType)) != null;
			assert (wasRemoved);
		}
		fieldMapping.setDeobfName(deobfName);
		if (deobfName != null) {
			boolean wasAdded = m_fieldsByDeobf.put(getFieldKey(deobfName, obfType), fieldMapping) == null;
			assert (wasAdded);
		}
	}
	
	public void setFieldObfNameAndType(String oldObfName, Type obfType, String newObfName, Type newObfType) {
		assert(newObfName != null);
		FieldMapping fieldMapping = m_fieldsByObf.remove(getFieldKey(oldObfName, obfType));
		assert(fieldMapping != null);
		fieldMapping.setObfName(newObfName);
		fieldMapping.setObfType(newObfType);
		boolean obfWasAdded = m_fieldsByObf.put(getFieldKey(newObfName, newObfType), fieldMapping) == null;
		assert(obfWasAdded);
	}
	
	
	//// METHODS ////////
	
	public Iterable<MethodMapping> methods() {
		assert (m_methodsByObf.size() >= m_methodsByDeobf.size());
		return m_methodsByObf.values();
	}
	
	public boolean containsObfMethod(String obfName, Signature obfSignature) {
		return m_methodsByObf.containsKey(getMethodKey(obfName, obfSignature));
	}
	
	public boolean containsDeobfMethod(String deobfName, Signature obfSignature) {
		return m_methodsByDeobf.containsKey(getMethodKey(deobfName, obfSignature));
	}
	
	public void addMethodMapping(MethodMapping methodMapping) {
		String obfKey = getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature());
		if (m_methodsByObf.containsKey(obfKey)) {
			throw new Error("Already have mapping for " + m_obfFullName + "." + obfKey);
		}
		boolean wasAdded = m_methodsByObf.put(obfKey, methodMapping) == null;
		assert (wasAdded);
		if (methodMapping.getDeobfName() != null) {
			String deobfKey = getMethodKey(methodMapping.getDeobfName(), methodMapping.getObfSignature());
			if (m_methodsByDeobf.containsKey(deobfKey)) {
				throw new Error("Already have mapping for " + m_deobfName + "." + deobfKey);
			}
			boolean deobfWasAdded = m_methodsByDeobf.put(deobfKey, methodMapping) == null;
			assert (deobfWasAdded);
		}
		assert (m_methodsByObf.size() >= m_methodsByDeobf.size());
	}
	
	public void removeMethodMapping(MethodMapping methodMapping) {
		boolean obfWasRemoved = m_methodsByObf.remove(getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature())) != null;
		assert (obfWasRemoved);
		if (methodMapping.getDeobfName() != null) {
			boolean deobfWasRemoved = m_methodsByDeobf.remove(getMethodKey(methodMapping.getDeobfName(), methodMapping.getObfSignature())) != null;
			assert (deobfWasRemoved);
		}
	}
	
	public MethodMapping getMethodByObf(String obfName, Signature obfSignature) {
		return m_methodsByObf.get(getMethodKey(obfName, obfSignature));
	}
	
	public MethodMapping getMethodByDeobf(String deobfName, Signature obfSignature) {
		return m_methodsByDeobf.get(getMethodKey(deobfName, obfSignature));
	}
	
	private String getMethodKey(String name, Signature signature) {
		if (name == null) {
			throw new IllegalArgumentException("name cannot be null!");
		}
		if (signature == null) {
			throw new IllegalArgumentException("signature cannot be null!");
		}
		return name + signature;
	}
	
	public void setMethodName(String obfName, Signature obfSignature, String deobfName) {
		MethodMapping methodMapping = m_methodsByObf.get(getMethodKey(obfName, obfSignature));
		if (methodMapping == null) {
			methodMapping = createMethodMapping(obfName, obfSignature);
		} else if (methodMapping.getDeobfName() != null) {
			boolean wasRemoved = m_methodsByDeobf.remove(getMethodKey(methodMapping.getDeobfName(), methodMapping.getObfSignature())) != null;
			assert (wasRemoved);
		}
		methodMapping.setDeobfName(deobfName);
		if (deobfName != null) {
			boolean wasAdded = m_methodsByDeobf.put(getMethodKey(deobfName, obfSignature), methodMapping) == null;
			assert (wasAdded);
		}
	}
	
	public void setMethodObfNameAndSignature(String oldObfName, Signature obfSignature, String newObfName, Signature newObfSignature) {
		assert(newObfName != null);
		MethodMapping methodMapping = m_methodsByObf.remove(getMethodKey(oldObfName, obfSignature));
		assert(methodMapping != null);
		methodMapping.setObfName(newObfName);
		methodMapping.setObfSignature(newObfSignature);
		boolean obfWasAdded = m_methodsByObf.put(getMethodKey(newObfName, newObfSignature), methodMapping) == null;
		assert(obfWasAdded);
	}
	
	//// ARGUMENTS ////////
	
	public void setArgumentName(String obfMethodName, Signature obfMethodSignature, int argumentIndex, String argumentName) {
		assert(argumentName != null);
		MethodMapping methodMapping = m_methodsByObf.get(getMethodKey(obfMethodName, obfMethodSignature));
		if (methodMapping == null) {
			methodMapping = createMethodMapping(obfMethodName, obfMethodSignature);
		}
		methodMapping.setArgumentName(argumentIndex, argumentName);
	}
	
	public void removeArgumentName(String obfMethodName, Signature obfMethodSignature, int argumentIndex) {
		m_methodsByObf.get(getMethodKey(obfMethodName, obfMethodSignature)).removeArgumentName(argumentIndex);
	}
	
	private MethodMapping createMethodMapping(String obfName, Signature obfSignature) {
		MethodMapping methodMapping = new MethodMapping(obfName, obfSignature);
		boolean wasAdded = m_methodsByObf.put(getMethodKey(obfName, obfSignature), methodMapping) == null;
		assert (wasAdded);
		return methodMapping;
	}
	
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append(m_obfFullName);
		buf.append(" <-> ");
		buf.append(m_deobfName);
		buf.append("\n");
		buf.append("Fields:\n");
		for (FieldMapping fieldMapping : fields()) {
			buf.append("\t");
			buf.append(fieldMapping.getObfName());
			buf.append(" <-> ");
			buf.append(fieldMapping.getDeobfName());
			buf.append("\n");
		}
		buf.append("Methods:\n");
		for (MethodMapping methodMapping : m_methodsByObf.values()) {
			buf.append(methodMapping.toString());
			buf.append("\n");
		}
		buf.append("Inner Classes:\n");
		for (ClassMapping classMapping : m_innerClassesByObfSimple.values()) {
			buf.append("\t");
			buf.append(classMapping.getObfSimpleName());
			buf.append(" <-> ");
			buf.append(classMapping.getDeobfName());
			buf.append("\n");
		}
		return buf.toString();
	}
	
	@Override
	public int compareTo(ClassMapping other) {
		// sort by a, b, c, ... aa, ab, etc
		if (m_obfFullName.length() != other.m_obfFullName.length()) {
			return m_obfFullName.length() - other.m_obfFullName.length();
		}
		return m_obfFullName.compareTo(other.m_obfFullName);
	}
	
	public boolean renameObfClass(String oldObfClassName, String newObfClassName) {
		
		// rename inner classes
		for (ClassMapping innerClassMapping : new ArrayList<ClassMapping>(m_innerClassesByObfSimple.values())) {
			if (innerClassMapping.renameObfClass(oldObfClassName, newObfClassName)) {
				boolean wasRemoved = m_innerClassesByObfSimple.remove(oldObfClassName) != null;
				assert (wasRemoved);
				boolean wasAdded = m_innerClassesByObfSimple.put(newObfClassName, innerClassMapping) == null;
				assert (wasAdded);
			}
		}
		
		// rename field types
		for (FieldMapping fieldMapping : new ArrayList<FieldMapping>(m_fieldsByObf.values())) {
			String oldFieldKey = getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType());
			if (fieldMapping.renameObfClass(oldObfClassName, newObfClassName)) {
				boolean wasRemoved = m_fieldsByObf.remove(oldFieldKey) != null;
				assert (wasRemoved);
				boolean wasAdded = m_fieldsByObf.put(getFieldKey(fieldMapping.getObfName(), fieldMapping.getObfType()), fieldMapping) == null;
				assert (wasAdded);
			}
		}
		
		// rename method signatures
		for (MethodMapping methodMapping : new ArrayList<MethodMapping>(m_methodsByObf.values())) {
			String oldMethodKey = getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature());
			if (methodMapping.renameObfClass(oldObfClassName, newObfClassName)) {
				boolean wasRemoved = m_methodsByObf.remove(oldMethodKey) != null;
				assert (wasRemoved);
				boolean wasAdded = m_methodsByObf.put(getMethodKey(methodMapping.getObfName(), methodMapping.getObfSignature()), methodMapping) == null;
				assert (wasAdded);
			}
		}
		
		if (m_obfFullName.equals(oldObfClassName)) {
			// rename this class
			m_obfFullName = newObfClassName;
			return true;
		}
		return false;
	}
	
	public boolean containsArgument(BehaviorEntry obfBehaviorEntry, String name) {
		MethodMapping methodMapping = m_methodsByObf.get(getMethodKey(obfBehaviorEntry.getName(), obfBehaviorEntry.getSignature()));
		if (methodMapping != null) {
			return methodMapping.containsArgument(name);
		}
		return false;
	}
	
	public static boolean isSimpleClassName(String name) {
		return name.indexOf('/') < 0 && name.indexOf('$') < 0;
	}

	public ClassEntry getObfEntry() {
		return new ClassEntry(m_obfFullName);
	}
} 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 FieldMapping implements Serializable, Comparable<FieldMapping>, MemberMapping<FieldEntry> {
	
	private static final long serialVersionUID = 8610742471440861315L;
	
	private String m_obfName;
	private String m_deobfName;
	private Type m_obfType;
	
	public FieldMapping(String obfName, Type obfType, String deobfName) {
		m_obfName = obfName;
		m_deobfName = NameValidator.validateFieldName(deobfName);
		m_obfType = obfType;
	}
	
	public FieldMapping(FieldMapping other, ClassNameReplacer obfClassNameReplacer) {
		m_obfName = other.m_obfName;
		m_deobfName = other.m_deobfName;
		m_obfType = new Type(other.m_obfType, obfClassNameReplacer);
	}

	@Override
	public String getObfName() {
		return m_obfName;
	}
	
	public void setObfName(String val) {
		m_obfName = NameValidator.validateFieldName(val);
	}
	
	public String getDeobfName() {
		return m_deobfName;
	}
	
	public void setDeobfName(String val) {
		m_deobfName = NameValidator.validateFieldName(val);
	}
	
	public Type getObfType() {
		return m_obfType;
	}
	
	public void setObfType(Type val) {
		m_obfType = val;
	}
	
	@Override
	public int compareTo(FieldMapping other) {
		return (m_obfName + m_obfType).compareTo(other.m_obfName + other.m_obfType);
	}

	public boolean renameObfClass(final String oldObfClassName, final String newObfClassName) {
		
		// rename obf classes in the type
		Type newType = new Type(m_obfType, new ClassNameReplacer() {
			@Override
			public String replace(String className) {
				if (className.equals(oldObfClassName)) {
					return newObfClassName;
				}
				return null;
			}
		});
		
		if (!newType.equals(m_obfType)) {
			m_obfType = newType;
			return true;
		}
		return false;
	}

	@Override
	public FieldEntry getObfEntry(ClassEntry classEntry) {
		return new FieldEntry(classEntry, m_obfName, new Type(m_obfType));
	}
} public class Mappings implements Serializable {
	
	private static final long serialVersionUID = 4649790259460259026L;
	
	protected Map<String,ClassMapping> m_classesByObf;
	protected Map<String,ClassMapping> m_classesByDeobf;
	
	public Mappings() {
		m_classesByObf = Maps.newHashMap();
		m_classesByDeobf = Maps.newHashMap();
	}
	
	public Mappings(Iterable<ClassMapping> classes) {
		this();
		
		for (ClassMapping classMapping : classes) {
			m_classesByObf.put(classMapping.getObfFullName(), classMapping);
			if (classMapping.getDeobfName() != null) {
				m_classesByDeobf.put(classMapping.getDeobfName(), classMapping);
			}
		}
	}
	
	public Collection<ClassMapping> classes() {
		assert (m_classesByObf.size() >= m_classesByDeobf.size());
		return m_classesByObf.values();
	}
	
	public void addClassMapping(ClassMapping classMapping) {
		if (m_classesByObf.containsKey(classMapping.getObfFullName())) {
			throw new Error("Already have mapping for " + classMapping.getObfFullName());
		}
		boolean obfWasAdded = m_classesByObf.put(classMapping.getObfFullName(), classMapping) == null;
		assert (obfWasAdded);
		if (classMapping.getDeobfName() != null) {
			if (m_classesByDeobf.containsKey(classMapping.getDeobfName())) {
				throw new Error("Already have mapping for " + classMapping.getDeobfName());
			}
			boolean deobfWasAdded = m_classesByDeobf.put(classMapping.getDeobfName(), classMapping) == null;
			assert (deobfWasAdded);
		}
	}
	
	public void removeClassMapping(ClassMapping classMapping) {
		boolean obfWasRemoved = m_classesByObf.remove(classMapping.getObfFullName()) != null;
		assert (obfWasRemoved);
		if (classMapping.getDeobfName() != null) {
			boolean deobfWasRemoved = m_classesByDeobf.remove(classMapping.getDeobfName()) != null;
			assert (deobfWasRemoved);
		}
	}
	
	public ClassMapping getClassByObf(ClassEntry entry) {
		return getClassByObf(entry.getName());
	}
	
	public ClassMapping getClassByObf(String obfName) {
		return m_classesByObf.get(obfName);
	}
	
	public ClassMapping getClassByDeobf(ClassEntry entry) {
		return getClassByDeobf(entry.getName());
	}
	
	public ClassMapping getClassByDeobf(String deobfName) {
		return m_classesByDeobf.get(deobfName);
	}
	
	public void setClassDeobfName(ClassMapping classMapping, String deobfName) {
		if (classMapping.getDeobfName() != null) {
			boolean wasRemoved = m_classesByDeobf.remove(classMapping.getDeobfName()) != null;
			assert (wasRemoved);
		}
		classMapping.setDeobfName(deobfName);
		if (deobfName != null) {
			boolean wasAdded = m_classesByDeobf.put(deobfName, classMapping) == null;
			assert (wasAdded);
		}
	}
	
	public Translator getTranslator(TranslationDirection direction, TranslationIndex index) {
		switch (direction) {
			case Deobfuscating:
				
				return new Translator(direction, m_classesByObf, index);
				
			case Obfuscating:
				
				// fill in the missing deobf class entries with obf entries
				Map<String,ClassMapping> classes = Maps.newHashMap();
				for (ClassMapping classMapping : classes()) {
					if (classMapping.getDeobfName() != null) {
						classes.put(classMapping.getDeobfName(), classMapping);
					} else {
						classes.put(classMapping.getObfFullName(), classMapping);
					}
				}
				
				// translate the translation index
				// NOTE: this isn't actually recursive
				TranslationIndex deobfIndex = new TranslationIndex(index, getTranslator(TranslationDirection.Deobfuscating, index));
				
				return new Translator(direction, classes, deobfIndex);
				
			default:
				throw new Error("Invalid translation direction!");
		}
	}
	
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		for (ClassMapping classMapping : m_classesByObf.values()) {
			buf.append(classMapping.toString());
			buf.append("\n");
		}
		return buf.toString();
	}
	
	public void renameObfClass(String oldObfName, String newObfName) {
		for (ClassMapping classMapping : new ArrayList<ClassMapping>(classes())) {
			if (classMapping.renameObfClass(oldObfName, newObfName)) {
				boolean wasRemoved = m_classesByObf.remove(oldObfName) != null;
				assert (wasRemoved);
				boolean wasAdded = m_classesByObf.put(newObfName, classMapping) == null;
				assert (wasAdded);
			}
		}
	}
	
	public Set<String> getAllObfClassNames() {
		final Set<String> classNames = Sets.newHashSet();
		for (ClassMapping classMapping : classes()) {
			
			// add the class name
			classNames.add(classMapping.getObfFullName());
			
			// add classes from method signatures
			for (MethodMapping methodMapping : classMapping.methods()) {
				for (Type type : methodMapping.getObfSignature().types()) {
					if (type.hasClass()) {
						classNames.add(type.getClassEntry().getClassName());
					}
				}
			}
		}
		return classNames;
	}
	
	public boolean containsDeobfClass(String deobfName) {
		return m_classesByDeobf.containsKey(deobfName);
	}
	
	public boolean containsDeobfField(ClassEntry obfClassEntry, String deobfName, Type obfType) {
		ClassMapping classMapping = m_classesByObf.get(obfClassEntry.getName());
		if (classMapping != null) {
			return classMapping.containsDeobfField(deobfName, obfType);
		}
		return false;
	}
	
	public boolean containsDeobfMethod(ClassEntry obfClassEntry, String deobfName, Signature deobfSignature) {
		ClassMapping classMapping = m_classesByObf.get(obfClassEntry.getName());
		if (classMapping != null) {
			return classMapping.containsDeobfMethod(deobfName, deobfSignature);
		}
		return false;
	}
	
	public boolean containsArgument(BehaviorEntry obfBehaviorEntry, String name) {
		ClassMapping classMapping = m_classesByObf.get(obfBehaviorEntry.getClassName());
		if (classMapping != null) {
			return classMapping.containsArgument(obfBehaviorEntry, name);
		}
		return false;
	}
	
	public List<ClassMapping> getClassMappingChain(ClassEntry obfClass) {
		List<ClassMapping> mappingChain = Lists.newArrayList();
		ClassMapping classMapping = null;
		for (ClassEntry obfClassEntry : obfClass.getClassChain()) {
			if (mappingChain.isEmpty()) {
				classMapping = m_classesByObf.get(obfClassEntry.getName());
			} else if (classMapping != null) {
				classMapping = classMapping.getInnerClassByObfSimple(obfClassEntry.getInnermostClassName());
			}
			mappingChain.add(classMapping);
		}
		return mappingChain;
	}
} public class MappingsChecker {
	
	private JarIndex m_index;
	private RelatedMethodChecker m_relatedMethodChecker;
	private Map<ClassEntry,ClassMapping> m_droppedClassMappings;
	private Map<ClassEntry,ClassMapping> m_droppedInnerClassMappings;
	private Map<FieldEntry,FieldMapping> m_droppedFieldMappings;
	private Map<BehaviorEntry,MethodMapping> m_droppedMethodMappings;
	
	public MappingsChecker(JarIndex index) {
		m_index = index;
		m_relatedMethodChecker = new RelatedMethodChecker(m_index);
		m_droppedClassMappings = Maps.newHashMap();
		m_droppedInnerClassMappings = Maps.newHashMap();
		m_droppedFieldMappings = Maps.newHashMap();
		m_droppedMethodMappings = Maps.newHashMap();
	}
	
	public RelatedMethodChecker getRelatedMethodChecker() {
		return m_relatedMethodChecker;
	}
	
	public Map<ClassEntry,ClassMapping> getDroppedClassMappings() {
		return m_droppedClassMappings;
	}
	
	public Map<ClassEntry,ClassMapping> getDroppedInnerClassMappings() {
		return m_droppedInnerClassMappings;
	}
	
	public Map<FieldEntry,FieldMapping> getDroppedFieldMappings() {
		return m_droppedFieldMappings;
	}
	
	public Map<BehaviorEntry,MethodMapping> getDroppedMethodMappings() {
		return m_droppedMethodMappings;
	}
	
	public void dropBrokenMappings(Mappings mappings) {
		for (ClassMapping classMapping : Lists.newArrayList(mappings.classes())) {
			if (!checkClassMapping(classMapping)) {
				mappings.removeClassMapping(classMapping);
				m_droppedClassMappings.put(EntryFactory.getObfClassEntry(m_index, classMapping), classMapping);
			}
		}
	}
	
	private boolean checkClassMapping(ClassMapping classMapping) {
		
		// check the class
		ClassEntry classEntry = EntryFactory.getObfClassEntry(m_index, classMapping);
		if (!m_index.getObfClassEntries().contains(classEntry)) {
			return false;
		}
		
		// check the fields
		for (FieldMapping fieldMapping : Lists.newArrayList(classMapping.fields())) {
			FieldEntry obfFieldEntry = EntryFactory.getObfFieldEntry(classMapping, fieldMapping);
			if (!m_index.containsObfField(obfFieldEntry)) {
				classMapping.removeFieldMapping(fieldMapping);
				m_droppedFieldMappings.put(obfFieldEntry, fieldMapping);
			}
		}
		
		// check methods
		for (MethodMapping methodMapping : Lists.newArrayList(classMapping.methods())) {
			BehaviorEntry obfBehaviorEntry = EntryFactory.getObfBehaviorEntry(classEntry, methodMapping);
			if (!m_index.containsObfBehavior(obfBehaviorEntry)) {
				classMapping.removeMethodMapping(methodMapping);
				m_droppedMethodMappings.put(obfBehaviorEntry, methodMapping);
			}
			
			m_relatedMethodChecker.checkMethod(classEntry, methodMapping);
		}
		
		// check inner classes
		for (ClassMapping innerClassMapping : Lists.newArrayList(classMapping.innerClasses())) {
			if (!checkClassMapping(innerClassMapping)) {
				classMapping.removeInnerClassMapping(innerClassMapping);
				m_droppedInnerClassMappings.put(EntryFactory.getObfClassEntry(m_index, innerClassMapping), innerClassMapping);
			}
		}
		
		return true;
	}
} 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 MethodMapping implements Serializable, Comparable<MethodMapping>, MemberMapping<BehaviorEntry> {
	
	private static final long serialVersionUID = -4409570216084263978L;
	
	private String m_obfName;
	private String m_deobfName;
	private Signature m_obfSignature;
	private Map<Integer,ArgumentMapping> m_arguments;
	
	public MethodMapping(String obfName, Signature obfSignature) {
		this(obfName, obfSignature, null);
	}
	
	public MethodMapping(String obfName, Signature obfSignature, String deobfName) {
		if (obfName == null) {
			throw new IllegalArgumentException("obf name cannot be null!");
		}
		if (obfSignature == null) {
			throw new IllegalArgumentException("obf signature cannot be null!");
		}
		m_obfName = obfName;
		m_deobfName = NameValidator.validateMethodName(deobfName);
		m_obfSignature = obfSignature;
		m_arguments = Maps.newTreeMap();
	}
	
	public MethodMapping(MethodMapping other, ClassNameReplacer obfClassNameReplacer) {
		m_obfName = other.m_obfName;
		m_deobfName = other.m_deobfName;
		m_obfSignature = new Signature(other.m_obfSignature, obfClassNameReplacer);
		m_arguments = Maps.newTreeMap();
		for (Entry<Integer,ArgumentMapping> entry : other.m_arguments.entrySet()) {
			m_arguments.put(entry.getKey(), new ArgumentMapping(entry.getValue()));
		}
	}

	@Override
	public String getObfName() {
		return m_obfName;
	}
	
	public void setObfName(String val) {
		m_obfName = NameValidator.validateMethodName(val);
	}
	
	public String getDeobfName() {
		return m_deobfName;
	}
	
	public void setDeobfName(String val) {
		m_deobfName = NameValidator.validateMethodName(val);
	}
	
	public Signature getObfSignature() {
		return m_obfSignature;
	}
	
	public void setObfSignature(Signature val) {
		m_obfSignature = val;
	}
	
	public Iterable<ArgumentMapping> arguments() {
		return m_arguments.values();
	}
	
	public boolean isConstructor() {
		return m_obfName.startsWith("<");
	}
	
	public void addArgumentMapping(ArgumentMapping argumentMapping) {
		boolean wasAdded = m_arguments.put(argumentMapping.getIndex(), argumentMapping) == null;
		assert (wasAdded);
	}
	
	public String getObfArgumentName(int index) {
		ArgumentMapping argumentMapping = m_arguments.get(index);
		if (argumentMapping != null) {
			return argumentMapping.getName();
		}
		
		return null;
	}
	
	public String getDeobfArgumentName(int index) {
		ArgumentMapping argumentMapping = m_arguments.get(index);
		if (argumentMapping != null) {
			return argumentMapping.getName();
		}
		
		return null;
	}
	
	public void setArgumentName(int index, String name) {
		ArgumentMapping argumentMapping = m_arguments.get(index);
		if (argumentMapping == null) {
			argumentMapping = new ArgumentMapping(index, name);
			boolean wasAdded = m_arguments.put(index, argumentMapping) == null;
			assert (wasAdded);
		} else {
			argumentMapping.setName(name);
		}
	}
	
	public void removeArgumentName(int index) {
		boolean wasRemoved = m_arguments.remove(index) != null;
		assert (wasRemoved);
	}
	
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append("\t");
		buf.append(m_obfName);
		buf.append(" <-> ");
		buf.append(m_deobfName);
		buf.append("\n");
		buf.append("\t");
		buf.append(m_obfSignature);
		buf.append("\n");
		buf.append("\tArguments:\n");
		for (ArgumentMapping argumentMapping : m_arguments.values()) {
			buf.append("\t\t");
			buf.append(argumentMapping.getIndex());
			buf.append(" -> ");
			buf.append(argumentMapping.getName());
			buf.append("\n");
		}
		return buf.toString();
	}
	
	@Override
	public int compareTo(MethodMapping other) {
		return (m_obfName + m_obfSignature).compareTo(other.m_obfName + other.m_obfSignature);
	}
	
	public boolean renameObfClass(final String oldObfClassName, final String newObfClassName) {
		
		// rename obf classes in the signature
		Signature newSignature = new Signature(m_obfSignature, new ClassNameReplacer() {
			@Override
			public String replace(String className) {
				if (className.equals(oldObfClassName)) {
					return newObfClassName;
				}
				return null;
			}
		});
		
		if (!newSignature.equals(m_obfSignature)) {
			m_obfSignature = newSignature;
			return true;
		}
		return false;
	}
	
	public boolean containsArgument(String name) {
		for (ArgumentMapping argumentMapping : m_arguments.values()) {
			if (argumentMapping.getName().equals(name)) {
				return true;
			}
		}
		return false;
	}

	@Override
	public BehaviorEntry getObfEntry(ClassEntry classEntry) {
		if (isConstructor()) {
			return new ConstructorEntry(classEntry, m_obfSignature);
		} else {
			return new MethodEntry(classEntry, m_obfName, m_obfSignature);
		}
	}
}
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.jar.JarFile;
import cuchaz.enigma.convert.ClassMatches;
import cuchaz.enigma.convert.MappingsConverter;
import cuchaz.enigma.convert.MatchesReader;
import cuchaz.enigma.convert.MatchesWriter;
import cuchaz.enigma.convert.MemberMatches;
import cuchaz.enigma.gui.ClassMatchingGui;
import cuchaz.enigma.gui.MemberMatchingGui;
import cuchaz.enigma.mapping.BehaviorEntry;
import cuchaz.enigma.mapping.ClassEntry;
import cuchaz.enigma.mapping.ClassMapping;
import cuchaz.enigma.mapping.FieldEntry;
import cuchaz.enigma.mapping.FieldMapping;
import cuchaz.enigma.mapping.MappingParseException;
import cuchaz.enigma.mapping.Mappings;
import cuchaz.enigma.mapping.MappingsChecker;
import cuchaz.enigma.mapping.MappingsReader;
import cuchaz.enigma.mapping.MappingsWriter;
import cuchaz.enigma.mapping.MethodMapping;
/*******************************************************************************
 * 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;




public class ConvertMain {

	public static void main(String[] args)
	throws IOException, MappingParseException {
		try{
			//Get all are args
			String JarOld = getArg(args, 1, "Path to Old Jar", true);
			String JarNew = getArg(args, 2, "Path to New Jar", true);
			String OldMappings = getArg(args, 3, "Path to old .mappings file", true);
			String NewMappings = getArg(args,4,"Path to new .mappings file",true);
			String ClassMatches = getArg(args, 5, "Path to Class .matches file", true);
			String FieldMatches = getArg(args, 6, "Path to Field .matches file", true);
			String MethodMatches = getArg(args, 7, "Path to Method .matches file", true);
			//OldJar
			JarFile sourceJar = new JarFile(new File(JarOld));
			//NewJar
			JarFile destJar = new JarFile(new File(JarNew));
			//Get the mapping files
			File inMappingsFile = new File(OldMappings);
			File outMappingsFile = new File(NewMappings);
			Mappings mappings = new MappingsReader().read(new FileReader(inMappingsFile));