package jadx.core.utils;

import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.PhiInsn;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper class for correct instructions removing,
 * can be used while iterating over instructions list
 */
public class InstructionRemover {

	private static final Logger LOG = LoggerFactory.getLogger(InstructionRemover.class);

	private final MethodNode mth;
	private final List<InsnNode> toRemove;
	private List<InsnNode> instrList;

	public InstructionRemover(MethodNode mth) {
		this(mth, null);
	}

	public InstructionRemover(MethodNode mth, BlockNode block) {
		this.mth = mth;
		this.toRemove = new ArrayList<InsnNode>();
		if (block != null) {
			this.instrList = block.getInstructions();
		}
	}

	public void setBlock(BlockNode block) {
		this.instrList = block.getInstructions();
	}

	public void add(InsnNode insn) {
		toRemove.add(insn);
	}

	public void perform() {
		if (toRemove.isEmpty()) {
			return;
		}
		removeAll(mth, instrList, toRemove);
		toRemove.clear();
	}

	public static void unbindInsnList(MethodNode mth, List<InsnNode> unbind) {
		for (InsnNode rem : unbind) {
			unbindInsn(mth, rem);
		}
	}

	public static void unbindInsn(MethodNode mth, InsnNode insn) {
		unbindResult(mth, insn);
		for (InsnArg arg : insn.getArguments()) {
			unbindArgUsage(mth, arg);
		}
		if (insn.getType() == InsnType.PHI) {
			for (InsnArg arg : insn.getArguments()) {
				if (arg instanceof RegisterArg) {
					fixUsedInPhiFlag((RegisterArg) arg);
				}
			}
		}
		insn.add(AFlag.INCONSISTENT_CODE);
	}

	public static void fixUsedInPhiFlag(RegisterArg useReg) {
		PhiInsn usedIn = null;
		for (RegisterArg reg : useReg.getSVar().getUseList()) {
			InsnNode parentInsn = reg.getParentInsn();
			if (parentInsn != null
					&& parentInsn.getType() == InsnType.PHI
					&& parentInsn.containsArg(useReg)) {
				usedIn = (PhiInsn) parentInsn;
			}
		}
		useReg.getSVar().setUsedInPhi(usedIn);
	}

	public static void unbindResult(MethodNode mth, InsnNode insn) {
		RegisterArg r = insn.getResult();
		if (r != null && r.getSVar() != null) {
			mth.removeSVar(r.getSVar());
		}
	}

	public static void unbindArgUsage(MethodNode mth, InsnArg arg) {
		if (arg instanceof RegisterArg) {
			RegisterArg reg = (RegisterArg) arg;
			SSAVar sVar = reg.getSVar();
			if (sVar != null) {
				sVar.removeUse(reg);
			}
		} else if (arg instanceof InsnWrapArg) {
			InsnWrapArg wrap = (InsnWrapArg) arg;
			unbindInsn(mth, wrap.getWrapInsn());
		}
	}

	// Don't use 'instrList.removeAll(toRemove)' because it will remove instructions by content
	// and here can be several instructions with same content
	private static void removeAll(MethodNode mth, List<InsnNode> insns, List<InsnNode> toRemove) {
		for (InsnNode rem : toRemove) {
			unbindInsn(mth, rem);
			int insnsCount = insns.size();
			for (int i = 0; i < insnsCount; i++) {
				if (insns.get(i) == rem) {
					insns.remove(i);
					break;
				}
			}
		}
	}

	public static void remove(MethodNode mth, InsnNode insn) {
		BlockNode block = BlockUtils.getBlockByInsn(mth, insn);
		if (block != null) {
			remove(mth, block, insn);
		}
	}

	public static void remove(MethodNode mth, BlockNode block, InsnNode insn) {
		unbindInsn(mth, insn);
		// remove by pointer (don't use equals)
		Iterator<InsnNode> it = block.getInstructions().iterator();
		while (it.hasNext()) {
			InsnNode ir = it.next();
			if (ir == insn) {
				it.remove();
				return;
			}
		}
	}

	public static void removeAll(MethodNode mth, BlockNode block, List<InsnNode> insns) {
		if (insns.isEmpty()) {
			return;
		}
		removeAll(mth, block.getInstructions(), insns);
	}

	public static void remove(MethodNode mth, BlockNode block, int index) {
		List<InsnNode> instructions = block.getInstructions();
		unbindInsn(mth, instructions.get(index));
		instructions.remove(index);
	}

}
