package jadx.core.utils;

import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IBlock;
import jadx.core.dex.nodes.IBranchRegion;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.trycatch.TryCatchBlock;
import jadx.core.utils.exceptions.JadxRuntimeException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class RegionUtils {

	private RegionUtils() {
	}

	public static boolean hasExitEdge(IContainer container) {
		if (container instanceof IBlock) {
			InsnNode lastInsn = BlockUtils.getLastInsn((IBlock) container);
			if (lastInsn == null) {
				return false;
			}
			InsnType type = lastInsn.getType();
			return type == InsnType.RETURN
					|| type == InsnType.CONTINUE
					|| type == InsnType.BREAK
					|| type == InsnType.THROW;
		} else if (container instanceof IBranchRegion) {
			for (IContainer br : ((IBranchRegion) container).getBranches()) {
				if (br == null || !hasExitEdge(br)) {
					return false;
				}
			}
			return true;
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			List<IContainer> blocks = region.getSubBlocks();
			return !blocks.isEmpty() && hasExitEdge(blocks.get(blocks.size() - 1));
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static InsnNode getLastInsn(IContainer container) {
		if (container instanceof IBlock) {
			IBlock block = (IBlock) container;
			List<InsnNode> insnList = block.getInstructions();
			if (insnList.isEmpty()) {
				return null;
			}
			return insnList.get(insnList.size() - 1);
		} else if (container instanceof IBranchRegion) {
			return null;
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			List<IContainer> blocks = region.getSubBlocks();
			if (blocks.isEmpty()) {
				return null;
			}
			return getLastInsn(blocks.get(blocks.size() - 1));
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	/**
	 * Return true if last block in region has no successors
	 */
	public static boolean hasExitBlock(IContainer container) {
		if (container instanceof BlockNode) {
			return ((BlockNode) container).getSuccessors().isEmpty();
		} else if (container instanceof IBlock) {
			return true;
		} else if (container instanceof IRegion) {
			List<IContainer> blocks = ((IRegion) container).getSubBlocks();
			return !blocks.isEmpty()
					&& hasExitBlock(blocks.get(blocks.size() - 1));
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static boolean hasBreakInsn(IContainer container) {
		if (container instanceof IBlock) {
			return BlockUtils.checkLastInsnType((IBlock) container, InsnType.BREAK);
		} else if (container instanceof IRegion) {
			List<IContainer> blocks = ((IRegion) container).getSubBlocks();
			return !blocks.isEmpty()
					&& hasBreakInsn(blocks.get(blocks.size() - 1));
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container);
		}
	}

	public static int insnsCount(IContainer container) {
		if (container instanceof IBlock) {
			return ((IBlock) container).getInstructions().size();
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			int count = 0;
			for (IContainer block : region.getSubBlocks()) {
				count += insnsCount(block);
			}
			return count;
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static boolean isEmpty(IContainer container) {
		return !notEmpty(container);
	}

	public static boolean notEmpty(IContainer container) {
		if (container instanceof IBlock) {
			return !((IBlock) container).getInstructions().isEmpty();
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			for (IContainer block : region.getSubBlocks()) {
				if (notEmpty(block)) {
					return true;
				}
			}
			return false;
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static void getAllRegionBlocks(IContainer container, Set<IBlock> blocks) {
		if (container instanceof IBlock) {
			blocks.add((IBlock) container);
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			for (IContainer block : region.getSubBlocks()) {
				getAllRegionBlocks(block, blocks);
			}
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static boolean isRegionContainsBlock(IContainer container, BlockNode block) {
		if (container instanceof IBlock) {
			return container == block;
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			for (IContainer b : region.getSubBlocks()) {
				if (isRegionContainsBlock(b, block)) {
					return true;
				}
			}
			return false;
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static List<IContainer> getExcHandlersForRegion(IContainer region) {
		CatchAttr cb = region.get(AType.CATCH_BLOCK);
		if (cb != null) {
			TryCatchBlock tb = cb.getTryBlock();
			List<IContainer> list = new ArrayList<IContainer>(tb.getHandlersCount());
			for (ExceptionHandler eh : tb.getHandlers()) {
				list.add(eh.getHandlerRegion());
			}
			return list;
		}
		return Collections.emptyList();
	}

	private static boolean isRegionContainsExcHandlerRegion(IContainer container, IRegion region) {
		if (container == region) {
			return true;
		}
		if (container instanceof IRegion) {
			IRegion r = (IRegion) container;

			// process sub blocks
			for (IContainer b : r.getSubBlocks()) {
				// process try block
				CatchAttr cb = b.get(AType.CATCH_BLOCK);
				if (cb != null && b instanceof IRegion) {
					TryCatchBlock tb = cb.getTryBlock();
					for (ExceptionHandler eh : tb.getHandlers()) {
						if (isRegionContainsRegion(eh.getHandlerRegion(), region)) {
							return true;
						}
					}
				}
				if (isRegionContainsRegion(b, region)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Check if {@code region} contains in {@code container}.
	 * <br>
	 * For simple region (not from exception handlers) search in parents
	 * otherwise run recursive search because exception handlers can have several parents
	 */
	public static boolean isRegionContainsRegion(IContainer container, IRegion region) {
		if (container == region) {
			return true;
		}
		if (region == null) {
			return false;
		}
		IRegion parent = region.getParent();
		while (container != parent) {
			if (parent == null) {
				if (region.contains(AType.EXC_HANDLER)) {
					return isRegionContainsExcHandlerRegion(container, region);
				}
				return false;
			}
			region = parent;
			parent = region.getParent();
		}
		return true;
	}

	public static IContainer getBlockContainer(IContainer container, BlockNode block) {
		if (container instanceof IBlock) {
			return container == block ? container : null;
		} else if (container instanceof IRegion) {
			IRegion region = (IRegion) container;
			for (IContainer c : region.getSubBlocks()) {
				IContainer res = getBlockContainer(c, block);
				if (res != null) {
					return res instanceof IBlock ? region : res;
				}
			}
			return null;
		} else {
			throw new JadxRuntimeException("Unknown container type: " + container.getClass());
		}
	}

	public static boolean isDominatedBy(BlockNode dom, IContainer cont) {
		if (dom == cont) {
			return true;
		}
		if (cont instanceof BlockNode) {
			BlockNode block = (BlockNode) cont;
			return block.isDominator(dom);
		} else if (cont instanceof IBlock) {
			return false;
		} else if (cont instanceof IRegion) {
			IRegion region = (IRegion) cont;
			for (IContainer c : region.getSubBlocks()) {
				if (!isDominatedBy(dom, c)) {
					return false;
				}
			}
			return true;
		} else {
			throw new JadxRuntimeException("Unknown container type: " + cont.getClass());
		}
	}

	public static boolean hasPathThruBlock(BlockNode block, IContainer cont) {
		if (block == cont) {
			return true;
		}
		if (cont instanceof BlockNode) {
			return BlockUtils.isPathExists(block, (BlockNode) cont);
		} else if (cont instanceof IBlock) {
			return false;
		} else if (cont instanceof IRegion) {
			IRegion region = (IRegion) cont;
			for (IContainer c : region.getSubBlocks()) {
				if (!hasPathThruBlock(block, c)) {
					return false;
				}
			}
			return true;
		} else {
			throw new JadxRuntimeException("Unknown container type: " + cont.getClass());
		}
	}

}
