package jadx.core.codegen;

import jadx.core.Consts;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.NamedArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class NameGen {

	private static final Map<String, String> OBJ_ALIAS;

	private final Set<String> varNames = new HashSet<String>();
	private final MethodNode mth;
	private final boolean fallback;

	static {
		OBJ_ALIAS = new HashMap<String, String>();
		OBJ_ALIAS.put(Consts.CLASS_STRING, "str");
		OBJ_ALIAS.put(Consts.CLASS_CLASS, "cls");
		OBJ_ALIAS.put(Consts.CLASS_THROWABLE, "th");
		OBJ_ALIAS.put(Consts.CLASS_OBJECT, "obj");
		OBJ_ALIAS.put("java.util.Iterator", "it");
		OBJ_ALIAS.put("java.lang.Boolean", "bool");
		OBJ_ALIAS.put("java.lang.Short", "sh");
		OBJ_ALIAS.put("java.lang.Integer", "num");
		OBJ_ALIAS.put("java.lang.Character", "ch");
		OBJ_ALIAS.put("java.lang.Byte", "b");
		OBJ_ALIAS.put("java.lang.Float", "f");
		OBJ_ALIAS.put("java.lang.Long", "l");
		OBJ_ALIAS.put("java.lang.Double", "d");
	}

	public NameGen(MethodNode mth, boolean fallback) {
		this.mth = mth;
		this.fallback = fallback;
	}

	public String assignArg(RegisterArg arg) {
		String name = makeArgName(arg);
		if (fallback) {
			return name;
		}
		name = getUniqueVarName(name);
		arg.setName(name);
		return name;
	}

	public String assignNamedArg(NamedArg arg) {
		String name = arg.getName();
		if (fallback) {
			return name;
		}
		name = getUniqueVarName(name);
		arg.setName(name);
		return name;
	}

	public String useArg(RegisterArg arg) {
		String name = arg.getName();
		if (name == null || fallback) {
			return getFallbackName(arg);
		}
		return name;
	}

	// TODO: avoid name collision with variables names
	public String getLoopLabel(LoopLabelAttr attr) {
		String name = "loop" + attr.getLoop().getId();
		varNames.add(name);
		return name;
	}

	private String getUniqueVarName(String name) {
		String r = name;
		int i = 2;
		while (varNames.contains(r)) {
			r = name + i;
			i++;
		}
		varNames.add(r);
		return r;
	}

	private String makeArgName(RegisterArg arg) {
		if (fallback) {
			return getFallbackName(arg);
		}
		String name = arg.getName();
		String varName;
		if (name != null) {
			if ("this".equals(name)) {
				return name;
			}
			varName = name;
		} else {
			varName = guessName(arg);
		}
		if (NameMapper.isReserved(varName)) {
			return varName + "R";
		}
		return varName;
	}

	private String getFallbackName(RegisterArg arg) {
		return "r" + arg.getRegNum();
	}

	private String guessName(RegisterArg arg) {
		SSAVar sVar = arg.getSVar();
		if (sVar != null && sVar.getName() == null) {
			RegisterArg assignArg = sVar.getAssign();
			InsnNode assignInsn = assignArg.getParentInsn();
			if (assignInsn != null) {
				String name = makeNameFromInsn(assignInsn);
				if (name != null && !NameMapper.isReserved(name)) {
					assignArg.setName(name);
					return name;
				}
			}
		}
		return makeNameForType(arg.getType());
	}

	private String makeNameForType(ArgType type) {
		if (type.isPrimitive()) {
			return makeNameForPrimitive(type);
		} else if (type.isArray()) {
			return makeNameForType(type.getArrayRootElement()) + "Arr";
		} else {
			return makeNameForObject(type);
		}
	}

	private static String makeNameForPrimitive(ArgType type) {
		return type.getPrimitiveType().getShortName().toLowerCase();
	}

	private String makeNameForObject(ArgType type) {
		if (type.isObject()) {
			String alias = getAliasForObject(type.getObject());
			if (alias != null) {
				return alias;
			}
			ClassInfo extClsInfo = ClassInfo.extCls(mth.dex(), type);
			String shortName = extClsInfo.getShortName();
			String vName = fromName(shortName);
			if (vName != null) {
				return vName;
			}
		}
		return Utils.escape(type.toString());
	}

	private static String fromName(String name) {
		if (name == null || name.isEmpty()) {
			return null;
		}
		if (name.toUpperCase().equals(name)) {
			// all characters are upper case
			return name.toLowerCase();
		}
		String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
		if (!v1.equals(name)) {
			return v1;
		}
		if (name.length() < 3) {
			return name + "Var";
		}
		return null;
	}

	private static String getAliasForObject(String name) {
		return OBJ_ALIAS.get(name);
	}

	private String makeNameFromInsn(InsnNode insn) {
		switch (insn.getType()) {
			case INVOKE:
				InvokeNode inv = (InvokeNode) insn;
				return makeNameFromInvoke(inv.getCallMth());

			case CONSTRUCTOR:
				ConstructorInsn co = (ConstructorInsn) insn;
				return makeNameForObject(co.getClassType().getType());

			case ARRAY_LENGTH:
				return "length";

			case ARITH:
			case TERNARY:
			case CAST:
				for (InsnArg arg : insn.getArguments()) {
					if (arg.isInsnWrap()) {
						InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
						String wName = makeNameFromInsn(wrapInsn);
						if (wName != null) {
							return wName;
						}
					}
				}
				break;

			default:
				break;
		}
		return null;
	}

	private String makeNameFromInvoke(MethodInfo callMth) {
		String name = callMth.getName();
		if (name.startsWith("get") || name.startsWith("set")) {
			return fromName(name.substring(3));
		}
		ArgType declType = callMth.getDeclClass().getAlias().getType();
		if ("iterator".equals(name)) {
			return "it";
		}
		if ("toString".equals(name)) {
			return makeNameForType(declType);
		}
		if ("forName".equals(name) && declType.equals(ArgType.CLASS)) {
			return OBJ_ALIAS.get(Consts.CLASS_CLASS);
		}
		return name;
	}
}
