package accrue.analysis.ext;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import polyglot.ast.Call;
import polyglot.ast.Expr;
import polyglot.ast.Receiver;
import polyglot.ast.Special;
import polyglot.types.MethodInstance;
import polyglot.types.Type;
import polyglot.types.TypeSystem;
import polyglot.util.InternalCompilerError;
import polyglot.util.SerialVersionUID;
import polyglot.visit.DataFlow;
import polyglot.visit.FlowGraph;
import polyglot.visit.FlowGraph.EdgeKey;
import polyglot.visit.FlowGraph.Peer;
import accrue.analysis.interprocanalysis.AnalysisContext;
import accrue.analysis.interprocanalysis.AnalysisContextMap;
import accrue.analysis.interprocanalysis.DataFlowDispatch;
import accrue.analysis.pointer.CallSiteNode;
import accrue.analysis.pointer.ReferenceVariable;
import accrue.analysis.pointer.RegisterPointerStmtsVisitor;
import accrue.analysis.pointer.StmtRegistrar;
import accrue.analysis.pointer.StmtRegistrar.ExceptionHandlerReferenceVariable;
import accrue.analysis.preciseExceptions.ThrowableType;

public class ExtCall extends ExtExpr implements ExtProcedureCall {
    private static final long serialVersionUID = SerialVersionUID.generate();

    private CallSiteNode callSiteNode;
    private final AnalysisContextMap<Set<ThrowableType>> throwTypes =
            new AnalysisContextMap<Set<ThrowableType>>();
    private final AnalysisContextMap<Boolean> canTerminateNormally =
            new AnalysisContextMap<Boolean>();

    @Override
    public void registerPointerStmts(StmtRegistrar registrar,
            RegisterPointerStmtsVisitor v) {

        Call c = (Call) this.node();

        MethodInstance mi = c.methodInstance();

        ReferenceVariable result = null;
        if (v.recordExprResults() && mi.returnType().isReference()) {
            registrar.createFreshReferenceVariable(c, v.constructorContext());
            result = v.getReferenceVariable(c);
        }

        if (c.target().type().isArray() && mi.name().equals("clone")
                && mi.formalTypes().isEmpty()) {
            // deal with clone() called on arrays separately
            registerPointerStmtsForArrayClone(registrar, v);
            return;
        }

        List<ExceptionHandlerReferenceVariable> exceptionHandlers =
                v.exceptionHandlers();
        Map<Type, ReferenceVariable> methodThrows =
                v.currentCode().exceptionRets;

        // build up localnodes for the actual args
        List<ReferenceVariable> actuals =
                new ArrayList<ReferenceVariable>(c.arguments().size());
        for (Expr arg : c.arguments()) {
            if (arg.type().isReference() || arg.type().isNull()) {
                actuals.add(v.getReferenceVariable(arg));
            }
        }

        boolean isStaticCall = mi.flags().isStatic();
        boolean isSpecialCall = false;
        if (!isStaticCall && c.target() instanceof Special) {
            Special sp = (Special) c.target();
            if (sp.kind() == Special.SUPER) {
                // not a dynamic dispath!
                isSpecialCall = true;
            }
        }
        if (isSpecialCall) {
            // This is a special call, i.e., to super.
            // We statically know the target, and this has a recevier.
            // get a local node for the receiver
            ReferenceVariable receiver =
                    v.getReferenceVariable((Expr) c.target());
            registrar.addSpecialCall(result,
                                     receiver,
                                     mi,
                                     this,
                                     actuals,
                                     exceptionHandlers,
                                     methodThrows,
                                     c.position(),
                                     v.currentCode());
        }
        else if (isStaticCall) {
            registrar.addStaticCall(result,
                                    mi,
                                    this,
                                    actuals,
                                    exceptionHandlers,
                                    methodThrows,
                                    c.position(),
                                    v.currentCode());
        }
        else {
            // This is a virtual call.
            // get a local node for the receiver
            ReferenceVariable receiver =
                    v.getReferenceVariable((Expr) c.target());
            registrar.addVirtualCall(result,
                                     receiver,
                                     mi,
                                     this,
                                     actuals,
                                     exceptionHandlers,
                                     methodThrows,
                                     c.position(),
                                     v.currentCode());

            // may generate a null pointer exception if the target is null.
            TypeSystem ts = v.typeSystem();

            // may generate a null pointer exception if the target is null.
            ExtExpr targ = (ExtExpr) AccrueExt_c.ext(c.target());
            if (targ.mayBeNull(null, null)) {
                registrar.addGeneratedExceptionAssignments(Collections.singleton(ts.NullPointerException()),
                                                           c,
                                                           v);
            }
        }

    }

