package by.radioegor146;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.TreeMap;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.objectweb.asm.*;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.IincInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.MultiANewArrayInsnNode;
import org.objectweb.asm.tree.TableSwitchInsnNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import ru.gravit.launchserver.asm.ClassMetadataReader;
import ru.gravit.launchserver.asm.SafeClassWriter;

public class NativeObfuscator {

	private static final Pattern PATTERN = Pattern.compile("([^a-zA-Z_0-9])");
	private static final Map<Integer, String> INSTRUCTIONS = new HashMap<>();
	private static final Properties CPP_SNIPPETS = new Properties();
	private static final String[] CPP_TYPES = { "void", // 0
			"jboolean", // 1
			"jchar", // 2
			"jbyte", // 3
			"jshort", // 4
			"jint", // 5
			"jfloat", // 6
			"jlong", // 7
			"jdouble", // 8
			"jarray", // 9
			"jobject", // 10
			"jobject" // 11
	};

	private static final String[] JAVA_DESCRIPTORS = { "V", // 0
			"Z", // 1
			"C", // 2
			"B", // 3
			"S", // 4
			"I", // 5
			"F", // 6
			"J", // 7
			"D", // 8
			"Ljava/lang/Object;", // 9
			"Ljava/lang/Object;", // 10
			"Ljava/lang/Object;" // 11
	};

	private static final int[] TYPE_TO_STACK = { 1, 1, 1, 1, 1, 1, 1, 2, 2, 0, 0, 0 };

	private static final int[] STACK_TO_STACK = { 1, 1, 1, 2, 2, 0, 0, 0, 0 };

	static {
		try {
			for (Field f : Opcodes.class.getFields())
				INSTRUCTIONS.put((int) f.get(null), f.getName());
			CPP_SNIPPETS.load(
					NativeObfuscator.class.getClassLoader().getResourceAsStream("sources/cppsnippets.properties"));
		} catch (IllegalArgumentException | IllegalAccessException | IOException ex) {
			throw new RuntimeException(ex);
		}
	}

	private String escapeCppNameString(String value) {
		Matcher m = PATTERN.matcher(value);
		StringBuffer sb = new StringBuffer(value.length());
		while (m.find())
			m.appendReplacement(sb, String.valueOf((int) m.group(1).charAt(0)));
		m.appendTail(sb);
		String output = sb.toString();
		if (output.length() > 0 && (output.charAt(0) >= '0' && output.charAt(0) <= '9'))
			output = "_" + output;
		return output;
	}

	private Map<String, String> createMap(Object... parts) {
		HashMap<String, String> tokens = new HashMap<>();
		for (int i = 0; i < parts.length; i += 2) {
			tokens.put(parts[i].toString(), parts[i + 1].toString());
		}
		return tokens;
	}

	private String dynamicFormat(String string, Map<String, String> tokens) {
		String patternString = "\\$("
				+ String.join("|", tokens.keySet().stream().map(x -> unicodify(x)).collect(Collectors.toList())) + ")";
		Pattern pattern = Pattern.compile(patternString);
		Matcher matcher = pattern.matcher(string);

		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			matcher.appendReplacement(sb, Matcher.quoteReplacement(tokens.get(matcher.group(1))));
		}
		matcher.appendTail(sb);

