package jadx.core.utils.android;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.api.ICodeWriter;
import jadx.api.plugins.input.data.AccessFlags;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.core.codegen.ClassGen;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.fldinit.FieldInitAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.ProcessState;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;

/**
 * Android resources specific handlers
 */
public class AndroidResourcesUtils {
	private static final Logger LOG = LoggerFactory.getLogger(AndroidResourcesUtils.class);

	private AndroidResourcesUtils() {
	}

	public static ClassNode searchAppResClass(RootNode root, ResourceStorage resStorage) {
		String appPackage = root.getAppPackage();
		String fullName = appPackage != null ? appPackage + ".R" : "R";
		ClassNode resCls = root.resolveClass(fullName);
		if (resCls != null) {
			addResourceFields(resCls, resStorage, true);
			return resCls;
		}
		LOG.info("Can't find 'R' class in app package: {}", appPackage);
		List<ClassNode> candidates = root.searchClassByShortName("R");
		if (candidates.size() == 1) {
			resCls = candidates.get(0);
			addResourceFields(resCls, resStorage, true);
			return resCls;
		}
		if (!candidates.isEmpty()) {
			LOG.info("Found several 'R' class candidates: {}", candidates);
		}
		LOG.info("App 'R' class not found, put all resources ids into : '{}'", fullName);
		ClassNode newResCls = makeClass(root, fullName, resStorage);
		addResourceFields(newResCls, resStorage, false);
		return newResCls;
	}

	public static boolean handleAppResField(ICodeWriter code, ClassGen clsGen, ClassInfo declClass) {
		ClassInfo parentClass = declClass.getParentClass();
		if (parentClass != null && parentClass.getShortName().equals("R")) {
			clsGen.useClass(code, parentClass);
			code.add('.');
			code.add(declClass.getAliasShortName());
			return true;
		}
		return false;
	}

	/**
	 * Force hex format for Android resources ids
	 */
	@SuppressWarnings("RedundantCast")
	public static boolean handleResourceFieldValue(ClassNode cls, ICodeWriter code, EncodedValue encodedValue) {
		if (encodedValue.getType() == EncodedType.ENCODED_INT && isResourceClass(cls)) {
			code.add(String.format("0x%X", ((Integer) encodedValue.getValue())));
			return true;
		}
		return false;
	}

	public static boolean isResourceClass(ClassNode cls) {
		ClassNode parentClass = cls.getParentClass();
		return parentClass != null && parentClass.getShortName().equals("R");
	}

	private static ClassNode makeClass(RootNode root, String clsName, ResourceStorage resStorage) {
		ClassNode rCls = ClassNode.addSyntheticClass(root, clsName, AccessFlags.PUBLIC | AccessFlags.FINAL);
		rCls.addAttr(AType.COMMENTS, "This class is generated by JADX");
		rCls.setState(ProcessState.PROCESS_COMPLETE);
		return rCls;
	}

	private static void addResourceFields(ClassNode resCls, ResourceStorage resStorage, boolean rClsExists) {
		Map<Integer, FieldNode> resFieldsMap = fillResFieldsMap(resCls);
		Map<String, ClassNode> innerClsMap = new TreeMap<>();
		if (rClsExists) {
			for (ClassNode innerClass : resCls.getInnerClasses()) {
				innerClsMap.put(innerClass.getShortName(), innerClass);
			}
		}
		for (ResourceEntry resource : resStorage.getResources()) {
			final String resTypeName = resource.getTypeName();
			ClassNode typeCls = innerClsMap.computeIfAbsent(
					resTypeName,
					name -> addClassForResType(resCls, rClsExists, name));
			final String resName;
			if ("style".equals(resTypeName)) {
				resName = resource.getKeyName().replace('.', '_');
			} else {
				resName = resource.getKeyName();
			}
			FieldNode rField = typeCls.searchFieldByName(resName);
			if (rField == null) {
				FieldInfo rFieldInfo = FieldInfo.from(typeCls.root(), typeCls.getClassInfo(), resName, ArgType.INT);
				rField = new FieldNode(typeCls, rFieldInfo, AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
				EncodedValue value = new EncodedValue(EncodedType.ENCODED_INT, resource.getId());
				rField.addAttr(FieldInitAttr.constValue(value));
				typeCls.getFields().add(rField);
				if (rClsExists) {
					rField.addAttr(AType.COMMENTS, "added by JADX");
				}
			}
			FieldNode fieldNode = resFieldsMap.get(resource.getId());
			if (fieldNode != null
					&& !fieldNode.getName().equals(resName)
					&& NameMapper.isValidAndPrintable(resName)
					&& resCls.root().getArgs().isRenameValid()) {
				fieldNode.add(AFlag.DONT_RENAME);
				fieldNode.getFieldInfo().setAlias(resName);
			}
		}
	}

	@NotNull
	private static ClassNode addClassForResType(ClassNode resCls, boolean rClsExists, String typeName) {
		ClassNode newTypeCls = ClassNode.addSyntheticClass(resCls.root(), resCls.getFullName() + '$' + typeName,
				AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
		resCls.addInnerClass(newTypeCls);
		if (rClsExists) {
			newTypeCls.addAttr(AType.COMMENTS, "Added by JADX");
		}
		return newTypeCls;
	}

	@NotNull
	private static Map<Integer, FieldNode> fillResFieldsMap(ClassNode resCls) {
		Map<Integer, FieldNode> resFieldsMap = new HashMap<>();
		ConstStorage constStorage = resCls.root().getConstValues();
		Map<Object, FieldNode> constFields = constStorage.getGlobalConstFields();
		for (Map.Entry<Object, FieldNode> entry : constFields.entrySet()) {
			Object key = entry.getKey();
			FieldNode field = entry.getValue();
			AccessInfo accessFlags = field.getAccessFlags();
			if (field.getType().equals(ArgType.INT)
					&& accessFlags.isStatic()
					&& accessFlags.isFinal()
					&& key instanceof Integer) {
				resFieldsMap.put((Integer) key, field);
			}
		}
		return resFieldsMap;
	}
}
