package ch.usi.inf.sp.callgraph;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Objects;

import ch.usi.inf.sp.framework.ClassAnalyzer;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;


/**
 * Build a call graph (as part of the class hierarchy)
 * consisting of CallSite nodes pointing to Method nodes.
 *
 * @author maggicl@usi.ch
 * @author Matthias.Hauswirth@usi.ch
 */
public final class CallGraphBuilder implements ClassAnalyzer {

    private final ClassHierarchy hierarchy;


    public CallGraphBuilder(final ClassHierarchy hierarchy) {
        this.hierarchy = hierarchy;
    }

    public void analyze(final String location, final ClassNode classNode) {
        try {
            final ClassType type = hierarchy.getOrCreateClass(classNode.name);
            final List<MethodNode> methodNodes = classNode.methods;
            for (final MethodNode methodNode : methodNodes) {
                final Method method = type.getMethod(methodNode.name, methodNode.desc);
                Objects.requireNonNull(method, "method is null");
                final InsnList instructions = methodNode.instructions;
                for (int i = 0; i < instructions.size(); i++) {
                    final AbstractInsnNode insn = instructions.get(i);

                    // invokedynamic deliberately ignored
                    if (insn.getType() == AbstractInsnNode.METHOD_INSN) {
                        final MethodInsnNode methodInsn = (MethodInsnNode) insn;
                        final int opCode = methodInsn.getOpcode();

                        final CallSite callSite = new CallSite(insn.getOpcode(), methodInsn.owner, methodInsn.name,
                                methodInsn.desc);
                        method.addCallSite(callSite);

                        final String targetClazz = callSite.getDeclaredTargetClassName();
                        final ClassType clazz = hierarchy.getOrCreateClass(targetClazz);
                        // ignoring if a class is unresolved on purpose, as analysis is constrained on user-defined
                        // classes (i.e. the contents of pacman-src.jar)

                        if (opCode == Opcodes.INVOKESTATIC || opCode == Opcodes.INVOKESPECIAL) {
                            // target is static, no fancy search needed
                            callSite.addPossibleTargetClass(clazz); // Cave Johnson, we're done here
                        } else if (opCode == Opcodes.INVOKEVIRTUAL) {
                            final Deque<ClassType> subClasses = new ArrayDeque<>();
                            subClasses.add(clazz);

                            // BFS over class and subclasses and add them all as possible targets
                            while (!subClasses.isEmpty()) {
                                final ClassType subClazz = subClasses.pop();
                                if (!subClazz.isAbstract()) {
                                    callSite.addPossibleTargetClass(subClazz);
                                }
                                subClasses.addAll(subClazz.getSubTypes());
                            }
                        } else { // opCode == Opcodes.INVOKEINTERFACE
                            // brute force our way through the type hierarchy to find compatible classes which may
                            // implement the interface

                            for (final Type aType : hierarchy.getTypes()) {
                                if (aType instanceof ClassType) {
                                    final ClassType aClazz = (ClassType) aType;

                                    if (aClazz.getInterfaces().contains(clazz)) {
                                        callSite.addPossibleTargetClass(aClazz);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (final TypeInconsistencyException ex) {
            System.err.println(ex);
        }
    }

}
