/*******************************************************************************
 * Copyright (c) 2018 Fraunhofer IEM, Paderborn, Germany.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *  
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Johannes Spaeth - initial API and implementation
 *******************************************************************************/
package boomerang;

import boomerang.callgraph.BackwardsObservableICFG;
import boomerang.callgraph.CalleeListener;
import boomerang.callgraph.CallerListener;
import boomerang.callgraph.ObservableICFG;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;

import boomerang.customize.BackwardEmptyCalleeFlow;
import boomerang.customize.EmptyCalleeFlow;
import boomerang.customize.ForwardEmptyCalleeFlow;
import boomerang.debugger.Debugger;
import boomerang.jimple.*;
import boomerang.poi.AbstractPOI;
import boomerang.poi.ExecuteImportFieldStmtPOI;
import boomerang.poi.PointOfIndirection;
import boomerang.preanalysis.BoomerangPretransformer;
import boomerang.results.BackwardBoomerangResults;
import boomerang.results.ForwardBoomerangResults;
import boomerang.seedfactory.SimpleSeedFactory;
import boomerang.solver.*;
import boomerang.seedfactory.SeedFactory;
import boomerang.solver.AbstractBoomerangSolver;
import boomerang.solver.BackwardBoomerangSolver;
import boomerang.solver.ForwardBoomerangSolver;
import boomerang.solver.ReachableMethodListener;
import boomerang.solver.StatementBasedCallTransitionListener;
import boomerang.stats.IBoomerangStats;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import heros.utilities.DefaultValueMap;

import soot.*;
import soot.jimple.*;
import sync.pds.solver.SyncPDSUpdateListener;
import sync.pds.solver.WeightFunctions;
import sync.pds.solver.nodes.*;
import wpds.impl.*;
import soot.Local;
import soot.Scene;
import soot.SootClass;
import soot.SootField;
import soot.SootMethod;
import soot.Unit;
import soot.jimple.ArrayRef;
import soot.jimple.AssignStmt;
import soot.jimple.InstanceFieldRef;
import soot.jimple.InvokeExpr;
import soot.jimple.NewMultiArrayExpr;
import soot.jimple.Stmt;
import soot.jimple.StringConstant;
import soot.jimple.toolkits.ide.icfg.BackwardsInterproceduralCFG;
import soot.jimple.toolkits.ide.icfg.BiDiInterproceduralCFG;
import sync.pds.solver.SyncPDSUpdateListener;
import sync.pds.solver.WeightFunctions;
import sync.pds.solver.nodes.GeneratedState;
import sync.pds.solver.nodes.INode;
import sync.pds.solver.nodes.Node;
import sync.pds.solver.nodes.SingleNode;
import wpds.impl.NestedWeightedPAutomatons;
import wpds.impl.Rule;
import wpds.impl.StackListener;
import wpds.impl.SummaryNestedWeightedPAutomatons;
import wpds.impl.Transition;
import wpds.impl.UnbalancedPopListener;
import wpds.impl.Weight;
import wpds.impl.WeightedPAutomaton;
import wpds.interfaces.State;
import wpds.interfaces.WPAStateListener;
import wpds.interfaces.WPAUpdateListener;

import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

public abstract class WeightedBoomerang<W extends Weight> {
    public static final boolean DEBUG = false;
    protected ObservableICFG<Unit, SootMethod> icfg;
    private static final Logger logger = LoggerFactory.getLogger(WeightedBoomerang.class);
    private Map<Entry<INode<Node<Statement, Val>>, Field>, INode<Node<Statement, Val>>> genField = new HashMap<>();
    private long lastTick;
    private IBoomerangStats<W> stats;
    private Set<SolverCreationListener<W>> solverCreationListeners = Sets.newHashSet();
    private Multimap<SolverPair, ExecuteImportFieldStmtPOI<W>> poiListeners = HashMultimap.create();
    private Multimap<SolverPair, INode<Node<Statement, Val>>> activatedPoi = HashMultimap.create();
    private final DefaultValueMap<Query, AbstractBoomerangSolver<W>> queryToSolvers = new DefaultValueMap<Query, AbstractBoomerangSolver<W>>() {

        @Override
        protected AbstractBoomerangSolver<W> createItem(final Query key) {
            final AbstractBoomerangSolver<W> solver;
            if (key instanceof BackwardQuery) {
                logger.debug("Backward solving query: " + key);
                solver = createBackwardSolver((BackwardQuery) key);
            } else {
                logger.debug("Forward solving query: " + key);
                solver = createForwardSolver((ForwardQuery) key);
            }
            solver.getCallAutomaton()
                    .registerUnbalancedPopListener(new UnbalancedPopListener<Statement, INode<Val>, W>() {

                        @Override
                        public void unbalancedPop(final INode<Val> returningFact,
                                final Transition<Statement, INode<Val>> trans, final W weight) {
                            Statement exitStmt = trans.getLabel();
                            SootMethod callee = exitStmt.getMethod();
                            if (!callee.isStaticInitializer()) {

                                UnbalancedPopHandler<W> info = new UnbalancedPopHandler<W>(returningFact, trans,
                                        weight);
                                icfg().addCallerListener(new UnbalancedPopCallerListener(callee, info, key, solver));
                            } else {
                                for (SootMethod entryPoint : Scene.v().getEntryPoints()) {
                                    for (Unit ep : WeightedBoomerang.this.icfg().getStartPointsOf(entryPoint)) {
                                        final Statement callStatement = new Statement((Stmt) ep,
                                                WeightedBoomerang.this.icfg().getMethodOf(ep));
                                        solver.submit(callStatement.getMethod(), new Runnable() {
                                            @Override
                                            public void run() {
                                                Val unbalancedFact = returningFact.fact().asUnbalanced(callStatement);
                                                SingleNode<Val> unbalancedState = new SingleNode<Val>(unbalancedFact);
                                                solver.getCallAutomaton().addUnbalancedState(unbalancedState);
                                                solver.getCallAutomaton().addWeightForTransition(
                                                        new Transition<Statement, INode<Val>>(
                                                                solver.getCallAutomaton().getInitialState(),
                                                                callStatement, unbalancedState),
                                                        solver.getCallAutomaton().getOne());

                                            }
                                        });
                                    }
                                }
                            }
                        }
                    });
            stats.registerSolver(key, solver);
            solver.getCallAutomaton().registerListener(new WPAUpdateListener<Statement, INode<Val>, W>() {
                @Override
                public void onWeightAdded(Transition<Statement, INode<Val>> t, W w,
                        WeightedPAutomaton<Statement, INode<Val>, W> aut) {
                    checkTimeout();
                }
            });
            solver.getFieldAutomaton().registerListener(new WPAUpdateListener<Field, INode<Node<Statement, Val>>, W>() {

                @Override
                public void onWeightAdded(Transition<Field, INode<Node<Statement, Val>>> t, W w,
                        WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
                    checkTimeout();
                }
            });
            SeedFactory<W> seedFactory = getSeedFactory();
            if (options.onTheFlyCallGraph() && seedFactory != null) {
                for (SootMethod m : seedFactory.getMethodScope(key)) {
                    solver.addReachable(m);
                }
            }
            onCreateSubSolver(key, solver);
            return solver;
        }
    };