		return sb.toString();
	}

	private String dynamicRawFormat(String string, Map<String, String> tokens) {
		if (tokens.isEmpty())
			return string;
		String patternString = "("
				+ String.join("|", tokens.keySet().stream().map(x -> unicodify(x)).collect(Collectors.toList())) + ")";
		Pattern pattern = Pattern.compile(patternString);
		Matcher matcher = pattern.matcher(string);

		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			matcher.appendReplacement(sb, Matcher.quoteReplacement(tokens.get(matcher.group(1))));
		}
		matcher.appendTail(sb);

		return sb.toString();
	}

	private final HashMap<String, Integer> stringPool = new HashMap<>();
	private final HashMap<String, Integer> cachedClasses = new HashMap<>();
	private final HashMap<CachedMethodInfo, Integer> cachedMethods = new HashMap<>();
	private final HashMap<CachedFieldInfo, Integer> cachedFields = new HashMap<>();
	private StringBuilder ifaceStaticNativeMethodsSb = new StringBuilder();
	private StringBuilder nativeMethodsSb = new StringBuilder();
	private Map<String, InvokeDynamicInsnNode> invokeDynamics = new HashMap<>();

	private int currentLength = 0;

	private String getStringPooledString(String value) {
		if (!stringPool.containsKey(value)) {
			stringPool.put(value, currentLength);
			currentLength += value.getBytes(StandardCharsets.UTF_8).length + 1;
		}
		return "((char *)(string_pool + " + stringPool.get(value) + "LL))";
	}

	private String getCachedClassPointer(String name) {
		if (!cachedClasses.containsKey(name))
			cachedClasses.put(name, cachedClasses.size());
		return "(cclasses[" + cachedClasses.get(name) + "])";
	}

	private String getCachedMethodPointer(String clazz, String name, String desc, boolean isStatic) {
		if (!cachedMethods.containsKey(new CachedMethodInfo(clazz, name, desc, isStatic)))
			cachedMethods.put(new CachedMethodInfo(clazz, name, desc, isStatic), cachedMethods.size());
		return "(cmethods[" + cachedMethods.get(new CachedMethodInfo(clazz, name, desc, isStatic)) + "].load())";
	}

	private String getCachedFieldPointer(String clazz, String name, String desc, boolean isStatic) {
		if (!cachedFields.containsKey(new CachedFieldInfo(clazz, name, desc, isStatic)))
			cachedFields.put(new CachedFieldInfo(clazz, name, desc, isStatic), cachedFields.size());
		return "(cfields[" + cachedFields.get(new CachedFieldInfo(clazz, name, desc, isStatic)) + "].load())";
	}

	private int getCachedMethodId(String clazz, String name, String desc, boolean isStatic) {
		if (!cachedMethods.containsKey(new CachedMethodInfo(clazz, name, desc, isStatic)))
			cachedMethods.put(new CachedMethodInfo(clazz, name, desc, isStatic), cachedMethods.size());
		return cachedMethods.get(new CachedMethodInfo(clazz, name, desc, isStatic));
	}

	private int getCachedFieldId(String clazz, String name, String desc, boolean isStatic) {
		if (!cachedFields.containsKey(new CachedFieldInfo(clazz, name, desc, isStatic)))
			cachedFields.put(new CachedFieldInfo(clazz, name, desc, isStatic), cachedFields.size());
		return cachedFields.get(new CachedFieldInfo(clazz, name, desc, isStatic));
	}

	private String unicodify(String string) {
		StringBuilder result = new StringBuilder();
		for (char c : string.toCharArray()) {
			result.append("\\u").append(String.format("%04x", (int) c));
		}
		return result.toString();
	}

	private String dynamicStringPoolFormat(String key, Map<String, String> tokens) {
		String value = CPP_SNIPPETS.getProperty(key);
		if (value == null)
			throw new RuntimeException(key + " not found");
		String[] stringVars = CPP_SNIPPETS.getProperty(key + "_S_VARS") == null
				|| CPP_SNIPPETS.getProperty(key + "_S_VARS").equals("") ? new String[0]
						: CPP_SNIPPETS.getProperty(key + "_S_VARS").split(",");
		HashMap<String, String> vars = new HashMap<>();
		for (String var : stringVars) {
			if (var.startsWith("#")) {
				vars.put(var, CPP_SNIPPETS.getProperty(key + "_S_CONST_" + var.substring(1)));
			} else if (var.startsWith("$")) {
				vars.put(var, tokens.get(var.substring(1)));
			} else {
				throw new RuntimeException("Unknown format modifier: " + var);
			}
		}
		vars.entrySet().stream().filter((var) -> (var.getValue() == null)).forEachOrdered((var) -> {
			throw new RuntimeException(key + " - " + var.getKey() + " is null");
		});
		HashMap<String, String> replaceTokens = new HashMap<>();
		vars.entrySet().forEach((var) -> {
			replaceTokens.put(var.getKey(), getStringPooledString(var.getValue()));
		});
		tokens.entrySet().forEach((var) -> {
			if (!replaceTokens.containsKey("$" + var.getKey()))
				replaceTokens.put("$" + var.getKey(), var.getValue());
		});
		return dynamicRawFormat(value, replaceTokens);
	}

	private final List<ClassNode> readyIfaceStaticClasses = new ArrayList<>();
	private ClassNode currentIfaceStaticClass;

	@SuppressWarnings("unchecked")
	static <T> Stream<T> reverse(Stream<T> input) {
		Object[] temp = input.toArray();
		return (Stream<T>) IntStream.range(0, temp.length).mapToObj(i -> temp[temp.length - i - 1]);
	}

	private void setupNewIfaceStaticClass(Boolean forAndroid) {
		if (currentIfaceStaticClass != null && currentIfaceStaticClass.methods.size() > 0)
			readyIfaceStaticClasses.add(currentIfaceStaticClass);
		currentIfaceStaticClass = new ClassNode();
		currentIfaceStaticClass.sourceFile = "synthetic";
		currentIfaceStaticClass.name = "native" + nativeDirId + "/interfacestatic/Methods"
				+ readyIfaceStaticClasses.size();
		if (forAndroid) { // Android
			currentIfaceStaticClass.version = 50;
		} else {
			currentIfaceStaticClass.version = 52;
		}
		currentIfaceStaticClass.superName = "java/lang/Object";
		currentIfaceStaticClass.access = Opcodes.ACC_PUBLIC;
	}

	private String visitMethod(ClassNode classNode, MethodNode methodNode, int index) {
		if (((methodNode.access & Opcodes.ACC_ABSTRACT) > 0) || ((methodNode.access & Opcodes.ACC_NATIVE) > 0))
			return "";
		if (methodNode.name.equals("<init>"))
			return "";
		StringBuilder outputSb = new StringBuilder("// ");
		outputSb.append(methodNode.name).append(methodNode.desc).append("\n");
		String methodName = "";
		MethodNode proxifiedResult;
		switch (methodNode.name) {
		case "<init>":
			proxifiedResult = new MethodNode(Opcodes.ASM7,
					Opcodes.ACC_NATIVE | Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC,
					"native_special_init" + index, methodNode.desc, methodNode.signature, new String[0]);
			classNode.methods.add(proxifiedResult);
			methodName += "native_special_init";
			break;
		case "<clinit>":
			proxifiedResult = new MethodNode(Opcodes.ASM7,
					Opcodes.ACC_NATIVE | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
					"native_special_clinit" + index, methodNode.desc, methodNode.signature, new String[0]);
			classNode.methods.add(proxifiedResult);
			methodName += "native_special_clinit";
			break;
		default:
			proxifiedResult = methodNode;
			methodNode.access |= Opcodes.ACC_NATIVE;
			methodName += "native_" + methodNode.name;
			break;
		}
		methodName += index;
		methodName = "__ngen_" + methodName.replace("/", "_");
		methodName = escapeCppNameString(methodName);

		int returnTypeSort = Type.getReturnType(methodNode.desc).getSort();
		Type[] args = Type.getArgumentTypes(methodNode.desc);
		MethodNode nativeMethod = null;
		if ((classNode.access & Opcodes.ACC_INTERFACE) > 0) {
			if (currentIfaceStaticClass.methods.size() > 16384) {
				throw new RuntimeException("too many static interface methods");
			}
			StringBuilder resultProcType = new StringBuilder("(");
			for (Type t : args)
				resultProcType.append(JAVA_DESCRIPTORS[t.getSort()]);
			resultProcType.append(")").append(JAVA_DESCRIPTORS[returnTypeSort]);
			String outerJavaMethodName = "iface_static_" + currentClassId + "_" + index;
			nativeMethod = new MethodNode(Opcodes.ASM7,
					Opcodes.ACC_NATIVE | Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
					outerJavaMethodName, resultProcType.toString(), null, new String[0]);
			currentIfaceStaticClass.methods.add(nativeMethod);
			ifaceStaticNativeMethodsSb.append("            { (char *)")
					.append(getStringPooledString(outerJavaMethodName)).append(", (char *)")
					.append(getStringPooledString(resultProcType.toString())).append(", (void *)&").append(methodName)
					.append(" },\n");
		} else
			nativeMethodsSb.append("            { (char *)").append(getStringPooledString(proxifiedResult.name))
					.append(", (char *)").append(getStringPooledString(methodNode.desc)).append(", (void *)&")
					.append(methodName).append(" },\n");
		outputSb.append(CPP_TYPES[returnTypeSort]).append(" ").append("JNICALL").append(" ").append(methodName)
				.append("(").append("JNIEnv *env").append(", ")
				.append(((methodNode.access & Opcodes.ACC_STATIC) > 0) ? "jclass clazz" : "jobject obj");
		if (args.length > 0)
			outputSb.append(", ");
		for (int i = 0; i < args.length; i++)
			outputSb.append(CPP_TYPES[args[i].getSort()]).append(" ").append("arg").append(i)
					.append(i == args.length - 1 ? "" : ", ");
		outputSb.append(") {").append("\n");
		if (methodNode.maxStack > 0)
			outputSb.append("    ").append("utils::jvm_stack<").append(methodNode.maxStack).append("> cstack;")
					.append("\n");
		if (methodNode.maxLocals > 0)
			outputSb.append("    ").append("utils::local_vars<").append(methodNode.maxLocals).append("> clocals;")
					.append("\n");
		outputSb.append("    ").append("std::unordered_set<jobject> refs;").append("\n");
		outputSb.append("\n");
		int localIndex = 0;
		if (((methodNode.access & Opcodes.ACC_STATIC) == 0)) {
			outputSb.append("    ").append(
					dynamicStringPoolFormat("LOCAL_LOAD_ARG_" + 9, createMap("index", localIndex, "arg", "obj")))
					.append("\n");
			localIndex++;
		}
		for (int i = 0; i < args.length; i++) {
			outputSb.append("    ").append(dynamicStringPoolFormat("LOCAL_LOAD_ARG_" + args[i].getSort(),
					createMap("index", localIndex, "arg", "arg" + i))).append("\n");
			localIndex += args[i].getSize();
		}
		outputSb.append("\n");
		List<TryCatchBlockNode> currentTryCatches = new ArrayList<>();
		int currentLine = -1;
		int invokeSpecialId = -1;
		List<Integer> currentStack = new ArrayList<>();
		List<Integer> currentLocals = new ArrayList<>();
		if ((methodNode.access & Opcodes.ACC_STATIC) == 0)
			currentLocals.add(TYPE_TO_STACK[Type.OBJECT]);
		for (Type localArg : args)
			currentLocals.add(TYPE_TO_STACK[localArg.getSort()]);
		for (int insnIndex = 0; insnIndex < methodNode.instructions.size(); insnIndex++) {
			if (methodNode.name.equals("<init>") && invokeSpecialId < 0) {
				if (methodNode.instructions.get(insnIndex).getOpcode() == Opcodes.INVOKESPECIAL) {
					invokeSpecialId = insnIndex;
				}
				continue;
			}
			AbstractInsnNode insnNode = methodNode.instructions.get(insnIndex);
			switch (insnNode.getType()) {
			case AbstractInsnNode.LABEL:
				outputSb.append(((LabelNode) insnNode).getLabel()).append(": ;").append("\n");
				reverse(methodNode.tryCatchBlocks.stream().filter((node) -> (node.start.equals(insnNode))))
						.forEachOrdered(currentTryCatches::add);
				methodNode.tryCatchBlocks.stream().filter((node) -> (node.end.equals(insnNode)))
						.forEachOrdered(currentTryCatches::remove);
				break;
			case AbstractInsnNode.LINE:
				outputSb.append("    ").append("// Line ").append(((LineNumberNode) insnNode).line).append(":")
						.append("\n");
				currentLine = ((LineNumberNode) insnNode).line;
				break;
			case AbstractInsnNode.FRAME:
				FrameNode frameNode = (FrameNode) insnNode;
				switch (frameNode.type) {
				case Opcodes.F_APPEND:
					for (Object local : frameNode.local) {
						if (local instanceof String)
							currentLocals.add(TYPE_TO_STACK[Type.OBJECT]);
						else if (local instanceof LabelNode)
							currentLocals.add(TYPE_TO_STACK[Type.OBJECT]);
						else
							currentLocals.add(STACK_TO_STACK[(int) local]);
					}
					break;
				case Opcodes.F_CHOP:
					for (int i = 0; i < frameNode.local.size(); i++)
						currentLocals.remove(currentLocals.size() - 1);
					currentStack.clear();
					break;
				case Opcodes.F_NEW:
				case Opcodes.F_FULL:
					currentLocals.clear();
					currentStack.clear();
					for (Object local : frameNode.local) {
						if (local instanceof String)
							currentLocals.add(TYPE_TO_STACK[Type.OBJECT]);
						else if (local instanceof LabelNode)
							currentLocals.add(TYPE_TO_STACK[Type.OBJECT]);
						else
							currentLocals.add(STACK_TO_STACK[(int) local]);
					}
					for (Object stack : frameNode.stack) {
						if (stack instanceof String)
							currentStack.add(TYPE_TO_STACK[Type.OBJECT]);
						else if (stack instanceof LabelNode)
							currentStack.add(TYPE_TO_STACK[Type.OBJECT]);
						else
							currentStack.add(STACK_TO_STACK[(int) stack]);
					}
					break;
				case Opcodes.F_SAME:
					break;
				case Opcodes.F_SAME1:
					if (frameNode.stack.get(0) instanceof String)
						currentStack.add(TYPE_TO_STACK[Type.OBJECT]);
					else if (frameNode.stack.get(0) instanceof LabelNode)
						currentStack.add(TYPE_TO_STACK[Type.OBJECT]);
					else
						currentStack.add(STACK_TO_STACK[(int) frameNode.stack.get(0)]);
					break;
				}
				if (currentStack.stream().anyMatch(x -> x == 0)) {
					int currentSp = 0;
					outputSb.append("    ");
					for (int type : currentStack) {
						if (type == 0)
							outputSb.append("refs.erase(cstack.refs[" + currentSp + "]); ");
						currentSp += Math.max(1, type);
					}
					outputSb.append("\n");
				}
				if (currentLocals.stream().anyMatch(x -> x == 0)) {
					int currentLp = 0;
					outputSb.append("    ");
					for (int type : currentLocals) {
						if (type == 0)
							outputSb.append("refs.erase(clocals.refs[" + currentLp + "]); ");
						currentLp += Math.max(1, type);
					}
					outputSb.append("\n");
				}
				outputSb.append("    utils::clear_refs(env, refs);\n");
				break;
			default:
				StringBuilder tryCatch = new StringBuilder("\n");
				if (currentTryCatches.size() > 0) {
					tryCatch.append("    ").append(dynamicStringPoolFormat("TRYCATCH_START", createMap())).append("\n");
					for (int i = currentTryCatches.size() - 1; i >= 0; i--) {
						TryCatchBlockNode tryCatchBlock = currentTryCatches.get(i);
						if (tryCatchBlock.type == null) {
							tryCatch.append("    ")
									.append(dynamicStringPoolFormat("TRYCATCH_ANY_L",
											createMap("rettype", CPP_TYPES[returnTypeSort], "handler_block",
													tryCatchBlock.handler.getLabel().toString())))
									.append("\n");
							break;
						} else {
							tryCatch.append("    ")
									.append(dynamicStringPoolFormat("TRYCATCH_CHECK",
											createMap("rettype", CPP_TYPES[returnTypeSort], "exception_class_ptr",
													getCachedClassPointer(tryCatchBlock.type), "handler_block",
													tryCatchBlock.handler.getLabel().toString())))
									.append("\n");
						}
					}
					tryCatch.append("    ").append(
							dynamicStringPoolFormat("TRYCATCH_END", createMap("rettype", CPP_TYPES[returnTypeSort])));
				} else
					tryCatch.append("    ").append(
							dynamicStringPoolFormat("TRYCATCH_EMPTY", createMap("rettype", CPP_TYPES[returnTypeSort])));
				outputSb.append("    ");
				String insnName = INSTRUCTIONS.getOrDefault(insnNode.getOpcode(), "NOTFOUND");
				HashMap<String, String> props = new HashMap<>();
				props.put("line", String.valueOf(currentLine));
				props.put("trycatchhandler", tryCatch.toString());
				props.put("rettype", CPP_TYPES[returnTypeSort]);
				String trimmedTryCatchBlock = tryCatch.toString().trim().replace("\n", " ");
				if (insnNode instanceof FieldInsnNode) {
					insnName += "_" + Type.getType(((FieldInsnNode) insnNode).desc).getSort();
					if (insnNode.getOpcode() == Opcodes.GETSTATIC || insnNode.getOpcode() == Opcodes.PUTSTATIC)
						props.put("class_ptr", getCachedClassPointer(((FieldInsnNode) insnNode).owner));
					int fieldId = getCachedFieldId(((FieldInsnNode) insnNode).owner, ((FieldInsnNode) insnNode).name,
							((FieldInsnNode) insnNode).desc,
							insnNode.getOpcode() == Opcodes.GETSTATIC || insnNode.getOpcode() == Opcodes.PUTSTATIC);
					outputSb.append("if (!cfields[").append(fieldId).append("].load()) { cfields[").append(fieldId)
							.append("].store(env->Get")
							.append((insnNode.getOpcode() == Opcodes.GETSTATIC
									|| insnNode.getOpcode() == Opcodes.PUTSTATIC) ? "Static" : "")
							.append("FieldID(").append(getCachedClassPointer(((FieldInsnNode) insnNode).owner))
							.append(", ").append(getStringPooledString(((FieldInsnNode) insnNode).name)).append(", ")
							.append(getStringPooledString(((FieldInsnNode) insnNode).desc)).append(")); ")
							.append(trimmedTryCatchBlock).append("  } ");
					props.put("fieldid", getCachedFieldPointer(((FieldInsnNode) insnNode).owner,
							((FieldInsnNode) insnNode).name, ((FieldInsnNode) insnNode).desc,
							insnNode.getOpcode() == Opcodes.GETSTATIC || insnNode.getOpcode() == Opcodes.PUTSTATIC));
				}
				if (insnNode instanceof IincInsnNode) {
					props.put("incr", String.valueOf(((IincInsnNode) insnNode).incr));
					props.put("var", String.valueOf(((IincInsnNode) insnNode).var));
				}
				if (insnNode instanceof IntInsnNode) {
					props.put("operand", String.valueOf(((IntInsnNode) insnNode).operand));
					if (insnNode.getOpcode() == Opcodes.NEWARRAY) {
						insnName += "_" + ((IntInsnNode) insnNode).operand;
					}
				}
				if (insnNode instanceof InvokeDynamicInsnNode) {
					String indyMethodName = "invokedynamic$" + methodNode.name + "$" + invokeDynamics.size();
					invokeDynamics.put(indyMethodName, (InvokeDynamicInsnNode) insnNode);
					Type returnType = Type.getReturnType(((InvokeDynamicInsnNode) insnNode).desc);
					Type[] argTypes = Type.getArgumentTypes(((InvokeDynamicInsnNode) insnNode).desc);
					insnName = "INVOKESTATIC_" + returnType.getSort();
					StringBuilder argsBuilder = new StringBuilder();
					List<Integer> argOffsets = new ArrayList<>();
					List<Integer> argSorts = new ArrayList<>();
					int stackOffset = -1;
					for (Type argType : argTypes) {
						int currentOffset = stackOffset;
						stackOffset -= argType.getSize();
						argOffsets.add(currentOffset);
						argSorts.add(argType.getSort());
					}
					for (int i = 0; i < argOffsets.size(); i++)
						argsBuilder.append(", ").append(dynamicStringPoolFormat("INVOKE_ARG_" + argSorts.get(i),
								createMap("index", String.valueOf(argOffsets.get(i)))));
					outputSb.append(dynamicStringPoolFormat("INVOKE_POPCNT",
							createMap("count", String.valueOf(-stackOffset - 1)))).append(" ");
					props.put("class_ptr", getCachedClassPointer(classNode.name));
					int methodId = getCachedMethodId(classNode.name, indyMethodName,
							((InvokeDynamicInsnNode) insnNode).desc, true);
					outputSb.append("if (!cmethods[").append(methodId).append("].load()) { cmethods[").append(methodId)
							.append("].store(env->GetStaticMethodID(").append(getCachedClassPointer(classNode.name))
							.append(", ").append(getStringPooledString(indyMethodName)).append(", ")
							.append(getStringPooledString(((InvokeDynamicInsnNode) insnNode).desc)).append(")); ")
							.append(trimmedTryCatchBlock).append("  } ");
					props.put("methodid", getCachedMethodPointer(classNode.name, indyMethodName,
							((InvokeDynamicInsnNode) insnNode).desc, true));
					props.put("args", argsBuilder.toString());
				}
				if (insnNode instanceof JumpInsnNode) {
					props.put("label", String.valueOf(((JumpInsnNode) insnNode).label.getLabel()));
				}
				if (insnNode instanceof LdcInsnNode) {
					Object cst = ((LdcInsnNode) insnNode).cst;
					if (cst instanceof java.lang.String) {
						insnName += "_STRING";
						props.put("cst", String.valueOf(((LdcInsnNode) insnNode).cst));
					} else if (cst instanceof java.lang.Integer) {
						insnName += "_INT";
						props.put("cst", String.valueOf(((LdcInsnNode) insnNode).cst));
					} else if (cst instanceof java.lang.Long) {
						insnName += "_LONG";
						props.put("cst", String.valueOf(((LdcInsnNode) insnNode).cst));
					} else if (cst instanceof java.lang.Float) {
						insnName += "_FLOAT";
						props.put("cst", String.valueOf(((LdcInsnNode) insnNode).cst));
						float cstVal = (float) cst;
						if (cst.toString().equals("NaN"))
							props.put("cst", "NAN");
						else if (cstVal == Float.POSITIVE_INFINITY)
							props.put("cst", "HUGE_VALF");
						else if (cstVal == Float.NEGATIVE_INFINITY)
							props.put("cst", "-HUGE_VALF");
					} else if (cst instanceof java.lang.Double) {
						insnName += "_DOUBLE";
						props.put("cst", String.valueOf(((LdcInsnNode) insnNode).cst));
						double cstVal = (double) cst;
						if (cst.toString().equals("NaN"))
							props.put("cst", "NAN");
						else if (cstVal == Double.POSITIVE_INFINITY)
							props.put("cst", "HUGE_VAL");
						else if (cstVal == Double.NEGATIVE_INFINITY)
							props.put("cst", "-HUGE_VAL");
					} else if (cst instanceof org.objectweb.asm.Type) {
						insnName += "_CLASS";
						props.put("cst_ptr", getCachedClassPointer(((LdcInsnNode) insnNode).cst.toString()));
					} else {
						throw new UnsupportedOperationException();
					}
				}
				if (insnNode instanceof LookupSwitchInsnNode) {
					outputSb.append(dynamicStringPoolFormat("LOOKUPSWITCH_START", createMap())).append("\n");
					for (int switchIndex = 0; switchIndex < ((LookupSwitchInsnNode) insnNode).labels
							.size(); switchIndex++)
						outputSb.append("    ").append("    ")
								.append(dynamicStringPoolFormat("LOOKUPSWITCH_PART", createMap("key",
										String.valueOf(((LookupSwitchInsnNode) insnNode).keys.get(switchIndex)),
										"label",
										String.valueOf(
												((LookupSwitchInsnNode) insnNode).labels.get(switchIndex).getLabel()))))
								.append("\n");
					outputSb.append("    ").append("    ")
							.append(dynamicStringPoolFormat("LOOKUPSWITCH_DEFAULT",
									createMap("label",
											String.valueOf(((LookupSwitchInsnNode) insnNode).dflt.getLabel()))))
							.append("\n");
					outputSb.append("    ").append(dynamicStringPoolFormat("LOOKUPSWITCH_END", createMap()))
							.append("\n");
					continue;
				}
				if (insnNode instanceof MethodInsnNode) {
					Type returnType = Type.getReturnType(((MethodInsnNode) insnNode).desc);
					Type[] argTypes = Type.getArgumentTypes(((MethodInsnNode) insnNode).desc);
					insnName += "_" + returnType.getSort();
					StringBuilder argsBuilder = new StringBuilder();
					List<Integer> argOffsets = new ArrayList<>();
					List<Integer> argSorts = new ArrayList<>();
					int stackOffset = -1;
					for (Type argType : argTypes) {
						int currentOffset = stackOffset;
						stackOffset -= argType.getSize();
						argOffsets.add(currentOffset);
						argSorts.add(argType.getSort());
					}
					if (insnNode.getOpcode() == Opcodes.INVOKEINTERFACE || insnNode.getOpcode() == Opcodes.INVOKESPECIAL
							|| insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL) {
						for (int i = 0; i < argOffsets.size(); i++)
							argsBuilder.append(", ").append(dynamicStringPoolFormat("INVOKE_ARG_" + argSorts.get(i),
									createMap("index", String.valueOf(argOffsets.get(i) - 1))));
						if (stackOffset != 0)
							outputSb.append(dynamicStringPoolFormat("INVOKE_POPCNT",
									createMap("count", String.valueOf(-stackOffset)))).append(" ");
						if (insnNode.getOpcode() == Opcodes.INVOKESPECIAL)
							props.put("class_ptr", getCachedClassPointer(((MethodInsnNode) insnNode).owner));
						int methodId = getCachedMethodId(((MethodInsnNode) insnNode).owner,
								((MethodInsnNode) insnNode).name, ((MethodInsnNode) insnNode).desc, false);
						outputSb.append("if (!cmethods[").append(methodId).append("].load()) { cmethods[")
								.append(methodId).append("].store(env->GetMethodID(")
								.append(getCachedClassPointer(((MethodInsnNode) insnNode).owner)).append(", ")
								.append(getStringPooledString(((MethodInsnNode) insnNode).name)).append(", ")
								.append(getStringPooledString(((MethodInsnNode) insnNode).desc)).append(")); ")
								.append(trimmedTryCatchBlock).append("  } ");
						props.put("methodid", getCachedMethodPointer(((MethodInsnNode) insnNode).owner,
								((MethodInsnNode) insnNode).name, ((MethodInsnNode) insnNode).desc, false));
						props.put("object_offset", "-1");
						props.put("args", argsBuilder.toString());
					} else {
						for (int i = 0; i < argOffsets.size(); i++)
							argsBuilder.append(", ").append(dynamicStringPoolFormat("INVOKE_ARG_" + argSorts.get(i),
									createMap("index", String.valueOf(argOffsets.get(i)))));
						if (-stackOffset - 1 != 0)
							outputSb.append(dynamicStringPoolFormat("INVOKE_POPCNT",
									createMap("count", String.valueOf(-stackOffset - 1)))).append(" ");
						props.put("class_ptr", getCachedClassPointer(((MethodInsnNode) insnNode).owner));
						int methodId = getCachedMethodId(((MethodInsnNode) insnNode).owner,
								((MethodInsnNode) insnNode).name, ((MethodInsnNode) insnNode).desc, true);
						outputSb.append("if (!cmethods[").append(methodId).append("].load()) { cmethods[")
								.append(methodId).append("].store(env->GetStaticMethodID(")
								.append(getCachedClassPointer(((MethodInsnNode) insnNode).owner)).append(", ")
								.append(getStringPooledString(((MethodInsnNode) insnNode).name)).append(", ")
								.append(getStringPooledString(((MethodInsnNode) insnNode).desc)).append(")); ")
								.append(trimmedTryCatchBlock).append("  } ");
						props.put("methodid", getCachedMethodPointer(((MethodInsnNode) insnNode).owner,
								((MethodInsnNode) insnNode).name, ((MethodInsnNode) insnNode).desc, true));
						props.put("args", argsBuilder.toString());
					}
				}
				if (insnNode instanceof MultiANewArrayInsnNode) {
					props.put("count", String.valueOf(((MultiANewArrayInsnNode) insnNode).dims));
					props.put("desc", ((MultiANewArrayInsnNode) insnNode).desc);
				}
				if (insnNode instanceof TableSwitchInsnNode) {
					outputSb.append(dynamicStringPoolFormat("TABLESWITCH_START", createMap())).append("\n");
					for (int switchIndex = 0; switchIndex < ((TableSwitchInsnNode) insnNode).labels
							.size(); switchIndex++)
						outputSb.append("    ").append("    ")
								.append(dynamicStringPoolFormat("TABLESWITCH_PART", createMap("index",
										String.valueOf(((TableSwitchInsnNode) insnNode).min + switchIndex), "label",
										String.valueOf(
												((TableSwitchInsnNode) insnNode).labels.get(switchIndex).getLabel()))))
								.append("\n");
					outputSb.append("    ").append("    ")
							.append(dynamicStringPoolFormat("TABLESWITCH_DEFAULT",
									createMap("label",
											String.valueOf(((TableSwitchInsnNode) insnNode).dflt.getLabel()))))
							.append("\n");
					outputSb.append("    ").append(dynamicStringPoolFormat("TABLESWITCH_END", createMap()))
							.append("\n");
					continue;
				}
				if (insnNode instanceof TypeInsnNode) {
					props.put("desc", (((TypeInsnNode) insnNode).desc));
					props.put("desc_ptr", getCachedClassPointer(((TypeInsnNode) insnNode).desc));
				}
				if (insnNode instanceof VarInsnNode) {
					props.put("var", String.valueOf(((VarInsnNode) insnNode).var));
				}
				String cppCode = CPP_SNIPPETS.getProperty(insnName);
				if (cppCode == null) {
					throw new RuntimeException("insn not found: " + insnName);
				} else {
					cppCode = dynamicStringPoolFormat(insnName, props);
					outputSb.append(cppCode);
				}
				outputSb.append("\n");
				break;
			}
		}
		outputSb.append("    return (").append(CPP_TYPES[returnTypeSort]).append(") 0;\n");
		outputSb.append("}\n\n");

		methodNode.localVariables.clear();
		methodNode.tryCatchBlocks.clear();

		switch (methodNode.name) {
		case "<init>": {
			InsnList list = new InsnList();
			for (int i = 0; i <= invokeSpecialId; i++)
				list.add(methodNode.instructions.get(i));
			list.add(new VarInsnNode(Opcodes.ALOAD, 0));
			int localVarsPosition = 1;
			for (Type arg : args) {
				list.add(new VarInsnNode(arg.getOpcode(Opcodes.ILOAD), localVarsPosition));
				localVarsPosition += arg.getSize();
			}
			list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, classNode.name, "native_special_init" + index,
					methodNode.desc));
			list.add(new InsnNode(Opcodes.RETURN));
			methodNode.instructions = list;
		}
			break;
		case "<clinit>":
			methodNode.instructions.clear();
			methodNode.instructions.add(new LdcInsnNode((int) currentClassId));
			methodNode.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "native" + nativeDirId + "/Loader",
					"registerNativesForClass", "(I)V"));
			methodNode.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, classNode.name,
					"native_special_clinit" + index, methodNode.desc));
			if ((classNode.access & Opcodes.ACC_INTERFACE) > 0) {
				proxifiedResult.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, currentIfaceStaticClass.name,
						nativeMethod.name, nativeMethod.desc));
				proxifiedResult.instructions.add(new InsnNode(Opcodes.RETURN));
			}
			methodNode.instructions.add(new InsnNode(Opcodes.RETURN));
			break;
		default:
			methodNode.instructions.clear();
			if ((classNode.access & Opcodes.ACC_INTERFACE) > 0) {
				InsnList list = new InsnList();
				for (int i = 0; i <= invokeSpecialId; i++)
					list.add(methodNode.instructions.get(i));
				int localVarsPosition = 0;
				for (Type arg : args) {
					list.add(new VarInsnNode(arg.getOpcode(Opcodes.ILOAD), localVarsPosition));
					localVarsPosition += arg.getSize();
				}
				methodNode.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, currentIfaceStaticClass.name,
						nativeMethod.name, nativeMethod.desc));
				methodNode.instructions
						.add(new InsnNode(Type.getReturnType(methodNode.desc).getOpcode(Opcodes.IRETURN)));
			}
			break;
		}

		return outputSb.toString();
	}

	private void processIndy(ClassNode classNode, String methodName, InvokeDynamicInsnNode indy) {
		MethodNode indyWrapper = new MethodNode(Opcodes.ASM7,
				Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC, methodName,
				indy.desc, null, new String[0]);
		int localVarsPosition = 0;
		for (Type arg : Type.getArgumentTypes(indy.desc)) {
			indyWrapper.instructions.add(new VarInsnNode(arg.getOpcode(Opcodes.ILOAD), localVarsPosition));
			localVarsPosition += arg.getSize();
		}
		indyWrapper.instructions.add(new InvokeDynamicInsnNode(indy.name, indy.desc, indy.bsm, indy.bsmArgs));
		indyWrapper.instructions.add(new InsnNode(Opcodes.ARETURN));
		classNode.methods.add(indyWrapper);
	}

	private String writeStreamToString(InputStream stream) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		transfer(stream, baos);
		return new String(baos.toByteArray(), StandardCharsets.UTF_8);
	}

	private void writeStreamToFile(InputStream stream, Path path) throws IOException {
		byte[] buffer = new byte[4096];
		int bytesRead;
		try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE,
				StandardOpenOption.TRUNCATE_EXISTING)) {
			while ((bytesRead = stream.read(buffer)) != -1) {
				outputStream.write(buffer, 0, bytesRead);
			}
		}
	}

	private String getGetterForType(String desc) {
		if (desc.startsWith("["))
			return "env->FindClass(" + getStringPooledString(desc) + ")";
		if (desc.endsWith(";"))
			desc = desc.substring(1, desc.length() - 1);
		return "utils::find_class_wo_static(env, " + getStringPooledString(desc.replace("/", ".")) + ")";
	}

	private int currentClassId;
	private int nativeDirId = 0;

	public void process(Path inputJar, Path outputDir, List<Path> libs, Boolean forAndroid) throws IOException {
		libs.add(inputJar);
		ClassMetadataReader metadataReader = new ClassMetadataReader(libs.stream().map(x -> {
			try {
				return new JarFile(inputJar.toFile());
			} catch (IOException ex) {
				return null;
			}
		}).collect(Collectors.toList()));
		final File jar = inputJar.toAbsolutePath().toFile();
		Files.createDirectories(outputDir);
		Files.createDirectories(outputDir.resolve("cpp"));
		Files.createDirectories(outputDir.resolve("cpp").resolve("output"));
		try (InputStream in = NativeObfuscator.class.getClassLoader().getResourceAsStream("sources/native_jvm.cpp")) {
			writeStreamToFile(in, outputDir.resolve("cpp").resolve("native_jvm.cpp"));
		}
		try (InputStream in = NativeObfuscator.class.getClassLoader().getResourceAsStream("sources/native_jvm.hpp")) {
			writeStreamToFile(in, outputDir.resolve("cpp").resolve("native_jvm.hpp"));
		}
		try (InputStream in = NativeObfuscator.class.getClassLoader()
				.getResourceAsStream("sources/native_jvm_output.hpp")) {
			writeStreamToFile(in, outputDir.resolve("cpp").resolve("native_jvm_output.hpp"));
		}
		try (InputStream in = NativeObfuscator.class.getClassLoader().getResourceAsStream("sources/string_pool.hpp")) {
			writeStreamToFile(in, outputDir.resolve("cpp").resolve("string_pool.hpp"));
		}

		if (forAndroid) { // Android
			try (InputStream in = NativeObfuscator.class.getClassLoader()
					.getResourceAsStream("sources/android/jvmti.h")) {
				writeStreamToFile(in, outputDir.resolve("cpp").resolve("jvmti.h"));
			}
		}
		StringBuilder outputHeaderSb = new StringBuilder();
		StringBuilder outputHeaderIncludesSb = new StringBuilder();
		List<String> cmakeClassFiles = new ArrayList<>();
		List<String> cmakeMainFiles = new ArrayList<>();
		cmakeMainFiles.add("native_jvm.hpp");
		cmakeMainFiles.add("native_jvm.cpp");
		cmakeMainFiles.add("native_jvm_output.hpp");
		cmakeMainFiles.add("native_jvm_output.cpp");
		cmakeMainFiles.add("string_pool.hpp");
		cmakeMainFiles.add("string_pool.cpp");
		String projectName = "native_jvm_classes_" + inputJar.getFileName().toString().replaceAll("[$#\\.\\s\\/]", "_")
				+ "_" + Math.abs(new Random().nextLong());
		try (final JarFile f = new JarFile(jar);
				final ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(outputDir.resolve(jar.getName()),
						StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
			System.out.println("Processing " + jar + "...");

			while (true) {
				final int currentNativeDirId = nativeDirId;
				if (!f.stream().anyMatch(x -> x.getName().startsWith("native" + currentNativeDirId)))
					break;
				nativeDirId++;
			}

			f.stream().forEach(e -> {
				try {
					if (e.getName().equals(JarFile.MANIFEST_NAME))
						return;
					if (!e.getName().endsWith(".class")) {
						writeEntry(f, out, e);
						return;
					}

					ByteArrayOutputStream baos = new ByteArrayOutputStream();
					try (InputStream in = f.getInputStream(e)) {
						transfer(in, baos);
					}
					byte[] src = baos.toByteArray();
					if (byteArrayToInt(Arrays.copyOfRange(src, 0, 4)) != 0xCAFEBABE) {
						writeEntry(out, e.getName(), src);
						return;
					}
					nativeMethodsSb = new StringBuilder();
					ifaceStaticNativeMethodsSb = new StringBuilder();
					invokeDynamics = new HashMap<>();
					ClassReader classReader = new ClassReader(src);
					ClassNode classNode = new ClassNode(Opcodes.ASM7);
					classReader.accept(classNode, 0);
					if (classNode.methods.stream()
							.filter(x -> (x.access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) == 0
									&& !x.name.equals("<init>"))
							.count() == 0) {
						System.out.println("Skipping " + classNode.name);
						writeEntry(out, e.getName(), src);
						return;
					}
					System.out.println("Processing " + classNode.name);
					if (!classNode.methods.stream().anyMatch(x -> x.name.equals("<clinit>")))
						classNode.methods.add(new MethodNode(Opcodes.ASM7, Opcodes.ACC_STATIC, "<clinit>", "()V", null,
								new String[0]));
					setupNewIfaceStaticClass(forAndroid);
					cachedClasses.clear();
					cachedMethods.clear();
					cachedFields.clear();
					try (BufferedWriter outputCppFile = new BufferedWriter(
							new OutputStreamWriter(new FileOutputStream(outputDir.resolve("cpp").resolve("output")
									.resolve(escapeCppNameString(classNode.name.replace('/', '_')).concat(".cpp"))
									.toFile()), StandardCharsets.UTF_8));
							BufferedWriter outputHppFile = new BufferedWriter(new OutputStreamWriter(
									new FileOutputStream(outputDir.resolve("cpp").resolve("output").resolve(
											escapeCppNameString(classNode.name.replace('/', '_')).concat(".hpp"))
											.toFile()),
									StandardCharsets.UTF_8))) {
						StringBuilder insnsSb = new StringBuilder();
						classNode.sourceFile = escapeCppNameString(classNode.name.replace('/', '_')) + ".cpp";
						for (int i = 0; i < classNode.methods.size(); i++)
							insnsSb.append(visitMethod(classNode, classNode.methods.get(i), i).replace("\n", "\n    "));
						if ((classNode.access & Opcodes.ACC_INTERFACE) > 0)
							for (int i = 0; i < classNode.methods.size(); i++)
								classNode.methods.get(i).access &= ~Opcodes.ACC_NATIVE;
						invokeDynamics.forEach((key, value) -> processIndy(classNode, key, value));
						classNode.version = 52;
						ClassWriter classWriter = new SafeClassWriter(metadataReader,
								Opcodes.ASM7 | ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
						classNode.accept(classWriter);
						writeEntry(out, e.getName(), classWriter.toByteArray());

						outputCppFile.append("#include \"../native_jvm.hpp\"\n");
						outputHppFile.append("#include \"../native_jvm.hpp\"\n");
						outputCppFile.append("#include \"../string_pool.hpp\"\n");
						outputCppFile.append("#include \"")
								.append(escapeCppNameString(classNode.name.replace('/', '_')).concat(".hpp"))
								.append("\"\n");
						cmakeClassFiles.add("output/" + escapeCppNameString(classNode.name.replace('/', '_')) + ".hpp");
						cmakeClassFiles.add("output/" + escapeCppNameString(classNode.name.replace('/', '_')) + ".cpp");
						outputHeaderIncludesSb.append("#include \"output/")
								.append(escapeCppNameString(classNode.name.replace('/', '_')).concat(".hpp"))
								.append("\"\n");
						outputCppFile.append("\n");
						outputCppFile.append("// ").append(classNode.name).append("\n");
						outputCppFile.append("namespace native_jvm::classes::__ngen_")
								.append(escapeCppNameString(classNode.name.replace("/", "_"))).append(" {\n\n");
						outputCppFile.append("    char *string_pool;\n\n");
						if (cachedClasses.size() > 0)
							outputCppFile.append("    jclass cclasses[" + cachedClasses.size() + "];\n");
						if (cachedMethods.size() > 0)
							outputCppFile
									.append("    std::atomic<jmethodID> cmethods[" + cachedMethods.size() + "];\n");
						if (cachedFields.size() > 0)
							outputCppFile.append("    std::atomic<jfieldID> cfields[" + cachedFields.size() + "];\n");
						outputCppFile.append("\n");
						outputHppFile.append("\n");
						outputHppFile.append("#ifndef ").append(
								escapeCppNameString(classNode.name.replace('/', '_')).concat("_hpp").toUpperCase())
								.append("_GUARD\n");
						outputHppFile.append("\n");
						outputHppFile.append("#define ").append(
								escapeCppNameString(classNode.name.replace('/', '_')).concat("_hpp").toUpperCase())
								.append("_GUARD\n");
						outputHppFile.append("\n");
						outputHppFile.append("// ").append(classNode.name).append("\n");
						outputHppFile.append("namespace native_jvm::classes::__ngen_")
								.append(escapeCppNameString(classNode.name.replace("/", "_"))).append(" {\n\n");
						outputCppFile.append("    ");
						outputCppFile.append(insnsSb);
						outputCppFile.append("\n");
						outputCppFile.append("    void __ngen_register_methods(JNIEnv *env, jvmtiEnv *jvmti_env) {\n");
						outputHppFile.append("    void __ngen_register_methods(JNIEnv *env, jvmtiEnv *jvmti_env);\n");
						outputCppFile.append("        string_pool = string_pool::get_pool();\n\n");

						for (Map.Entry<String, Integer> clazz : cachedClasses.entrySet())
							outputCppFile.append("        if (jclass clazz = ").append(getGetterForType(clazz.getKey()))
									.append(") { cclasses[" + clazz.getValue()
											+ "] = (jclass) env->NewGlobalRef(clazz); env->DeleteLocalRef(clazz); }\n");
						if (!cachedClasses.isEmpty())
							outputCppFile.append("\n");

						if (nativeMethodsSb.length() > 0) {
							outputCppFile.append("        JNINativeMethod __ngen_methods[] = {\n");
							outputCppFile.append(nativeMethodsSb);
							outputCppFile.append("        };\n\n");
							outputCppFile.append("        jclass clazz = ").append(getGetterForType(classNode.name))
									.append(";\n");
							outputCppFile.append(
									"        if (clazz) env->RegisterNatives(clazz, __ngen_methods, sizeof(__ngen_methods) / sizeof(__ngen_methods[0]));\n");
							outputCppFile.append(
									"        if (env->ExceptionCheck()) { fprintf(stderr, \"Exception occured while registering native_jvm for %s\\n\", ")
									.append(getStringPooledString(classNode.name.replace("/", ".")))
									.append("); fflush(stderr); env->ExceptionDescribe(); env->ExceptionClear(); }\n");
							outputCppFile.append("\n");
						}
						if (ifaceStaticNativeMethodsSb.length() > 0) {
							outputCppFile.append("        JNINativeMethod __ngen_static_iface_methods[] = {\n");
							outputCppFile.append(ifaceStaticNativeMethodsSb);
							outputCppFile.append("        };\n\n");
							outputCppFile.append("        jclass clazz = utils::find_class_wo_static(env, ")
									.append(getStringPooledString(currentIfaceStaticClass.name.replace("/", ".")))
									.append(");\n");
							outputCppFile.append(
									"        if (clazz) env->RegisterNatives(clazz, __ngen_static_iface_methods, sizeof(__ngen_static_iface_methods) / sizeof(__ngen_static_iface_methods[0]));\n");
							outputCppFile.append(
									"        if (env->ExceptionCheck()) { fprintf(stderr, \"Exception occured while registering native_jvm for %s\\n\", ")
									.append(getStringPooledString(classNode.name.replace("/", ".")))
									.append("); fflush(stderr); env->ExceptionDescribe(); env->ExceptionClear(); }\n");
						}
						outputCppFile.append("    }\n");
						outputCppFile.append("}");
						outputHppFile.append("}\n\n#endif");
						outputHeaderSb.append("        reg_methods[").append(currentClassId)
								.append("] = &(native_jvm::classes::__ngen_")
								.append(escapeCppNameString(classNode.name.replace("/", "_")))
								.append("::__ngen_register_methods);\n");
					}
					currentClassId++;
				} catch (IOException e1) {
					e1.printStackTrace(System.err);
				}
			});
			Manifest mf = f.getManifest();
			setupNewIfaceStaticClass(forAndroid);
			for (ClassNode ifaceStaticClass : readyIfaceStaticClasses) {
				ClassWriter classWriter = new SafeClassWriter(metadataReader,
						Opcodes.ASM7 | ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
				ifaceStaticClass.accept(classWriter);
				writeEntry(out, ifaceStaticClass.name + ".class", classWriter.toByteArray());
			}
			ClassNode loaderClass = new ClassNode();
			loaderClass.sourceFile = "synthetic";
			loaderClass.name = "native" + nativeDirId + "/Loader";
			if (forAndroid) { // Android
				loaderClass.version = 50;
			} else {
				loaderClass.version = 52;
			}

			loaderClass.superName = "java/lang/Object";
			loaderClass.access = Opcodes.ACC_PUBLIC;

			if (forAndroid) { // Android
				MethodNode mainMethod = new MethodNode(Opcodes.ASM7, Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
				mainMethod.instructions.add(new LdcInsnNode(projectName));
				mainMethod.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "loadLibrary",
						"(Ljava/lang/String;)V"));
				mainMethod.instructions.add(new InsnNode(Opcodes.RETURN));
				loaderClass.methods.add(mainMethod);
			}

			MethodNode registerNativesForClassMethod = new MethodNode(Opcodes.ASM7,
					Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_NATIVE, "registerNativesForClass", "(I)V",
					null, new String[0]);
			loaderClass.methods.add(registerNativesForClassMethod);
			ClassWriter classWriter = new SafeClassWriter(metadataReader,
					Opcodes.ASM7 | ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
			loaderClass.accept(classWriter);
			writeEntry(out, "native" + nativeDirId + "/Loader.class", classWriter.toByteArray());
			System.out.println("Jar file ready!");
			if (!forAndroid) { // Android
				String mainClass = (String) mf.getMainAttributes().get(Name.MAIN_CLASS);
				if (mainClass != null) {
					System.out.println("Creating bootstrap classes...");
					mf.getMainAttributes().put(Name.MAIN_CLASS, "native" + nativeDirId + "/Bootstrap");
					ClassNode bootstrapClass = new ClassNode(Opcodes.ASM7);
					bootstrapClass.sourceFile = "synthetic";
					bootstrapClass.name = "native" + nativeDirId + "/Bootstrap";
					bootstrapClass.version = 52;
					bootstrapClass.superName = "java/lang/Object";
					bootstrapClass.access = Opcodes.ACC_PUBLIC;
					MethodNode mainMethod = new MethodNode(Opcodes.ASM7, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
							"main", "([Ljava/lang/String;)V", null, new String[0]);
					mainMethod.instructions.add(new LdcInsnNode(projectName));
					mainMethod.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System",
							"loadLibrary", "(Ljava/lang/String;)V"));
					mainMethod.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
					mainMethod.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, mainClass.replace(".", "/"),
							"main", "([Ljava/lang/String;)V"));
					mainMethod.instructions.add(new InsnNode(Opcodes.RETURN));
					bootstrapClass.methods.add(mainMethod);
					bootstrapClass.accept(classWriter);
					writeEntry(out, "native" + nativeDirId + "/Bootstrap.class", classWriter.toByteArray());
					System.out.println("Created!");
				} else {
					System.out.println("Main-Class not found - no bootstrap classes!");
				}
				out.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
				mf.write(out);
			}
			out.closeEntry();
			metadataReader.close();
		}

		TreeMap<Integer, String> stringPoolSorted = new TreeMap<>();
		stringPool.entrySet().forEach((string) -> {
			stringPoolSorted.put(string.getValue(), string.getKey());
		});
		List<Byte> stringPoolResult = new ArrayList<>();
		stringPoolSorted.entrySet().forEach((string) -> {
			for (byte b : string.getValue().getBytes(StandardCharsets.UTF_8))
				stringPoolResult.add(b);
			stringPoolResult.add((byte) 0);
		});
		try (InputStream in = NativeObfuscator.class.getClassLoader().getResourceAsStream("sources/string_pool.cpp")) {
			StringBuilder spValue = new StringBuilder("{ ");
			for (int i = 0; i < stringPoolResult.size(); i++)
				spValue.append(stringPoolResult.get(i)).append(i == stringPoolResult.size() - 1 ? "" : ", ");
			spValue.append(" }");
			Files.write(outputDir.resolve("cpp").resolve("string_pool.cpp"),
					dynamicFormat(writeStreamToString(in),
							createMap("size", stringPoolResult.size() + "LL", "value", spValue.toString()))
									.getBytes(StandardCharsets.UTF_8),
					StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
		}

		if (!forAndroid) {
			try (InputStream in = NativeObfuscator.class.getClassLoader()
					.getResourceAsStream("sources/native_jvm_output.cpp")) {
				Files.write(outputDir.resolve("cpp").resolve("native_jvm_output.cpp"),
						dynamicFormat(writeStreamToString(in),
								createMap("register_code", outputHeaderSb, "includes", outputHeaderIncludesSb,
										"native_dir_id", nativeDirId, "class_count", currentClassId))
												.getBytes(StandardCharsets.UTF_8),
						StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
			}

			try (InputStream in = NativeObfuscator.class.getClassLoader()
					.getResourceAsStream("sources/CMakeLists.txt")) {
				Files.write(outputDir.resolve("cpp").resolve("CMakeLists.txt"),
						dynamicFormat(writeStreamToString(in),
								createMap("classfiles", String.join(" ", cmakeClassFiles), "mainfiles",
										String.join(" ", cmakeMainFiles), "projectname", projectName))
												.getBytes(StandardCharsets.UTF_8),
						StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
			}
		} else { // Android
			try (InputStream in = NativeObfuscator.class.getClassLoader()
					.getResourceAsStream("sources/android/native_jvm_output.cpp")) {
				Files.write(outputDir.resolve("cpp").resolve("native_jvm_output.cpp"),
						dynamicFormat(writeStreamToString(in),
								createMap("register_code", outputHeaderSb, "includes", outputHeaderIncludesSb,
										"native_dir_id", nativeDirId, "class_count", currentClassId))
												.getBytes(StandardCharsets.UTF_8),
						StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
			}

			try (InputStream in = NativeObfuscator.class.getClassLoader()
					.getResourceAsStream("sources/android/CMakeLists.txt")) {
				Files.write(outputDir.resolve("cpp").resolve("CMakeLists.txt"),
						dynamicFormat(writeStreamToString(in),
								createMap("classfiles", String.join(" ", cmakeClassFiles), "mainfiles",
										String.join(" ", cmakeMainFiles), "projectname", projectName))
												.getBytes(StandardCharsets.UTF_8),
						StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
			}
		}
	}

	private static void writeEntry(JarFile f, ZipOutputStream out, JarEntry e) throws IOException {
		out.putNextEntry(new JarEntry(e.getName()));
		try (InputStream in = f.getInputStream(e)) {
			transfer(in, out);
		}
		out.closeEntry();
	}

	private static void writeEntry(ZipOutputStream out, String entryName, byte[] data) throws IOException {
		out.putNextEntry(new JarEntry(entryName));
		out.write(data, 0, data.length);
		out.closeEntry();
	}

	private static void transfer(InputStream in, OutputStream out) throws IOException {
		byte[] buffer = new byte[4096];
		for (int r = in.read(buffer, 0, 4096); r != -1; r = in.read(buffer, 0, 4096)) {
			out.write(buffer, 0, r);
		}
	}

	private static int byteArrayToInt(byte[] b) {
		if (b.length == 4) {
			return b[0] << 24 | (b[1] & 0xff) << 16 | (b[2] & 0xff) << 8 | (b[3] & 0xff);
		} else if (b.length == 2) {
			return (b[0] & 0xff) << 8 | (b[1] & 0xff);
		}

		return 0;
	}
}