package accrue.analysis.pointer;

import java.io.PrintStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import polyglot.main.Report;
import polyglot.types.CodeInstance;
import polyglot.types.InitializerInstance;
import polyglot.types.ProcedureInstance;
import polyglot.types.Type;
import polyglot.util.InternalCompilerError;
import polyglot.util.Position;
import polyglot.util.StringUtil;
import accrue.analysis.AnalysisTopics;
import accrue.analysis.pointer.PointsToEngine.StmtAndContext;
import accrue.analysis.pointer.analyses.HeapAbstractionFactory;

public class PointsToGraphSerial extends PointsToGraph {
    private final Map<PointsToGraphNode, Set<HContext>> g =
            new HashMap<PointsToGraphNode, Set<HContext>>();
    private boolean recordUsage = true;

    final CallGraph callGraph = new CallGraph(1);
    /*
     * The contexts that a given code declaration occurs in
     */
    private Map<CodeInstance, Set<CContext>> contexts =
            new HashMap<CodeInstance, Set<CContext>>();

    /*
     * Record which nodes had their pointsto sets changed, and 
     * what the new contexts added to procedure instances are, and also
     * what nodes were accessed.
     */
    private Set<PointsToGraphNode> changedNodes =
            new HashSet<PointsToGraphNode>();
    private Map<ProcedureInstance, Set<CContext>> newContexts =
            new HashMap<ProcedureInstance, Set<CContext>>();
    private Set<PointsToGraphNode> readNodes = new HashSet<PointsToGraphNode>();

    public PointsToGraphSerial(HeapAbstractionFactory af,
            StmtRegistrar registrar) {
        super(af);
        // seed the initial contexts based on the registrar
        for (CodeInstance ci : registrar.getInitialCodeInstances()) {
            addInitialContext(ci);
        }
    }

    private void addInitialContext(CodeInstance ci) {
        Set<CContext> s = contexts.get(ci);
        if (s == null) {
            s = new HashSet<CContext>();
            contexts.put(ci, s);
        }
        s.add(af.initialContext());
    }

    Map<ProcedureInstance, Set<CContext>> getAndClearNewContexts() {
        if (!recordUsage)
            throw new InternalCompilerError("Not recording usage");
        Map<ProcedureInstance, Set<CContext>> s = newContexts;
        newContexts = new HashMap<ProcedureInstance, Set<CContext>>();
        return s;
    }

    Set<PointsToGraphNode> getAndClearChangedNodes() {
        if (!recordUsage)
            throw new InternalCompilerError("Not recording usage");
        Set<PointsToGraphNode> s = changedNodes;
        changedNodes = new HashSet<PointsToGraphNode>();
        return s;
    }

    Set<PointsToGraphNode> getAndClearReadNodes() {
        if (!recordUsage)
            throw new InternalCompilerError("Not recording usage");
        Set<PointsToGraphNode> s = readNodes;
        readNodes = new HashSet<PointsToGraphNode>();
        return s;
    }

    void setRecordUsage(boolean record) {
        this.recordUsage = record;
    }

    /**
     * Returns true iff changes were made to the graph.
     * @param src
     * @param trgs
     * @return
     */
    @Override
    public boolean addEdges(StmtAndContext sac, PointsToGraphNode src,
            Set<HContext> trgs, Position origin) {
        if (trgs.isEmpty()) return false;

        // check that the types are OK
        //checkTypes(src.expectedType(), trgs, origin);

        Set<HContext> s = g.get(src);

        if (s == null) {
            s = new LinkedHashSet<HContext>();
            g.put(src, s);
        }

        boolean changed = s.addAll(trgs);
        if (changed) {
            if (Report.should_report(AnalysisTopics.POINTSTO, 4))
                Report.report(4, "Adding edges " + src + " -> " + trgs);

            if (recordUsage) {
                changedNodes.add(src);
            }
        }
        return changed;
    }

    /**
     * Returns true iff changes were made to the graph.
     * @param src
     * @param trgs
     * @return
     */
    @Override
    public boolean addEdge(StmtAndContext sac, PointsToGraphNode src,
            HContext trg, Position origin) {
        if (src == null || trg == null) throw new IllegalArgumentException();

        // check that the types are OK
        //checkTypes(src.expectedType(), trg, origin);

        Set<HContext> s = g.get(src);

        if (s == null) {
            s = new LinkedHashSet<HContext>();
            g.put(src, s);
        }
        boolean changed = s.add(trg);
        if (changed) {
            if (Report.should_report(AnalysisTopics.POINTSTO, 4))
                Report.report(4, "Added edge " + src + " -> " + trg);

            if (recordUsage) {
                changedNodes.add(src);
            }
        }

        return changed;
    }

    private void checkTypes(Type expectedType, Set<HContext> os, Position origin) {
        if (expectedType == null) return;
        for (HContext o : os) {
            checkTypes(expectedType, o, origin);
        }
    }

