package accrue.analysis.ext;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import polyglot.ast.Expr;
import polyglot.ast.JLang;
import polyglot.ast.NewArray;
import polyglot.types.SemanticException;
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.DataFlowDispatch;
import accrue.analysis.pointer.AllocSiteNode;
import accrue.analysis.pointer.ConstructorContext;
import accrue.analysis.pointer.ReferenceVariable;
import accrue.analysis.pointer.RegisterPointerStmtsVisitor;
import accrue.analysis.pointer.StmtRegistrar;
import accrue.analysis.preciseExceptions.ThrowableType;

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

    private Map<ConstructorContext, AllocSiteNode> allocNodes;

    @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 (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) {
        NewArray n = (NewArray) this.node();
        registrar.createFreshReferenceVariable(n, v.constructorContext());
        if (n.init() == null) {
            // no initializer, need to take care of multiple dimensions.

            ReferenceVariable ln = v.getReferenceVariable(n);
            registrar.registerAlloc(this,
                                    n.type(),
                                    v.currentCode(),
                                    v.constructorContext());
            Type type = n.type().toArray().base();
            for (int i = 1; i < n.dims().size(); i++) {
                // ln points to the contents of the array
                // make a new alloc, and set "ln.contents = new"
                // that is, make two equations "ln' = new; ln.contents = ln'"
                ReferenceVariable lnp =
                        registrar.registerAlloc(type.toReference(),
                                                n.position(),
                                                type.toString(),
                                                v.currentCode());
                registrar.addAssignmentToField(ln,
                                               StmtRegistrar.ARRAY_CONTENTS,
                                               type,
                                               lnp,
                                               n.position(),
                                               v.currentCode());

                type = n.type().toArray().base();
                ln = lnp;
            }
        }
        else {
            // there is an array initializer that has already recorded the
            // allocation site
            registrar.addAssignment(v.getReferenceVariable(n),
                                    v.getReferenceVariable(n.init()),
                                    n.position(),
                                    v.currentCode());
        }

    }

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

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

    public boolean mayThrowNegativeArraySizeException() {
        NewArray n = (NewArray) this.node();

        // a very simple test for the moment.
        // if it has a dimenion that isn't 

        List<Expr> dims = n.dims();
        if (dims != null) {
            for (Expr e : dims) {
                if (e.isConstant() && e.constantValue() != null
                        && ((Number) e.constantValue()).intValue() >= 0) {
                    continue;
                }
                return true;
            }
        }
        return false;
    }

    @Override
    public Collection<ThrowableType> throwTypes(TypeSystem ts,
            AnalysisContext context) {
        if (this.mayThrowNegativeArraySizeException()) {
            try {
                return Collections.singleton(new ThrowableType(ts.typeForName("java.lang.NegativeArraySizeException"),
                                                               true));
            }
            catch (SemanticException e) {
                throw new InternalCompilerError(e);
            }
        }
        return Collections.emptyList();
    }
}