    private void registerUnbalancedPopListener(Node<Statement, AbstractBoomerangSolver<W>> unbalancedPopPair,
            UnbalancedPopHandler<W> unbalancedPopInfo) {
        if (unbalancedPopPairs.contains(unbalancedPopPair)) {
            unbalancedPopInfo.trigger(unbalancedPopPair.stmt(), unbalancedPopPair.fact());
        } else {
            unbalancedListeners.put(unbalancedPopPair, unbalancedPopInfo);
        }

    }

    public void checkTimeout() {
        if (options.analysisTimeoutMS() > 0) {
            long elapsed = analysisWatch.elapsed(TimeUnit.MILLISECONDS);
            if (elapsed - lastTick > 15000) {
                System.out.println("Alive " + elapsed + "  " + options.analysisTimeoutMS());
                lastTick = elapsed;
            }
            if (options.analysisTimeoutMS() < elapsed) {
                if (analysisWatch.isRunning())
                    analysisWatch.stop();
                throw new BoomerangTimeoutException(elapsed, stats);
            }
        }
    }

    Multimap<Node<Statement, AbstractBoomerangSolver<W>>, UnbalancedPopHandler<W>> unbalancedListeners = HashMultimap
            .create();
    Set<Node<Statement, AbstractBoomerangSolver<W>>> unbalancedPopPairs = Sets.newHashSet();

    private void triggerUnbalancedPop(Node<Statement, AbstractBoomerangSolver<W>> unbalancedPopPair) {
        if (unbalancedPopPairs.add(unbalancedPopPair)) {
            for (UnbalancedPopHandler<W> unbalancedPopInfo : Lists
                    .newArrayList(unbalancedListeners.get(unbalancedPopPair))) {
                unbalancedPopInfo.trigger(unbalancedPopPair.stmt(), unbalancedPopPair.fact());
            }
        }
    }

    private ObservableICFG<Unit, SootMethod> bwicfg;
    private EmptyCalleeFlow forwardEmptyCalleeFlow = new ForwardEmptyCalleeFlow();
    private EmptyCalleeFlow backwardEmptyCalleeFlow = new BackwardEmptyCalleeFlow();

    private NestedWeightedPAutomatons<Statement, INode<Val>, W> backwardCallSummaries = new SummaryNestedWeightedPAutomatons<>();
    private NestedWeightedPAutomatons<Field, INode<Node<Statement, Val>>, W> backwardFieldSummaries = new SummaryNestedWeightedPAutomatons<>();
    private NestedWeightedPAutomatons<Statement, INode<Val>, W> forwardCallSummaries = new SummaryNestedWeightedPAutomatons<>();
    private NestedWeightedPAutomatons<Field, INode<Node<Statement, Val>>, W> forwardFieldSummaries = new SummaryNestedWeightedPAutomatons<>();
    private DefaultValueMap<FieldWritePOI, FieldWritePOI> fieldWrites = new DefaultValueMap<FieldWritePOI, FieldWritePOI>() {
        @Override
        protected FieldWritePOI createItem(FieldWritePOI key) {
            stats.registerFieldWritePOI(key);
            return key;
        }
    };
    private DefaultValueMap<FieldReadPOI, FieldReadPOI> fieldReads = new DefaultValueMap<FieldReadPOI, FieldReadPOI>() {
        @Override
        protected FieldReadPOI createItem(FieldReadPOI key) {
            stats.registerFieldReadPOI(key);
            return key;
        }
    };
    protected final BoomerangOptions options;
    private Debugger<W> debugger;
    private Stopwatch analysisWatch = Stopwatch.createUnstarted();
    private Set<BackwardQuery> scopedQueries = Sets.newHashSet();

    public WeightedBoomerang(BoomerangOptions options) {
        this.options = options;
        this.stats = options.statsFactory();
        if (!BoomerangPretransformer.v().isApplied()) {
            throw new RuntimeException(
                    "Using WeightedBoomerang requires a call to BoomerangPretransformer.v().apply() prior constructing the ICFG");
        }
    }

    public WeightedBoomerang() {
        this(new DefaultBoomerangOptions());
    }

    protected AbstractBoomerangSolver<W> createBackwardSolver(final BackwardQuery backwardQuery) {
        BackwardBoomerangSolver<W> solver = new BackwardBoomerangSolver<W>(bwicfg(), backwardQuery, genField, options,
                createCallSummaries(backwardQuery, backwardCallSummaries),
                createFieldSummaries(backwardQuery, backwardFieldSummaries)) {

            @Override
            protected Collection<? extends State> computeCallFlow(SootMethod caller, Statement returnSite,
                    Statement callSite, InvokeExpr invokeExpr, Val fact, SootMethod callee, Stmt calleeSp) {
                return super.computeCallFlow(caller, returnSite, callSite, invokeExpr, fact, callee, calleeSp);
            }

            @Override
            protected Collection<? extends State> getEmptyCalleeFlow(SootMethod caller, Stmt callSite, Val value,
                    Stmt returnSite) {
                return backwardEmptyCalleeFlow.getEmptyCalleeFlow(caller, callSite, value, returnSite);
            }

            @Override
            protected WeightFunctions<Statement, Val, Field, W> getFieldWeights() {
                return WeightedBoomerang.this.getBackwardFieldWeights();
            }

            @Override
            protected WeightFunctions<Statement, Val, Statement, W> getCallWeights() {
                return WeightedBoomerang.this.getBackwardCallWeights();
            }

            @Override
            protected void onManyStateListenerRegister() {
                checkTimeout();
            }

        };
        solver.registerListener(new SyncPDSUpdateListener<Statement, Val>() {
            @Override
            public void onReachableNodeAdded(Node<Statement, Val> node) {
                if (hasNoMethod(node)) {
                    return;
                }
                Optional<AllocVal> allocNode = isAllocationNode(node.stmt(), node.fact());
                if (allocNode.isPresent()) {
                    ForwardQuery q = new ForwardQuery(node.stmt(), allocNode.get());
                    final AbstractBoomerangSolver<W> forwardSolver = forwardSolve(q);
                    solver.registerReachableMethodListener(new ReachableMethodListener<W>() {

                        @Override
                        public void reachable(SootMethod m) {
                            forwardSolver.addReachable(m);
                        }
                    });
                }
                if (isFieldStore(node.stmt())) {
                } else if (isArrayLoad(node.stmt())) {
                    backwardHandleFieldRead(node, createArrayFieldLoad(node.stmt()), backwardQuery);
                } else if (isFieldLoad(node.stmt())) {
                    backwardHandleFieldRead(node, createFieldLoad(node.stmt()), backwardQuery);
                }
                if (isBackwardEnterCall(node.stmt())) {
                    // TODO
                }
                if (options.trackStaticFieldAtEntryPointToClinit() && node.fact().isStatic()
                        && isFirstStatementOfEntryPoint(node.stmt())) {
                    StaticFieldVal val = (StaticFieldVal) node.fact();
                    for (SootMethod m : val.field().getDeclaringClass().getMethods()) {
                        if (m.isStaticInitializer()) {
                            solver.addReachable(m);
                            for (Unit ep : icfg().getEndPointsOf(m)) {
                                StaticFieldVal newVal = new StaticFieldVal(val.value(), val.field(), m);
                                solver.addNormalCallFlow(node,
                                        new Node<Statement, Val>(new Statement((Stmt) ep, m), newVal));
                                solver.addNormalFieldFlow(node,
                                        new Node<Statement, Val>(new Statement((Stmt) ep, m), newVal));
                            }
                        }
                    }
                }
            }

        });

        return solver;
    }

