package accrue.analysis.ext;

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

import polyglot.ast.Expr;
import polyglot.ast.New;
import polyglot.types.ConstructorInstance;
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.AllocSiteNode;
import accrue.analysis.pointer.CallSiteNode;
import accrue.analysis.pointer.ConstructorContext;
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 ExtNew extends ExtExpr implements ExtAlloc, ExtProcedureCall {
    private static final long serialVersionUID = SerialVersionUID.generate();
    private Map<ConstructorContext, AllocSiteNode> allocNodes;
    private CallSiteNode callSiteNode;
    private final AnalysisContextMap<Set<ThrowableType>> throwTypes =
            new AnalysisContextMap<Set<ThrowableType>>();
    private final AnalysisContextMap<Boolean> canTerminateNormally =
            new AnalysisContextMap<Boolean>();

    @Override
    public void setAllocNode(AllocSiteNode allocNode,
            ConstructorContext constructorContext) {
        Expr e = (Expr) this.node();
        if (!e.type().isReference()) {
            throw new InternalCompilerError("Local nodes only for reference types");
        }
        if (((New) e).anonType() != null) {
            throw new InternalCompilerError("Anonymous classes not dealt with");
        }
        if (this.allocNodes == null) {
            this.allocNodes =
                    new LinkedHashMap<ConstructorContext, AllocSiteNode>();
        }
        if (this.allocNodes.get(constructorContext) != null) {
            throw new InternalCompilerError("Alloc nodes should only be set once!");
        }
        this.allocNodes.put(constructorContext, allocNode);
    }

    @Override
    public AllocSiteNode getAllocNode(ConstructorContext constructorContext) {
        if (this.allocNodes == null) return null;
        return this.allocNodes.get(constructorContext);
    }

    @Override
    public void registerPointerStmts(StmtRegistrar registrar,
            RegisterPointerStmtsVisitor v) {
        New n = (New) this.node();

        if (n.body() != null) {
            throw new InternalCompilerError("Don't handle anonymous classes");
        }
        registrar.createFreshReferenceVariable(n, v.constructorContext());
        registrar.registerAlloc(this,
                                n.objectType().type(),
                                v.currentCode(),
                                v.constructorContext());

        if (n.anonType() != null) {
            throw new InternalCompilerError("Don't handle anon classes");
        }

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

        ConstructorInstance ci = n.constructorInstance();

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

        registrar.addConstructorCall(v.getReferenceVariable(n),
                                     ci,
                                     this,
                                     actuals,
                                     exceptionHandlers,
                                     methodThrows,
                                     n.position(),
                                     v.currentCode());
    }

    @Override
    public boolean mayBeNull(AnalysisContext context) {
        return false;
    }

    @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);
        }
        return s;
    }

    @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> dataflow, DFItem dfIn,
            FlowGraph<DFItem> graph, Peer<DFItem> peer) {
        New n = (New) this.node();
        return dataflow.flowNew(n, dfIn, graph, peer);
    }

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

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

}