    private void registerPointerStmtsForArrayClone(StmtRegistrar registrar,
            RegisterPointerStmtsVisitor v) {
        Call c = (Call) this.node();

        MethodInstance mi = c.methodInstance();
        if (!(mi.name().equals("clone") && mi.formalTypes().isEmpty() && c.target()
                                                                          .type()
                                                                          .isArray())) {
            throw new InternalCompilerError("This only handles clone on arrays",
                                            c.position());
        }

        // we have a clone method called on an array.
        // A[] a; b = a.clone()
        // treat it like: "b = a.clone(); b.f = a.f;"
        // or rather, "b = new; temp = a.f; b.f = temp;"

        if (v.getReferenceVariable(c) == null) {
            registrar.createFreshReferenceVariable(c, v.constructorContext());
        }

        registrar.registerArrayClone(c, v.currentCode(), v.constructorContext());
        // may generate a null pointer exception if the target is null.
        TypeSystem ts = v.typeSystem();

        // may generate a null pointer exception if the target is null.
        ExtExpr targ = (ExtExpr) AccrueExt_c.ext(c.target());
        if (targ.mayBeNull(null, null)) {
            registrar.addGeneratedExceptionAssignments(Collections.singleton(ts.NullPointerException()),
                                                       c,
                                                       v);
        }

    }

    @Override
    public void recordPreciseExResults(AnalysisContext context,
            Set<ThrowableType> throwTypes, boolean canTerminateNormally) {
        this.throwTypes.put(context, throwTypes);
        this.canTerminateNormally.put(context, canTerminateNormally);
    }

    @Override
    public Collection<ThrowableType> throwTypes(TypeSystem ts,
            AnalysisContext context) {
        Set<ThrowableType> s = throwTypes.get(context);
        if (s == null) {
            return super.throwTypes(ts, context);
        }

        if (this.targetMayBeNull(context)) {
            ThrowableType npe =
                    new ThrowableType(ts.NullPointerException(), true);
            if (!s.contains(npe)) {
                Set<ThrowableType> t = new HashSet<ThrowableType>(s);
                t.add(npe);
                return t;
            }
        }
        return s;
    }

    public boolean targetMayBeNull(AnalysisContext context) {
        Receiver target = ((Call) this.node()).target();
        if (target instanceof Expr) {
            ExtExpr et = (ExtExpr) AccrueExt_c.ext(target);
            return et.mayBeNull(context);
        }
        return false;
    }

    @Override
    public boolean canTerminateNormally(AnalysisContext context) {
        Boolean b = canTerminateNormally.get(context);
        if (b == null) return true;
        return b.booleanValue();
    }

    @Override
    public <DFItem extends DataFlow.Item> Map<EdgeKey, DFItem> flowDispatch(
            DataFlowDispatch<DFItem> d, DFItem dfIn, FlowGraph<DFItem> graph,
            Peer<DFItem> peer) {
        Call n = (Call) this.node();
        return d.flowCall(n, dfIn, graph, peer);
    }

    @Override
    public void setCallSiteNode(CallSiteNode callSiteNode) {
        this.callSiteNode = callSiteNode;
    }

    @Override
    public CallSiteNode getCallSiteNode() {
        return this.callSiteNode;
    }

}
