package accrue.analysis.pointer;

import java.io.PrintStream;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

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

/**
 * A concurrent implementation of the PointsToGraph.
 * @param <E>
 *
 */
public class PointsToGraphSync extends PointsToGraph {
    /**
     * Underlying data structure for the points-to graph.
     */
    private final ConcurrentHashMap<PointsToGraphNode, Set<HContext>> g;

    final CallGraph callGraph;

    /**
     * The contexts that a given code declaration occurs in.
     */
    private final ConcurrentHashMap<CodeInstance, Set<CContext>> contexts;

    private PointsToEngineMultiThread engine;

    public PointsToGraphSync(StmtRegistrar registrar,
            PointsToEngineMultiThread engine) {
        super(engine.heapAbstractionFactory());
        this.engine = engine;
        this.g =
                new ConcurrentHashMap<PointsToGraphNode, Set<HContext>>(16,
                                                                        0.75f,
                                                                        engine.numThreads());

        this.contexts =
                new ConcurrentHashMap<CodeInstance, Set<CContext>>(16,
                                                                   0.75f,
                                                                   engine.numThreads());

        this.callGraph = new CallGraph(engine.numThreads());

        // seed the initial contexts based on the registrar
        for (CodeInstance ci : registrar.getInitialCodeInstances()) {
            addInitialContext(ci);
        }
    }

    private void addInitialContext(CodeInstance ci) {
        Set<CContext> s = getOrCreateContextSet(ci);
        s.add(af.initialContext());
    }

    private Set<HContext> getOrCreate(PointsToGraphNode src) {
        // Double-Checked Lock pattern, works with ConcurrentHashMap in Java 5.0 and later
        Set<HContext> set = g.get(src);
        if (set == null) {
            // Set does not yet exist
            Set<HContext> newSet =
                    Collections.newSetFromMap(new ConcurrentHashMap<HContext, Boolean>(16,
                                                                                       0.75f,
                                                                                       engine.numThreads()));

            // It's possible that another thread created the set for the current src
            set = g.putIfAbsent(src, newSet);
            if (set == null) {
                // put succeeded, use new value
                set = newSet;
            }
        }
        return set;
    }

    private Set<CContext> getOrCreateContextSet(CodeInstance callee) {
        // Double-Checked Lock pattern, works with ConcurrentHashMap in Java 5.0 and later
        Set<CContext> set = contexts.get(callee);
        if (set == null) {
            // Set does not yet exist
            Set<CContext> newSet =
                    Collections.newSetFromMap(new ConcurrentHashMap<CContext, Boolean>(16,
                                                                                       0.75f,
                                                                                       engine.numThreads()));

            // It's possible that another thread created the Set for the current callee
            set = contexts.putIfAbsent(callee, newSet);
            if (set == null) {
                // put succeeded, use new value
                set = newSet;
            }
        }
        return set;
    }

    @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) {
            return Collections.emptySet();
        }

        return s;
    }

    public Set<PointsToGraphNode> getNodes() {
        return g.keySet();
    }

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

    @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 numEdges = 0;
        for (Set<HContext> s : this.g.values()) {
            numEdges += s.size();
        }
        return numEdges;
    }

    @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
    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 = getOrCreate(src);
        boolean changed = s.add(trg);
        if (changed) {
            if (Report.should_report(AnalysisTopics.POINTSTO, 4))
                Report.report(4, "Added edge " + src + " -> " + trg);

            engine.changedNode(src);
        }

        return changed;
    }

    @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 = getOrCreate(src);
        boolean changed = s.addAll(trgs);
        if (changed) {
            if (Report.should_report(AnalysisTopics.POINTSTO, 4))
                Report.report(4, "Adding edges " + src + " -> " + trgs);

            engine.changedNode(src);
        }
        return changed;
    }

    @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 = getOrCreateContextSet(callee);
        boolean changed = s.add(calleeContext);

        if (changed) {
            engine.newContext(callee, calleeContext);
        }

        return changed;

    }

    @Override
    public Set<HContext> pointsTo(StmtAndContext sac, PointsToGraphNode r) {
        if (sac != null) {
            engine.recordRead(sac, r);
        }
        else if (!this.solved) {
            throw new InternalCompilerError("No sac given, even though it is not yet solved!");
        }

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

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

}
