package accrue.analysis.ext;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import polyglot.ast.Cast;
import polyglot.ast.Expr;
import polyglot.ast.JLang;
import polyglot.types.Type;
import polyglot.types.TypeSystem;
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.AnalysisUtil;
import accrue.analysis.interprocanalysis.DataFlowDispatch;
import accrue.analysis.pointer.HContext;
import accrue.analysis.pointer.RegisterPointerStmtsVisitor;
import accrue.analysis.pointer.StmtRegistrar;
import accrue.analysis.preciseExceptions.ThrowableType;

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

    private AnalysisContextMap<Boolean> mayThrowClassCastException =
            new AnalysisContextMap<Boolean>();

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

        Expr e = (Expr) this.node();
        if (!e.type().isReference()) return;

        Cast c = (Cast) e;

        if (v.recordExprResults()) {
            registrar.createFreshReferenceVariable(c, v.constructorContext());

            // only objects that are a subtype of e.type() will succeed in the
            // cast
            // and so we can filter this assignment.
            registrar.addFilteredAssignment(v.getReferenceVariable(c),
                                            v.getReferenceVariable(c.expr()),
                                            e.type(),
                                            Collections.<Type> emptySet(),
                                            c.position(),
                                            v.currentCode());
        }

        // may generate a classcast exception if it is a cast to a reference
        // type
        if (e.type().isReference()) {
            TypeSystem ts = v.typeSystem();
            registrar.addGeneratedExceptionAssignments(Collections.singleton(ts.ClassCastException()),
                                                       c,
                                                       v);
        }
    }

    @Override
    public int numArgumentsOnResultStack() {
        return 2;
    }

    @Override
    public Collection<ThrowableType> throwTypes(TypeSystem ts,
            AnalysisContext context) {
        if (mayThrowClassCastException(context)) {
            return Collections.singleton(new ThrowableType(ts.ClassCastException(),
                                                           true));
        }
        return Collections.emptyList();
    }

    public void recordClassCastAnalysisResult(AnalysisUtil<?> autil) {
        mayThrowClassCastException.put(autil.currentContext(),
                                       Boolean.valueOf(mayThrowClassCastException(autil)));
    }

    public boolean mayThrowClassCastException(AnalysisContext context) {
        Boolean b = mayThrowClassCastException.get(context);
        if (b != null) return b;
        Cast n = (Cast) this.node();
        return n.type().isReference();
    }

    private boolean mayThrowClassCastException(AnalysisUtil<?> autil) {
        // check to see if any of the objects it points to may be the wrong
        // class.
        Cast n = (Cast) this.node();
        if (n.type().isReference()) {
            Set<HContext> pt = autil.pointsTo(n.expr());
            for (HContext o : pt) {
                if (!(o.type().isSubtype(n.castType().type()))
                        && !(o.type().isImplicitCastValid(n.castType().type()))) {
                    return true;
                }
            }
        }
        return false;
    }

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

}