    protected boolean hasNoMethod(Node<Statement, Val> node) {
        if (icfg().getMethodOf(node.stmt().getUnit().get()) == null) {
            return true;
        }
        return false;
    }

    protected boolean isFirstStatementOfEntryPoint(Statement stmt) {
        for (SootMethod m : Scene.v().getEntryPoints()) {
            if (m.hasActiveBody()) {
                if (stmt.equals(new Statement((Stmt) m.getActiveBody().getUnits().getFirst(), m))) {
                    return true;
                }
            }
        }
        return false;
    }

    protected boolean isBackwardEnterCall(Statement stmt) {
        if (!stmt.getUnit().isPresent())
            return false;
        try {
            return icfg().isExitStmt(stmt.getUnit().get());
        } catch (NullPointerException e) {
            return false;
        }
    }

    protected Optional<AllocVal> isAllocationNode(Statement s, Val fact) {
        Optional<Stmt> optUnit = s.getUnit();
        if (optUnit.isPresent()) {
            Stmt stmt = optUnit.get();
            return options.getAllocationVal(s.getMethod(), stmt, fact, icfg());
        }
        return Optional.absent();
    }

    protected ForwardBoomerangSolver<W> createForwardSolver(final ForwardQuery sourceQuery) {
        final ForwardBoomerangSolver<W> solver = new ForwardBoomerangSolver<W>(icfg(), sourceQuery, genField, options,
                createCallSummaries(sourceQuery, forwardCallSummaries),
                createFieldSummaries(sourceQuery, forwardFieldSummaries)) {

            @Override
            protected Collection<? extends State> getEmptyCalleeFlow(SootMethod caller, Stmt callSite, Val value,
                    Stmt returnSite) {
                return forwardEmptyCalleeFlow.getEmptyCalleeFlow(caller, callSite, value, returnSite);
            }

            @Override
            protected WeightFunctions<Statement, Val, Statement, W> getCallWeights() {
                return WeightedBoomerang.this.getForwardCallWeights(sourceQuery);
            }

            @Override
            protected WeightFunctions<Statement, Val, Field, W> getFieldWeights() {
                return WeightedBoomerang.this.getForwardFieldWeights();
            }

            @Override
            public void addCallRule(Rule<Statement, INode<Val>, W> rule) {
                if (preventCallRuleAdd(sourceQuery, rule)) {
                    return;
                }
                super.addCallRule(rule);
            }

            @Override
            protected void onManyStateListenerRegister() {
                checkTimeout();
            }

        };

        solver.registerListener(new SyncPDSUpdateListener<Statement, Val>() {
            @Override
            public void onReachableNodeAdded(Node<Statement, Val> node) {
                if (hasNoMethod(node)) {
                    return;
                }
                if (isFieldStore(node.stmt())) {
                    forwardHandleFieldWrite(node, createFieldStore(node.stmt()), sourceQuery);
                } else if (isArrayStore(node.stmt())) {
                    if (options.arrayFlows()) {
                        forwardHandleFieldWrite(node, createArrayFieldStore(node.stmt()), sourceQuery);
                    }
                } else if (isFieldLoad(node.stmt())) {
                    forwardHandleFieldLoad(node, createFieldLoad(node.stmt()), sourceQuery);
                } else if (isArrayLoad(node.stmt())) {
                    forwardHandleFieldLoad(node, createArrayFieldLoad(node.stmt()), sourceQuery);
                }
                if (isBackwardEnterCall(node.stmt())) {
                    // TODO
                }
            }
        });

        return solver;
    }

    private NestedWeightedPAutomatons<Statement, INode<Val>, W> createCallSummaries(final Query sourceQuery,
            final NestedWeightedPAutomatons<Statement, INode<Val>, W> summaries) {
        return new NestedWeightedPAutomatons<Statement, INode<Val>, W>() {

            @Override
            public void putSummaryAutomaton(INode<Val> target, WeightedPAutomaton<Statement, INode<Val>, W> aut) {
                summaries.putSummaryAutomaton(target, aut);
            }

            @Override
            public WeightedPAutomaton<Statement, INode<Val>, W> getSummaryAutomaton(INode<Val> target) {
                if (target.fact().equals(sourceQuery.var())) {
                    return queryToSolvers.getOrCreate(sourceQuery).getCallAutomaton();
                }
                return summaries.getSummaryAutomaton(target);
            }
        };
    }

    private NestedWeightedPAutomatons<Field, INode<Node<Statement, Val>>, W> createFieldSummaries(final Query query,
            final NestedWeightedPAutomatons<Field, INode<Node<Statement, Val>>, W> summaries) {
        return new NestedWeightedPAutomatons<Field, INode<Node<Statement, Val>>, W>() {

            @Override
            public void putSummaryAutomaton(INode<Node<Statement, Val>> target,
                    WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
                summaries.putSummaryAutomaton(target, aut);
            }

            @Override
            public WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> getSummaryAutomaton(
                    INode<Node<Statement, Val>> target) {
                if (target.fact().equals(query.asNode())) {
                    return queryToSolvers.getOrCreate(query).getFieldAutomaton();
                }
                return summaries.getSummaryAutomaton(target);
            }

        };
    }

