package accrue.analysis.pointer;

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

import polyglot.main.Report;
import polyglot.types.CodeInstance;
import polyglot.types.ProcedureInstance;
import polyglot.util.Position;
import accrue.analysis.AnalysisTopics;

class CallGraph {
    private final ConcurrentHashMap<Caller, Set<Callee>> g;

    /**
     * The max number of threads that may concurrently access this call graph.
     * Used in the creation of new ConcurrentHashMaps.
     */
    private final int numThreads;

    public CallGraph(int numThreads) {
        this.numThreads = numThreads;
        this.g =
                new ConcurrentHashMap<Caller, Set<Callee>>(16,
                                                           0.75f,
                                                           this.numThreads);
    }

    public static class Caller {
        public final CodeInstance code;
        public final CContext context;

        public Caller(CodeInstance code, CContext context) {
            this.code = code;
            this.context = context;
            if (code == null) {
                throw new IllegalArgumentException("Null procedure instance");
            }
            if (context == null) {
                throw new IllegalArgumentException("Null CContext");
            }
        }

        @Override
        public int hashCode() {
            return code.hashCode() ^ context.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            Caller that = (Caller) o;
            return this.code == that.code && this.context.equals(that.context);
        }
    }

    public static class Callee {
        public final ProcedureInstance code;
        public final CContext context;

        public Callee(ProcedureInstance code, CContext context) {
            this.code = code;
            this.context = context;
            if (code == null) {
                throw new IllegalArgumentException("Null procedure instance");
            }
            if (context == null) {
                throw new IllegalArgumentException("Null CContext");
            }
        }

        @Override
        public int hashCode() {
            return code.hashCode() ^ context.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            Callee that = (Callee) o;
            return this.code == that.code && this.context.equals(that.context);
        }
    }

    public boolean addEdge(CodeInstance caller, CContext callerContext,
            ProcedureInstance callee, CContext calleeContext, Position origin) {
        Caller src = new Caller(caller, callerContext);
        Callee trg = new Callee(callee, calleeContext);

        Set<Callee> s = g.get(src);
        if (s == null) {
            Set<Callee> newS =
                    Collections.newSetFromMap(new ConcurrentHashMap<Callee, Boolean>(16,
                                                                                     0.75f,
                                                                                     numThreads));
            s = g.putIfAbsent(src, newS);
            if (s == null) {
                s = newS;
            }
        }

        boolean changed = s.add(trg);
        if (changed && Report.should_report(AnalysisTopics.POINTSTO, 4))
            Report.report(4, "Adding call graph edge " + src + " -> " + trg);

        return changed;
    }

    public Set<Caller> callers() {
        return g.keySet();
    }

    public Set<Callee> callees(Caller r) {
        Set<Callee> s = g.get(r);
        if (s == null) {
            return Collections.emptySet();
        }
        return s;
    }

    public void dump(PrintStream out) {
        for (Caller n : g.keySet()) {
            out.println(n);
            for (Callee s : g.get(n)) {
                out.println("   " + s);
            }
        }
    }

    public int nodeSize() {
        return g.size();
    }

    public long totalSize() {
        long sum = 0;
        for (Set<Callee> s : this.g.values()) {
            sum += s.size();
        }
        return sum;
    }
}