    private void checkTypes(Type expectedType, HContext o, Position origin) {
        if (expectedType == null) return;
        Type t = o.type();
        if (!(t.isImplicitCastValid(expectedType))) {
            throw new InternalCompilerError("Points to breaks type safety! "
                                                    + o + " is a  " + t
                                                    + " which is not a "
                                                    + expectedType,
                                            origin);
        }
    }

    @Override
    public Set<HContext> pointsTo(StmtAndContext sac, PointsToGraphNode r) {
        if (recordUsage) {
            readNodes.add(r);
        }

        Set<HContext> s = g.get(r);
        if (s == null) {
            return Collections.EMPTY_SET;
        }
        return s;
    }

    @Override
    public Set<CContext> getContexts(CodeInstance pi) {
        if (pi instanceof InitializerInstance && !pi.flags().isStatic()) {
            throw new InternalCompilerError("Should only see static initializers here!");
        }
        Set<CContext> s = contexts.get(pi);
        if (s == null) {
            // no contexts for this statement yet. May be dead code.
            return Collections.EMPTY_SET;
        }
        return s;
    }

    @Override
    public boolean addCall(StmtAndContext sac, CodeInstance caller,
            CContext callerContext, ProcedureInstance callee,
            CContext calleeContext, Position origin) {
        callGraph.addEdge(caller, callerContext, callee, calleeContext, origin);
        Set<CContext> s = contexts.get(callee);
        if (s == null) {
            s = new HashSet<CContext>();
            contexts.put(callee, s);
        }
        boolean changed = s.add(calleeContext);

        if (changed && recordUsage) {
            // add it to the change set
            Set<CContext> t = newContexts.get(callee);
            if (t == null) {
                t = new HashSet<CContext>();
                newContexts.put(callee, t);
            }
            t.add(calleeContext);
        }

        return changed;

    }

    @Override
    public void dump(PrintStream out) {
        for (PointsToGraphNode n : g.keySet()) {
            out.println(n);
            for (HContext s : g.get(n)) {
                out.println("   " + s);
            }
        }
    }

    @Override
    public long edgeSize() {
        long sum = 0;
        for (Set s : this.g.values()) {
            sum += s.size();
        }
        return sum;
    }

    @Override
    public CallGraph callGraph() {
        return this.callGraph;
    }

    @Override
    public String toString() {
        String tmp = "";
        for (PointsToGraphNode n : g.keySet()) {
            tmp += n.toString();
            for (HContext s : g.get(n)) {
                tmp += "   " + s.toString();
            }
            tmp += "\n";
        }
        return tmp;
    }

    @Override
    public void dumpPointsToGraph(int level) {
        StringBuffer shortName = new StringBuffer();
        shortName.append("PointsTo Graph by ");
        shortName.append(StringUtil.getShortNameComponent(this.getClass()
                                                              .getName()));

        StringBuffer name = new StringBuffer(shortName.toString());
        if (level <= 1) {
            name.append(" (Not showing non-singleton ReferenceVariableReplica nodes. Use a higher reporting level to see those nodes in the graph.)");
        }

        Report.report(level, "// PointsTo graph");
        Report.report(level, "digraph \"" + shortName + "\" {");
        Report.report(level, "  label=\"" + name + "\\n"
                + "\"; fontsize=20; center=true; ratio=auto;");

        // get all object names  
        Set<HContext> allObjectNames = new LinkedHashSet<HContext>();
        for (Set<HContext> ss : this.g.values()) {
            allObjectNames.addAll(ss);
        }

        // print out object nodes once
        for (HContext s : allObjectNames) {
            Report.report(level,
                          System.identityHashCode(s)
                                  + " [ label = \""
                                  + StringUtil.escape(s.toString())
                                  + " ("
                                  + StringUtil.escape(StringUtil.getShortNameComponent(s.getClass()
                                                                                        .getName()))
                                  + ")\" ];");
        }

        for (PointsToGraphNode n : g.keySet()) {
            // dump out this node
            if (level > 1
                    || !(n instanceof ReferenceVariableReplica)
                    || ((ReferenceVariableReplica) n).r.isSingletonReferenceVariable()) {
                Report.report(level,
                              System.identityHashCode(n)
                                      + " [ label = \""
                                      + StringUtil.escape(n.toString())
                                      + "\\n("
                                      + StringUtil.escape(StringUtil.getShortNameComponent(n.getClass()
                                                                                            .getName()))
                                      + ")\" ];");

                // dump out the successors.
                for (HContext s : g.get(n)) {
                    String label = StringUtil.escape(s.toString());
                    label += "\\n";
                    Report.report(level, System.identityHashCode(n) + " -> "
                            + System.identityHashCode(s) + " [label=\"" + label
                            + "\"];");
                }
            }
        }
        Report.report(level, "}");
    }

    @Override
    Set<PointsToGraphNode> getDomain() {
        return Collections.unmodifiableSet(this.g.keySet());
    }
}