    public boolean preventCallRuleAdd(ForwardQuery sourceQuery, Rule<Statement, INode<Val>, W> rule) {
        return false;
    }

    protected FieldReadPOI createFieldLoad(Statement s) {
        Stmt stmt = s.getUnit().get();
        AssignStmt as = (AssignStmt) stmt;
        InstanceFieldRef ifr = (InstanceFieldRef) as.getRightOp();
        Val base = new Val(ifr.getBase(), icfg().getMethodOf(as));
        Field field = new Field(ifr.getField());
        return fieldReads
                .getOrCreate(new FieldReadPOI(s, base, field, new Val(as.getLeftOp(), icfg().getMethodOf(as))));
    }

    protected FieldReadPOI createArrayFieldLoad(Statement s) {
        Stmt stmt = s.getUnit().get();
        AssignStmt as = (AssignStmt) stmt;
        ArrayRef ifr = (ArrayRef) as.getRightOp();
        Val base = new Val(ifr.getBase(), icfg().getMethodOf(as));
        Val stored = new Val(as.getLeftOp(), icfg().getMethodOf(as));
        return fieldReads.getOrCreate(new FieldReadPOI(s, base, Field.array(), stored));
    }

    protected FieldWritePOI createArrayFieldStore(Statement s) {
        Stmt stmt = s.getUnit().get();
        AssignStmt as = (AssignStmt) stmt;
        ArrayRef ifr = (ArrayRef) as.getLeftOp();
        Val base = new Val(ifr.getBase(), icfg().getMethodOf(as));
        Val stored = new Val(as.getRightOp(), icfg().getMethodOf(as));
        return fieldWrites.getOrCreate(new FieldWritePOI(s, base, Field.array(), stored));
    }

    protected FieldWritePOI createFieldStore(Statement s) {
        Stmt stmt = s.getUnit().get();
        AssignStmt as = (AssignStmt) stmt;
        InstanceFieldRef ifr = (InstanceFieldRef) as.getLeftOp();
        Val base = new Val(ifr.getBase(), icfg().getMethodOf(as));
        Val stored = new Val(as.getRightOp(), icfg().getMethodOf(as));
        Field field = new Field(ifr.getField());
        return fieldWrites.getOrCreate(new FieldWritePOI(s, base, field, stored));
    }

    public static boolean isFieldStore(Statement s) {
        Optional<Stmt> optUnit = s.getUnit();
        if (optUnit.isPresent()) {
            Stmt stmt = optUnit.get();
            if (stmt instanceof AssignStmt && ((AssignStmt) stmt).getLeftOp() instanceof InstanceFieldRef) {
                return true;
            }
        }
        return false;
    }

    public static boolean isArrayStore(Statement s) {
        Optional<Stmt> optUnit = s.getUnit();
        if (optUnit.isPresent()) {
            Stmt stmt = optUnit.get();
            if (stmt instanceof AssignStmt && ((AssignStmt) stmt).getLeftOp() instanceof ArrayRef) {
                return true;
            }
        }
        return false;
    }

    public static boolean isArrayLoad(Statement s) {
        Optional<Stmt> optUnit = s.getUnit();
        if (optUnit.isPresent()) {
            Stmt stmt = optUnit.get();
            if (stmt instanceof AssignStmt && ((AssignStmt) stmt).getRightOp() instanceof ArrayRef) {
                return true;
            }
        }
        return false;
    }

    public static boolean isFieldLoad(Statement s) {
        Optional<Stmt> optUnit = s.getUnit();
        if (optUnit.isPresent()) {
            Stmt stmt = optUnit.get();
            if (stmt instanceof AssignStmt && ((AssignStmt) stmt).getRightOp() instanceof InstanceFieldRef) {
                return true;
            }
        }
        return false;
    }

    protected void backwardHandleFieldRead(final Node<Statement, Val> node, FieldReadPOI fieldRead,
            final BackwardQuery sourceQuery) {
        if (node.fact().equals(fieldRead.getStoredVar())) {
            fieldRead.addFlowAllocation(sourceQuery);
        }
    }

    protected void forwardHandleFieldWrite(final Node<Statement, Val> node, final FieldWritePOI fieldWritePoi,
            final ForwardQuery sourceQuery) {
        BackwardQuery backwardQuery = new BackwardQuery(node.stmt(), fieldWritePoi.getBaseVar());
        if (node.fact().equals(fieldWritePoi.getStoredVar())) {
            backwardSolveUnderScope(backwardQuery, sourceQuery, node);
            queryToSolvers.get(sourceQuery).getFieldAutomaton()
                    .registerListener(new ForwardHandleFieldWrite(sourceQuery, fieldWritePoi, node.stmt()));
        }
        if (node.fact().equals(fieldWritePoi.getBaseVar())) {
            queryToSolvers.getOrCreate(sourceQuery).getFieldAutomaton()
                    .registerListener(new TriggerBaseAllocationAtFieldWrite(new SingleNode<Node<Statement, Val>>(node),
                            fieldWritePoi, sourceQuery));
        }
    }

    public BackwardBoomerangResults<W> backwardSolveUnderScope(BackwardQuery backwardQuery, ForwardQuery forwardQuery,
            Node<Statement, Val> node) {
        scopedQueries.add(backwardQuery);
        boolean timedout = false;
        try {
            backwardSolve(backwardQuery);
            final AbstractBoomerangSolver<W> bwSolver = queryToSolvers.getOrCreate(backwardQuery);
            AbstractBoomerangSolver<W> fwSolver = queryToSolvers.getOrCreate(forwardQuery);
            fwSolver.registerReachableMethodListener(new ReachableMethodListener<W>() {
                @Override
                public void reachable(SootMethod m) {
                    bwSolver.addReachable(m);
                }
            });

            fwSolver.getCallAutomaton().registerListener(new StackListener<Statement, INode<Val>, W>(
                    fwSolver.getCallAutomaton(), new SingleNode<>(node.fact()), node.stmt()) {

                @Override
                public void stackElement(Statement callSite) {
                    triggerUnbalancedPop(new Node<Statement, AbstractBoomerangSolver<W>>(callSite, bwSolver));
                }

                @Override
                public void anyContext(Statement end) {
                    for (Unit sP : icfg().getStartPointsOf(end.getMethod())) {
                        bwSolver.registerStatementCallTransitionListener(new CanUnbalancedReturn(end.getMethod(),
                                new Statement((Stmt) sP, end.getMethod()), bwSolver));
                    }
                }
            });
        } catch (BoomerangTimeoutException e) {
            timedout = true;
            cleanup();
        }

        return new BackwardBoomerangResults<W>(backwardQuery, timedout, this.queryToSolvers, getStats(), analysisWatch);
    }

