package accrue.analysis.pointer;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import polyglot.types.ClassType;
import polyglot.types.CodeInstance;
import polyglot.types.ProcedureInstance;
import polyglot.types.Type;
import polyglot.types.TypeSystem;
import polyglot.util.InternalCompilerError;
import polyglot.util.Position;
import accrue.analysis.pointer.PointsToEngine.StmtAndContext;
import accrue.analysis.pointer.StmtRegistrar.CodeInfo;
import accrue.analysis.pointer.StmtRegistrar.ExceptionHandlerReferenceVariable;
import accrue.analysis.pointer.analyses.HeapAbstractionFactory;

public abstract class StmtProcedureCall extends PointsToStmt {
    protected final CallSiteNode callSiteNode;

    public StmtProcedureCall(Position origin, CodeInstance code,
            CallSiteNode callSiteNode) {
        super(origin, code, null, null, false);
        this.callSiteNode = callSiteNode;
    }

    protected static boolean addEdgesForCall(CodeInstance caller,
            CContext callerContext, CallSiteNode callSiteNode,
            StmtRegistrar registrar, PointsToGraph g, CodeInfo calleeProcInfo,
            HContext receiver, List<ReferenceVariableReplica> args,
            ReferenceVariableReplica l,
            List<ExceptionHandlerPointsToGraphNode> handlers,
            Map<Type, ReferenceVariableReplica> procThrows, Position origin,
            HeapAbstractionFactory af, StmtAndContext sac) {

        CContext calleeContext =
                af.merge(callSiteNode, receiver, callerContext);

        boolean changed = false;
        ProcedureInstance pi = (ProcedureInstance) calleeProcInfo.codeInstance;
        TypeSystem ts = pi.typeSystem();

        // record that we are calling this procedure
        changed |=
                g.addCall(sac, caller, callerContext, pi, calleeContext, origin);

        // add edge for the receiver node
        if (receiver != null) {
            changed |=
                    g.addEdge(sac,
                              ReferenceVariableReplica.create(calleeContext,
                                                              calleeProcInfo.thisNode,
                                                              af),
                              receiver,
                              origin);
        }

        // add edges for the args
        for (int i = 0; i < args.size(); i++) {
            ReferenceVariableReplica ra = args.get(i);
            PointsToGraphNode pa =
                    ReferenceVariableReplica.create(calleeContext,
                                                    calleeProcInfo.formals.get(i),
                                                    af);
            changed |= g.addEdges(sac, pa, g.pointsTo(sac, ra), origin);
        }

        // add edges for the return value
        if (l != null && calleeProcInfo.retValue != null) {
            changed |=
                    g.addEdges(sac,
                               l,
                               g.pointsTo(sac,
                                          ReferenceVariableReplica.create(calleeContext,
                                                                          calleeProcInfo.retValue,
                                                                          af)),
                               origin);
        }
        if (l == null && calleeProcInfo.retValue != null) {
            // this is OK: the caller made a specific decision not to record the result
            // possibly because it is not a reference, or because
            // the result is not stored anywhere.
        }
        if (l != null && calleeProcInfo.retValue == null) {
            throw new InternalCompilerError("Mismatch in return types! l = "
                    + l + " ; calleeProcInfo.retValue = "
                    + calleeProcInfo.retValue + "   ;  "
                    + calleeProcInfo.codeInstance);
        }

        // now add edges for exceptions.
        List<ClassType> throwTypes = (List<ClassType>) pi.throwTypes();
        for (ClassType thrown : throwTypes) {
            changed |=
                    checkThrown(thrown,
                                ts,
                                g,
                                calleeContext,
                                calleeProcInfo,
                                handlers,
                                procThrows,
                                origin,
                                af,
                                sac);
        }
        if (!throwTypes.contains(ts.RuntimeException())) {
            changed |=
                    checkThrown(ts.RuntimeException(),
                                ts,
                                g,
                                calleeContext,
                                calleeProcInfo,
                                handlers,
                                procThrows,
                                origin,
                                af,
                                sac);
        }
        if (!throwTypes.contains(ts.Error())) {
            changed |=
                    checkThrown(ts.Error(),
                                ts,
                                g,
                                calleeContext,
                                calleeProcInfo,
                                handlers,
                                procThrows,
                                origin,
                                af,
                                sac);
        }
        return changed;
    }

    private static boolean checkThrown(ClassType thrown, TypeSystem ts,
            PointsToGraph g, CContext calleeContext, CodeInfo calleeProcInfo,
            List<ExceptionHandlerPointsToGraphNode> handlers,
            Map<Type, ReferenceVariableReplica> procThrows, Position origin,
            HeapAbstractionFactory af, StmtAndContext sac) {
        // see where thrown may be caught.
        // walk up the handlers, assigning to appropriate variables
        boolean definitelyCaught = false;
        boolean maybeCaught = false;
        boolean changed = false;

        ReferenceVariableReplica thrownNode =
                ReferenceVariableReplica.create(calleeContext,
                                                calleeProcInfo.exceptionRets.get(thrown),
                                                af);

        Set<Type> alreadyCaught = new LinkedHashSet<Type>();
        for (ExceptionHandlerPointsToGraphNode ehln : handlers) {
            if (ts.isSubtype(thrown, ehln.exType)) {
                // there is a catch block that definitely catches this exception
                changed |=
                        g.addEdges(sac,
                                   ehln.node,
                                   g.pointsToFiltered(sac,
                                                      thrownNode,
                                                      ehln.exType,
                                                      alreadyCaught),
                                   origin)
                                || changed;

                definitelyCaught = true;
                alreadyCaught.add(ehln.exType);
                maybeCaught = true;
                break;
            }
            else if (ts.isSubtype(ehln.exType, thrown)) {
                // may be caught
                changed |=
                        g.addEdges(sac,
                                   ehln.node,
                                   g.pointsToFiltered(sac,
                                                      thrownNode,
                                                      ehln.exType,
                                                      alreadyCaught),
                                   origin)
                                || changed;
                alreadyCaught.add(ehln.exType);
                maybeCaught = true;
            }
        }
        if (!definitelyCaught) {
            // the exception may not have been caught, so it may go beyond the
            // scope of the calling method.
            for (Type throwType : procThrows.keySet()) {
                if (ts.isSubtype(throwType, thrown)
                        || ts.isSubtype(thrown, throwType)) {
                    // may fall under this throwtype.
                    changed |=
                            g.addEdges(sac,
                                       procThrows.get(throwType),
                                       g.pointsToFiltered(sac,
                                                          thrownNode,
                                                          throwType,
                                                          alreadyCaught),
                                       origin)
                                    || changed;
                    maybeCaught = true;
                }
                if (ts.isSubtype(thrown, throwType)) {
                    definitelyCaught = true;
                }
            }
        }
        if (!definitelyCaught) {
            throw new InternalCompilerError("May not be tracking the exception "
                    + thrown + " when calling " + calleeProcInfo);
        }
        return changed;
    }

    protected static List<ReferenceVariableReplica> convert(
            CContext callerContext, List<ReferenceVariable> l, HeapAbstractionFactory af) {
        List<ReferenceVariableReplica> args =
                new ArrayList<ReferenceVariableReplica>(l.size());
        for (ReferenceVariable ln : l) {
            args.add(ReferenceVariableReplica.create(callerContext, ln, af));
        }
        return args;
    }

    protected static List<ExceptionHandlerPointsToGraphNode> convert2(
            CContext callerContext, List<ExceptionHandlerReferenceVariable> l,
            HeapAbstractionFactory af) {
        List<ExceptionHandlerPointsToGraphNode> handlers =
                new ArrayList<ExceptionHandlerPointsToGraphNode>(l.size());
        for (ExceptionHandlerReferenceVariable eh : l) {
            handlers.add(new ExceptionHandlerPointsToGraphNode(eh.exType,
                                                               ReferenceVariableReplica.create(callerContext,
                                                                                               eh.ln,
                                                                                               af)));
        }
        return handlers;

    }

    protected static Map<Type, ReferenceVariableReplica> convert(
            CContext callerContext, Map<Type, ReferenceVariable> m,
            HeapAbstractionFactory af) {
        Map<Type, ReferenceVariableReplica> n =
                new LinkedHashMap<Type, ReferenceVariableReplica>();
        for (Type t : m.keySet()) {
            n.put(t,
                  ReferenceVariableReplica.create(callerContext, m.get(t), af));
        }
        return n;
    }

    public static class ExceptionHandlerPointsToGraphNode {
        public ExceptionHandlerPointsToGraphNode(Type exType,
                ReferenceVariableReplica o) {
            this.exType = exType;
            this.node = o;
        }

        final public Type exType;
        final public ReferenceVariableReplica node;

        @Override
        public String toString() {
            return exType.toString() + " -> " + node.toString();
        }
    }

}