    private void cleanup() {
        for (AbstractBoomerangSolver<W> solver : queryToSolvers.values()) {
            solver.cleanup();
        }
        this.poiListeners.clear();
        this.unbalancedListeners.clear();
    }

    public BackwardBoomerangResults<W> backwardSolveUnderScope(BackwardQuery backwardQuery,
            IContextRequester requester) {
        scopedQueries.add(backwardQuery);
        boolean timedout = false;
        try {
            if (analysisWatch.isRunning()) {
                analysisWatch.stop();
            }
            analysisWatch = Stopwatch.createStarted();
            backwardSolve(backwardQuery);

            final AbstractBoomerangSolver<W> bwSolver = queryToSolvers.getOrCreate(backwardQuery);
            Collection<Context> callSiteOf = requester.getCallSiteOf(requester.initialContext(backwardQuery.stmt()));
            for (Context c : callSiteOf) {
                bwSolver.registerListener(
                        new CanUnbalancedReturnToCallSite(backwardQuery.stmt().getMethod(), c, bwSolver, requester));
            }
            if (analysisWatch.isRunning()) {
                analysisWatch.stop();
            }
        } catch (BoomerangTimeoutException e) {
            timedout = true;
            cleanup();
        }

        return new BackwardBoomerangResults<W>(backwardQuery, timedout, this.queryToSolvers, getStats(), analysisWatch);
    }

    private final class UnbalancedPopCallerListener implements CallerListener<Unit, SootMethod> {
        private final SootMethod callee;
        private final UnbalancedPopHandler<W> info;
        private final Query key;
        private final AbstractBoomerangSolver<W> solver;

        private UnbalancedPopCallerListener(SootMethod callee, UnbalancedPopHandler<W> info, Query key,
                AbstractBoomerangSolver<W> solver) {
            this.callee = callee;
            this.info = info;
            this.key = key;
            this.solver = solver;
        }

        @Override
        public SootMethod getObservedCallee() {
            return callee;
        }

        @Override
        public void onCallerAdded(Unit callSite, SootMethod m) {
            if (!((Stmt) callSite).containsInvokeExpr())
                return;
            final Statement callStatement = new Statement((Stmt) callSite,
                    WeightedBoomerang.this.icfg().getMethodOf(callSite));
            Node<Statement, AbstractBoomerangSolver<W>> solverPair = new Node<>(callStatement, solver);
            registerUnbalancedPopListener(solverPair, info);
            if (callee.isStatic() || !scopedQueries.contains(key)) {
                triggerUnbalancedPop(solverPair);
            }
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((callee == null) ? 0 : callee.hashCode());
            result = prime * result + ((info == null) ? 0 : info.hashCode());
            result = prime * result + ((key == null) ? 0 : key.hashCode());
            result = prime * result + ((solver == null) ? 0 : solver.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            UnbalancedPopCallerListener other = (UnbalancedPopCallerListener) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (callee == null) {
                if (other.callee != null)
                    return false;
            } else if (!callee.equals(other.callee))
                return false;
            if (info == null) {
                if (other.info != null)
                    return false;
            } else if (!info.equals(other.info))
                return false;
            if (key == null) {
                if (other.key != null)
                    return false;
            } else if (!key.equals(other.key))
                return false;
            if (solver == null) {
                if (other.solver != null)
                    return false;
            } else if (!solver.equals(other.solver))
                return false;
            return true;
        }

        private WeightedBoomerang getOuterType() {
            return WeightedBoomerang.this;
        }

    }

    private class CanUnbalancedReturnToCallSite implements SyncPDSUpdateListener<Statement, Val> {

        private AbstractBoomerangSolver<W> bwSolver;
        private SootMethod method;
        private Collection<Unit> startPointsOf;
        private Context callSite;
        private IContextRequester req;

        public CanUnbalancedReturnToCallSite(SootMethod method, Context callSite, AbstractBoomerangSolver<W> bwSolver,
                IContextRequester req) {
            this.method = method;
            this.callSite = callSite;
            this.bwSolver = bwSolver;
            this.req = req;
            this.startPointsOf = icfg().getStartPointsOf(method);
        }

        @Override
        public void onReachableNodeAdded(Node<Statement, Val> reachableNode) {
            if (startPointsOf.contains(reachableNode.stmt().getUnit().get())) {
                Node<Statement, AbstractBoomerangSolver<W>> solverPair = new Node<>(callSite.getStmt(), bwSolver);
                bwSolver.addReachable(callSite.getStmt().getMethod());
                triggerUnbalancedPop(solverPair);
                for (Context parent : req.getCallSiteOf(callSite)) {
                    bwSolver.registerListener(
                            new CanUnbalancedReturnToCallSite(callSite.getStmt().getMethod(), parent, bwSolver, req));
                }
            }
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((bwSolver == null) ? 0 : bwSolver.hashCode());
            result = prime * result + ((method == null) ? 0 : method.hashCode());
            result = prime * result + ((callSite == null) ? 0 : callSite.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            CanUnbalancedReturnToCallSite other = (CanUnbalancedReturnToCallSite) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (bwSolver == null) {
                if (other.bwSolver != null)
                    return false;
            } else if (!bwSolver.equals(other.bwSolver))
                return false;
            if (method == null) {
                if (other.method != null)
                    return false;
            } else if (!method.equals(other.method))
                return false;
            if (callSite == null) {
                if (other.callSite != null)
                    return false;
            } else if (!callSite.equals(other.callSite))
                return false;
            return true;
        }

        private WeightedBoomerang getOuterType() {
            return WeightedBoomerang.this;
        }

    }

    private final class UnbalancedReturnCallerListener implements CallerListener<Unit, SootMethod> {
        private final AbstractBoomerangSolver<W> bwSolver;
        private final SootMethod method;

        public UnbalancedReturnCallerListener(AbstractBoomerangSolver<W> bwSolver, SootMethod method) {
            this.bwSolver = bwSolver;
            this.method = method;
        }

        @Override
        public SootMethod getObservedCallee() {
            return method;
        }

        @Override
        public void onCallerAdded(Unit callSite, SootMethod m) {
            if (!((Stmt) callSite).containsInvokeExpr())
                return;
            final Statement callStatement = new Statement((Stmt) callSite,
                    WeightedBoomerang.this.icfg().getMethodOf(callSite));
            Node<Statement, AbstractBoomerangSolver<W>> solverPair = new Node<>(callStatement, bwSolver);
            triggerUnbalancedPop(solverPair);
            for (Unit sP : icfg().getStartPointsOf(callStatement.getMethod())) {
                bwSolver.registerStatementCallTransitionListener(new CanUnbalancedReturn(callStatement.getMethod(),
                        new Statement((Stmt) sP, callStatement.getMethod()), bwSolver));
            }
        }
    }

    private class CanUnbalancedReturn extends StatementBasedCallTransitionListener<W> {

        private AbstractBoomerangSolver<W> bwSolver;
        private Statement startPoint;
        private SootMethod method;

        public CanUnbalancedReturn(SootMethod method, Statement startPoint, AbstractBoomerangSolver<W> bwSolver) {
            super(startPoint);
            this.method = method;
            this.bwSolver = bwSolver;
            this.startPoint = startPoint;
        }

        @Override
        public void onAddedTransition(Transition<Statement, INode<Val>> t, W w) {
            if (t.getLabel().equals(startPoint)) {
                WeightedBoomerang.this.icfg().addCallerListener(new UnbalancedReturnCallerListener(bwSolver, method));
            }
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((bwSolver == null) ? 0 : bwSolver.hashCode());
            result = prime * result + ((startPoint == null) ? 0 : startPoint.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            CanUnbalancedReturn other = (CanUnbalancedReturn) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (bwSolver == null) {
                if (other.bwSolver != null)
                    return false;
            } else if (!bwSolver.equals(other.bwSolver))
                return false;
            if (startPoint == null) {
                if (other.startPoint != null)
                    return false;
            } else if (!startPoint.equals(other.startPoint))
                return false;
            return true;
        }

        private WeightedBoomerang getOuterType() {
            return WeightedBoomerang.this;
        }

    }

    private final class ForwardHandleFieldWrite implements WPAUpdateListener<Field, INode<Node<Statement, Val>>, W> {
        private final Query sourceQuery;
        private final AbstractPOI<Statement, Val, Field> fieldWritePoi;
        private final Statement stmt;

        private ForwardHandleFieldWrite(Query sourceQuery, AbstractPOI<Statement, Val, Field> fieldWritePoi,
                Statement statement) {
            this.sourceQuery = sourceQuery;
            this.fieldWritePoi = fieldWritePoi;
            this.stmt = statement;
        }

        @Override
        public void onWeightAdded(Transition<Field, INode<Node<Statement, Val>>> t, W w,
                WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
            if (t.getStart() instanceof GeneratedState)
                return;
            if (t.getStart().fact().stmt().equals(stmt) && t.getLabel().equals(Field.empty())) {
                fieldWritePoi.addFlowAllocation(sourceQuery);
            }
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((sourceQuery == null) ? 0 : sourceQuery.hashCode());
            result = prime * result + ((stmt == null) ? 0 : stmt.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ForwardHandleFieldWrite other = (ForwardHandleFieldWrite) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (sourceQuery == null) {
                if (other.sourceQuery != null)
                    return false;
            } else if (!sourceQuery.equals(other.sourceQuery))
                return false;
            if (stmt == null) {
                if (other.stmt != null)
                    return false;
            } else if (!stmt.equals(other.stmt))
                return false;
            return true;
        }

        private WeightedBoomerang getOuterType() {
            return WeightedBoomerang.this;
        }

    }

    private class TriggerBaseAllocationAtFieldWrite extends WPAStateListener<Field, INode<Node<Statement, Val>>, W> {

        private final PointOfIndirection<Statement, Val, Field> fieldWritePoi;
        private final ForwardQuery sourceQuery;

        public TriggerBaseAllocationAtFieldWrite(INode<Node<Statement, Val>> state,
                PointOfIndirection<Statement, Val, Field> fieldWritePoi, ForwardQuery sourceQuery) {
            super(state);
            this.fieldWritePoi = fieldWritePoi;
            this.sourceQuery = sourceQuery;
        }

        @Override
        public void onOutTransitionAdded(Transition<Field, INode<Node<Statement, Val>>> t, W w,
                WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
            if (isAllocationNode(t.getTarget().fact().fact(), sourceQuery)) {
                fieldWritePoi.addBaseAllocation(sourceQuery);
            }
        }

        @Override
        public void onInTransitionAdded(Transition<Field, INode<Node<Statement, Val>>> t, W w,
                WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = super.hashCode();
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((fieldWritePoi == null) ? 0 : fieldWritePoi.hashCode());
            result = prime * result + ((sourceQuery == null) ? 0 : sourceQuery.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (!super.equals(obj))
                return false;
            if (getClass() != obj.getClass())
                return false;
            TriggerBaseAllocationAtFieldWrite other = (TriggerBaseAllocationAtFieldWrite) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (fieldWritePoi == null) {
                if (other.fieldWritePoi != null)
                    return false;
            } else if (!fieldWritePoi.equals(other.fieldWritePoi))
                return false;
            if (sourceQuery == null) {
                if (other.sourceQuery != null)
                    return false;
            } else if (!sourceQuery.equals(other.sourceQuery))
                return false;
            return true;
        }

        private WeightedBoomerang getOuterType() {
            return WeightedBoomerang.this;
        }

    }

    private void forwardHandleFieldLoad(final Node<Statement, Val> node, final FieldReadPOI fieldReadPoi,
            final ForwardQuery sourceQuery) {
        if (node.fact().equals(fieldReadPoi.getBaseVar())) {
            queryToSolvers.getOrCreate(sourceQuery).getFieldAutomaton()
                    .registerListener(new TriggerBaseAllocationAtFieldWrite(new SingleNode<Node<Statement, Val>>(node),
                            fieldReadPoi, sourceQuery));
        }
    }

    private boolean isAllocationNode(Val fact, ForwardQuery sourceQuery) {
        return fact.equals(sourceQuery.var());
    }

    private ObservableICFG<Unit, SootMethod> bwicfg() {
        if (bwicfg == null)
            bwicfg = new BackwardsObservableICFG(icfg());
        return bwicfg;
    }

    public ForwardBoomerangResults<W> solve(ForwardQuery query) {
        if (!analysisWatch.isRunning()) {
            analysisWatch.start();
        }
        boolean timedout = false;
        try {
            logger.debug("Starting forward analysis of: {}", query);
            forwardSolve(query);
            logger.debug("Terminated forward analysis of: {}", query);
        } catch (BoomerangTimeoutException e) {
            timedout = true;
            cleanup();
            logger.debug("Timeout of query: {}", query);
        }

        if (analysisWatch.isRunning()) {
            analysisWatch.stop();
        }
        return new ForwardBoomerangResults<W>(query, icfg(), timedout, this.queryToSolvers, getStats(), analysisWatch);
    }

    public BackwardBoomerangResults<W> solve(BackwardQuery query) {
        icfg().addUnbalancedMethod(query.stmt().getMethod());
        return solve(query, true);
    }

    public BackwardBoomerangResults<W> solve(BackwardQuery query, boolean timing) {
        if (timing && !analysisWatch.isRunning()) {
            analysisWatch.start();
        }
        boolean timedout = false;
        try {
            logger.debug("Starting backward analysis of: {}", query);
            backwardSolve(query);
            logger.debug("Terminated backward analysis of: {}", query);
        } catch (BoomerangTimeoutException e) {
            timedout = true;
            cleanup();
            logger.debug("Timeout of query: {}", query);
        }
        if (timing && analysisWatch.isRunning()) {
            analysisWatch.stop();
        }

        return new BackwardBoomerangResults<W>(query, timedout, this.queryToSolvers, getStats(), analysisWatch);
    }

    protected void backwardSolve(BackwardQuery query) {
        if (!options.aliasing())
            return;
        Optional<Stmt> unit = query.asNode().stmt().getUnit();
        AbstractBoomerangSolver<W> solver = queryToSolvers.getOrCreate(query);
        if (unit.isPresent()) {
            solver.solve(query.asNode());
        }
    }

    private AbstractBoomerangSolver<W> forwardSolve(ForwardQuery query) {
        Optional<Stmt> unit = query.asNode().stmt().getUnit();
        AbstractBoomerangSolver<W> solver = queryToSolvers.getOrCreate(query);
        if (unit.isPresent()) {
            if (isMultiArrayAllocation(unit.get()) && options.arrayFlows()) {
                // TODO fix; adjust as below;
                SingleNode<Node<Statement, Val>> sourveVal = new SingleNode<Node<Statement, Val>>(query.asNode());
                GeneratedState<Node<Statement, Val>, Field> genState = new GeneratedState<Node<Statement, Val>, Field>(
                        sourveVal, Field.array());
                insertTransition(solver.getFieldAutomaton(),
                        new Transition<Field, INode<Node<Statement, Val>>>(sourveVal, Field.array(), genState));
                insertTransition(solver.getFieldAutomaton(), new Transition<Field, INode<Node<Statement, Val>>>(
                        genState, Field.empty(), solver.getFieldAutomaton().getInitialState()));
            }
            if (isStringAllocation(unit.get())) {
                // Scene.v().forceResolve("java.lang.String",
                // SootClass.BODIES);
                SootClass stringClass = Scene.v().getSootClass("java.lang.String");
                if (stringClass.declaresField("char[] value")) {
                    SootField valueField = stringClass.getFieldByName("value");
                    SingleNode<Node<Statement, Val>> s = new SingleNode<Node<Statement, Val>>(query.asNode());
                    INode<Node<Statement, Val>> irState = solver.getFieldAutomaton().createState(s,
                            new Field(valueField));
                    insertTransition(solver.getFieldAutomaton(), new Transition<Field, INode<Node<Statement, Val>>>(
                            new SingleNode<Node<Statement, Val>>(query.asNode()), new Field(valueField), irState));
                    insertTransition(solver.getFieldAutomaton(), new Transition<Field, INode<Node<Statement, Val>>>(
                            irState, Field.empty(), solver.getFieldAutomaton().getInitialState()));
                }
            }
            if (query instanceof WeightedForwardQuery) {
                WeightedForwardQuery<W> q = (WeightedForwardQuery<W>) query;
                solver.solve(q.asNode(), q.weight());
            } else {
                solver.solve(query.asNode());
            }
        }
        return solver;
    }

    private boolean isStringAllocation(Stmt stmt) {
        if (stmt instanceof AssignStmt && ((AssignStmt) stmt).getRightOp() instanceof StringConstant) {
            return true;
        }
        return false;
    }

    private boolean insertTransition(final WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut,
            Transition<Field, INode<Node<Statement, Val>>> transition) {
        if (!aut.nested()) {
            return aut.addTransition(transition);
        }
        INode<Node<Statement, Val>> target = transition.getTarget();
        if (!(target instanceof GeneratedState)) {
            forwardFieldSummaries.putSummaryAutomaton(target, aut);

            aut.registerListener(new WPAUpdateListener<Field, INode<Node<Statement, Val>>, W>() {

                @Override
                public void onWeightAdded(Transition<Field, INode<Node<Statement, Val>>> t, W w,
                        WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
                    if (t.getStart() instanceof GeneratedState) {
                        WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> n = forwardFieldSummaries
                                .getSummaryAutomaton(t.getStart());
                        aut.addNestedAutomaton(n);
                    }
                }
            });
            return aut.addTransition(transition);
        }
        final WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> nested = forwardFieldSummaries
                .getSummaryAutomaton(target);
        nested.registerListener(new WPAUpdateListener<Field, INode<Node<Statement, Val>>, W>() {

            @Override
            public void onWeightAdded(Transition<Field, INode<Node<Statement, Val>>> t, W w,
                    WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> aut) {
                if (t.getStart() instanceof GeneratedState) {
                    WeightedPAutomaton<Field, INode<Node<Statement, Val>>, W> n = forwardFieldSummaries
                            .getSummaryAutomaton(t.getStart());
                    aut.addNestedAutomaton(n);
                }
            }
        });
        return nested.addTransition(transition);
    }

    private boolean isMultiArrayAllocation(Stmt stmt) {
        return (stmt instanceof AssignStmt) && ((AssignStmt) stmt).getRightOp() instanceof NewMultiArrayExpr;
    }

    public class FieldWritePOI extends AbstractPOI<Statement, Val, Field> {

        public FieldWritePOI(Statement statement, Val base, Field field, Val stored) {
            super(statement, base, field, stored);
        }

        @Override
        public void execute(final ForwardQuery baseAllocation, final Query flowAllocation) {
            if (flowAllocation instanceof BackwardQuery) {
            } else if (flowAllocation instanceof ForwardQuery) {
                AbstractBoomerangSolver<W> baseSolver = queryToSolvers.get(baseAllocation);
                AbstractBoomerangSolver<W> flowSolver = queryToSolvers.get(flowAllocation);
                ExecuteImportFieldStmtPOI<W> exec = new ExecuteImportFieldStmtPOI<W>(WeightedBoomerang.this, baseSolver,
                        flowSolver, FieldWritePOI.this, getStmt()) {
                    public void activate(INode<Node<Statement, Val>> start) {
                        activateAllPois(new SolverPair(flowSolver, baseSolver), start);
                    };
                };
                registerActivationListener(new SolverPair(flowSolver, baseSolver), exec);
                exec.solve();
            }
        }

    }

    public class FieldReadPOI extends AbstractPOI<Statement, Val, Field> {

        public FieldReadPOI(Statement statement, Val base, Field field, Val stored) {
            super(statement, base, field, stored);
        }

        @Override
        public void execute(final ForwardQuery baseAllocation, final Query flowAllocation) {
            if (WeightedBoomerang.this instanceof WholeProgramBoomerang)
                throw new RuntimeException("should not be invoked!");
            if (flowAllocation instanceof ForwardQuery) {
            } else if (flowAllocation instanceof BackwardQuery) {
                AbstractBoomerangSolver<W> baseSolver = queryToSolvers.get(baseAllocation);
                AbstractBoomerangSolver<W> flowSolver = queryToSolvers.get(flowAllocation);
                for (Statement succ : flowSolver.getSuccsOf(getStmt())) {
                    ExecuteImportFieldStmtPOI<W> exec = new ExecuteImportFieldStmtPOI<W>(WeightedBoomerang.this,
                            baseSolver, flowSolver, FieldReadPOI.this, succ) {
                        public void activate(INode<Node<Statement, Val>> start) {
                            activateAllPois(new SolverPair(flowSolver, baseSolver), start);
                        };
                    };
                    registerActivationListener(new SolverPair(flowSolver, baseSolver), exec);
                    exec.solve();
                }
            }
        }

    }

    protected void activateAllPois(SolverPair pair, INode<Node<Statement, Val>> start) {
        if (activatedPoi.put(pair, start)) {
            Collection<ExecuteImportFieldStmtPOI<W>> listeners = poiListeners.get(pair);
            for (ExecuteImportFieldStmtPOI<W> l : Lists.newArrayList(listeners)) {
                l.trigger(start);
            }
        }
    }

    public void registerActivationListener(WeightedBoomerang<W>.SolverPair solverPair,
            ExecuteImportFieldStmtPOI<W> exec) {
        Collection<INode<Node<Statement, Val>>> listeners = activatedPoi.get(solverPair);
        for (INode<Node<Statement, Val>> node : Lists.newArrayList(listeners)) {
            exec.trigger(node);
        }
        poiListeners.put(solverPair, exec);
    }

    private class SolverPair {

        private AbstractBoomerangSolver<W> flowSolver;
        private AbstractBoomerangSolver<W> baseSolver;

        public SolverPair(AbstractBoomerangSolver<W> flowSolver, AbstractBoomerangSolver<W> baseSolver) {
            this.flowSolver = flowSolver;
            this.baseSolver = baseSolver;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((baseSolver == null) ? 0 : baseSolver.hashCode());
            result = prime * result + ((flowSolver == null) ? 0 : flowSolver.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SolverPair other = (SolverPair) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (baseSolver == null) {
                if (other.baseSolver != null)
                    return false;
            } else if (!baseSolver.equals(other.baseSolver))
                return false;
            if (flowSolver == null) {
                if (other.flowSolver != null)
                    return false;
            } else if (!flowSolver.equals(other.flowSolver))
                return false;
            return true;
        }

        private WeightedBoomerang getOuterType() {
            return WeightedBoomerang.this;
        }

    }

    public void createPOI(BiDiInterproceduralCFG<Unit, SootMethod> icfg, AbstractBoomerangSolver<W> baseSolver,
            AbstractBoomerangSolver<W> flowSolver, WeightedBoomerang<W>.FieldReadPOI fieldReadPOI, Statement succ) {
        // TODO Auto-generated method stub

    }

    public abstract ObservableICFG<Unit, SootMethod> icfg();

    protected abstract WeightFunctions<Statement, Val, Field, W> getForwardFieldWeights();

    protected abstract WeightFunctions<Statement, Val, Field, W> getBackwardFieldWeights();

    protected abstract WeightFunctions<Statement, Val, Statement, W> getBackwardCallWeights();

    protected abstract WeightFunctions<Statement, Val, Statement, W> getForwardCallWeights(ForwardQuery sourceQuery);

    public DefaultValueMap<Query, AbstractBoomerangSolver<W>> getSolvers() {
        return queryToSolvers;
    }

    public abstract Debugger<W> createDebugger();

    public void debugOutput() {
        Debugger<W> debugger = getOrCreateDebugger();
        debugger.done(queryToSolvers);
    }

    public Debugger<W> getOrCreateDebugger() {
        if (this.debugger == null)
            this.debugger = createDebugger();
        return debugger;
    }

    public SeedFactory<W> getSeedFactory() {
        return null;
    }

    public IBoomerangStats<W> getStats() {
        return stats;
    }

    public void onCreateSubSolver(Query key, AbstractBoomerangSolver<W> solver) {
        for (SolverCreationListener<W> l : solverCreationListeners) {
            l.onCreatedSolver(key, solver);
        }
    }

    public void registerSolverCreationListener(SolverCreationListener<W> l) {
        if(solverCreationListeners.add(l)) {
          for(Entry<Query, AbstractBoomerangSolver<W>> e : Lists.newArrayList(queryToSolvers.entrySet())) {
            l.onCreatedSolver(e.getKey(), e.getValue());
          }
        }
    }

    public Table<Statement, Val, W> getResults(Query seed) {
        final Table<Statement, Val, W> results = HashBasedTable.create();
        WeightedPAutomaton<Statement, INode<Val>, W> fieldAut = queryToSolvers.getOrCreate(seed).getCallAutomaton();
        for (Entry<Transition<Statement, INode<Val>>, W> e : fieldAut.getTransitionsToFinalWeights().entrySet()) {
            Transition<Statement, INode<Val>> t = e.getKey();
            W w = e.getValue();
            if (t.getLabel().equals(Statement.epsilon()))
                continue;
            if (t.getStart().fact().value() instanceof Local
                    && !t.getLabel().getMethod().equals(t.getStart().fact().m()))
                continue;
            if (t.getLabel().getUnit().isPresent())
                results.put(t.getLabel(), t.getStart().fact(), w);
        }
        return results;
    }

    public BoomerangOptions getOptions() {
        return this.options;
    }

}
