/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2006, University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba.deref;

import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.annotation.CheckForNull;

import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.vna.ValueNumber;
import edu.umd.cs.findbugs.ba.vna.ValueNumberFactory;
import edu.umd.cs.findbugs.ba.vna.ValueNumberFrame;
import edu.umd.cs.findbugs.util.Util;

/**
 * A set of values unconditionally dereferenced in the future.
 *
 * @author David Hovemeyer
 */
public class UnconditionalValueDerefSet {
    /** Number of distinct value numbers in method */
    private final int numValueNumbersInMethod;

    /** Set of value numbers unconditionally dereferenced */
    private final BitSet valueNumbersUnconditionallyDereferenced;

    /** Map of value numbers to locations */
    private final Map<ValueNumber, Set<Location>> derefLocationSetMap;

    boolean resultsFromBackEdge = false;

    int backEdgeUpdateCount = 0;

    private int lastUpdateTimestamp;

    /**
     * Constructor.
     *
     * @param numValueNumbersInMethod
     *            number of distinct value numbers in method
     */
    public UnconditionalValueDerefSet(int numValueNumbersInMethod) {
        this.numValueNumbersInMethod = numValueNumbersInMethod;
        this.valueNumbersUnconditionallyDereferenced = new BitSet();
        this.derefLocationSetMap = new HashMap<ValueNumber, Set<Location>>(3);

    }

    /**
     * Is this the bottom value?
     *
     * @return true if this is the bottom value, false otherwise
     */
    public boolean isBottom() {
        return valueNumbersUnconditionallyDereferenced.get(numValueNumbersInMethod);
    }

    /**
     * Make this dataflow fact the bottom value.
     */
    public void setIsBottom() {
        clear();
        valueNumbersUnconditionallyDereferenced.set(numValueNumbersInMethod);
    }

    /**
     * Is this the top value?
     *
     * @return true if this is the top value, false otherwise
     */
    public boolean isTop() {
        return valueNumbersUnconditionallyDereferenced.get(numValueNumbersInMethod + 1);
    }

    /**
     * Make this dataflow fact the top value.
     */
    public void setIsTop() {
        clear();
        valueNumbersUnconditionallyDereferenced.set(numValueNumbersInMethod + 1);
        lastUpdateTimestamp = 0;
    }

    /**
     * Clear the deref set. This sets the fact so it is valid as the dataflow
     * entry fact: no future dereferences are guaranteed.
     */
    void clear() {
        valueNumbersUnconditionallyDereferenced.clear();
        derefLocationSetMap.clear();
    }

    /**
     * Make this dataflow fact the same as the given one.
     *
     * @param source
     *            another dataflow fact
     */
    public void makeSameAs(UnconditionalValueDerefSet source) {
        // Copy value numbers
        valueNumbersUnconditionallyDereferenced.clear();
        valueNumbersUnconditionallyDereferenced.or(source.valueNumbersUnconditionallyDereferenced);
        lastUpdateTimestamp = source.lastUpdateTimestamp;
        // Copy dereference locations for each value number
        derefLocationSetMap.clear();
        if (source.derefLocationSetMap.size() > 0) {
            for (Map.Entry<ValueNumber, Set<Location>> sourceEntry : source.derefLocationSetMap.entrySet()) {
                Set<Location> derefLocationSet = Util.makeSmallHashSet(sourceEntry.getValue());
                derefLocationSetMap.put(sourceEntry.getKey(), derefLocationSet);
            }
        }
    }

    /**
     * Return whether or not this dataflow fact is identical to the one given.
     *
     * @param otherFact
     *            another dataflow fact
     * @return true if the other dataflow fact is identical to this one, false
     *         otherwise
     */
    public boolean isSameAs(UnconditionalValueDerefSet otherFact) {
        return valueNumbersUnconditionallyDereferenced.equals(otherFact.valueNumbersUnconditionallyDereferenced)
                && derefLocationSetMap.equals(otherFact.derefLocationSetMap);
    }

    /**
     * Merge given dataflow fact into this one. We take the intersection of the
     * unconditional deref value number set, and union the deref locations.
     *
     * @param fact
     *            another dataflow fact
     * @param skipMe
     *            TODO
     */
    public void mergeWith(UnconditionalValueDerefSet fact, @CheckForNull ValueNumber skipMe, ValueNumberFactory valueNumberFactory) {
        if (UnconditionalValueDerefAnalysis.DEBUG) {
            System.out.println("merge update of # " + System.identityHashCode(this) + " from " + System.identityHashCode(fact));
            System.out.println("update " + this);
            System.out.println("with " + fact);

        }
        boolean resultForSkippedValue = false;
        if (skipMe != null) {
            resultForSkippedValue = valueNumbersUnconditionallyDereferenced.get(skipMe.getNumber());
        }
        // Compute the intersection of the unconditionally dereferenced value
        // sets
        valueNumbersUnconditionallyDereferenced.and(fact.valueNumbersUnconditionallyDereferenced);
        if (skipMe != null) {
            valueNumbersUnconditionallyDereferenced.set(skipMe.getNumber(), resultForSkippedValue);
        }

        // For each unconditionally dereferenced value...
        for (int i = 0; i < numValueNumbersInMethod; i++) {
            ValueNumber vn = valueNumberFactory.forNumber(i);
            if (vn.equals(skipMe)) {
                continue;
            }
            Set<Location> factDerefLocationSet = fact.derefLocationSetMap.get(vn);
            if (valueNumbersUnconditionallyDereferenced.get(i)) {
                if (factDerefLocationSet != null && !factDerefLocationSet.isEmpty()) {
                    // Compute the union of the dereference locations for
                    // this value number.
                    Set<Location> derefLocationSet = derefLocationSetMap.get(vn);
                    if (derefLocationSet == null) {
                        derefLocationSet = new HashSet<Location>();
                        derefLocationSetMap.put(vn, derefLocationSet);
                    }
                    derefLocationSet.addAll(fact.derefLocationSetMap.get(vn));
                }
            } else {
                Set<Location> removed = derefLocationSetMap.remove(vn);
                // The value number is not in the fact:
                // remove its location set
                if (removed != null) {
                    if (UnconditionalValueDerefAnalysis.DEBUG) {
                        System.out.println("Goodbye: " + removed);
                    }
                }
            }
        }
    }

    public void unionWith(UnconditionalValueDerefSet fact, ValueNumberFactory valueNumberFactory) {
        if (UnconditionalValueDerefAnalysis.DEBUG) {
            System.out.println("union update of # " + System.identityHashCode(this) + " from " + System.identityHashCode(fact));
        }
        // Compute the union of the unconditionally dereferenced value sets
        valueNumbersUnconditionallyDereferenced.or(fact.valueNumbersUnconditionallyDereferenced);

        // For each unconditionally dereferenced value...
        for (int i = 0; i < numValueNumbersInMethod; i++) {
            ValueNumber vn = valueNumberFactory.forNumber(i);

            if (fact.valueNumbersUnconditionallyDereferenced.get(i)) {
                // Compute the union of the dereference locations for
                // this value number.
                Set<Location> derefLocationSet = derefLocationSetMap.get(vn);
                if (derefLocationSet == null) {
                    derefLocationSet = new HashSet<Location>();
                    derefLocationSetMap.put(vn, derefLocationSet);
                }
                derefLocationSet.addAll(fact.derefLocationSetMap.get(vn));
            } else {
                derefLocationSetMap.put(vn, new HashSet<Location>(fact.getDerefLocationSet(vn)));
            }
        }
    }

    /**
     * Mark a value as being dereferenced at given Location.
     *
     * @param vn
     *            the value
     * @param location
     *            the Location
     */
    public void addDeref(ValueNumber vn, Location location) {
        if (UnconditionalValueDerefAnalysis.DEBUG) {
            System.out.println("Adding dereference of " + vn + " to # " + System.identityHashCode(this) + " @ " + location);
        }
        valueNumbersUnconditionallyDereferenced.set(vn.getNumber());

        Set<Location> derefLocationSet = getDerefLocationSet(vn);
        derefLocationSet.add(location);
    }

    /**
     * Set a value as being unconditionally dereferenced at the given set of
     * locations.
     *
     * @param vn
     *            the value
     * @param derefSet
     *            the Set of dereference Locations
     */
    public void setDerefSet(ValueNumber vn, Set<Location> derefSet) {
        if (UnconditionalValueDerefAnalysis.DEBUG) {
            System.out.println("Adding dereference of " + vn + " for # " + System.identityHashCode(this) + " to " + derefSet);
        }
        valueNumbersUnconditionallyDereferenced.set(vn.getNumber());

        Set<Location> derefLocationSet = getDerefLocationSet(vn);
        derefLocationSet.clear();
        derefLocationSet.addAll(derefSet);
    }

    /**
     * Clear the set of dereferences for given ValueNumber
     *
     * @param value
     *            the ValueNumber
     */
    public void clearDerefSet(ValueNumber value) {
        if (UnconditionalValueDerefAnalysis.DEBUG) {
            System.out.println("Clearing dereference of " + value + " for # " + System.identityHashCode(this));
        }
        valueNumbersUnconditionallyDereferenced.clear(value.getNumber());
        derefLocationSetMap.remove(value);
    }

    /**
     * Get the set of dereference Locations for given value number.
     *
     * @param vn
     *            the value number
     * @return the set of dereference Locations
     */
    public Set<Location> getDerefLocationSet(ValueNumber vn) {
        Set<Location> derefLocationSet = derefLocationSetMap.get(vn);
        if (derefLocationSet == null) {
            derefLocationSet = new HashSet<Location>();
            derefLocationSetMap.put(vn, derefLocationSet);
        }
        return derefLocationSet;
    }

    /**
     * Return whether or not the given value number is unconditionally
     * dereferenced.
     *
     * @param vn
     *            the value number
     * @return true if the value is unconditionally dereferenced, false
     *         otherwise
     */
    public boolean isUnconditionallyDereferenced(ValueNumber vn) {
        return valueNumbersUnconditionallyDereferenced.get(vn.getNumber());
    }

    public Set<ValueNumber> getValueNumbersThatAreUnconditionallyDereferenced() {
        HashSet<ValueNumber> result = new HashSet<ValueNumber>();
        for (Map.Entry<ValueNumber, Set<Location>> e : derefLocationSetMap.entrySet()) {
            if (!e.getValue().isEmpty()) {
                result.add(e.getKey());
            }
        }
        return result;
    }

    public void retainOnlyTheseValueNumbers(Collection<ValueNumber> valueNumbers) {
        for (Iterator<ValueNumber> i = derefLocationSetMap.keySet().iterator(); i.hasNext();) {
            ValueNumber v = i.next();
            if (!valueNumbers.contains(v)) {
                i.remove();
                valueNumbersUnconditionallyDereferenced.clear(v.getNumber());
            }
        }
    }

    /**
     * Get the set of Locations where given value is guaranteed to be
     * dereferenced. (I.e., if non-implicit-exception control paths are
     * followed, one of these locations will be reached).
     *
     * @param vn
     *            the value
     * @return set of Locations, one of which will definitely be reached if
     *         non-implicit-exception control paths are followed
     */
    public Set<Location> getUnconditionalDerefLocationSet(ValueNumber vn) {
        Set<Location> derefLocationSet = derefLocationSetMap.get(vn);
        if (derefLocationSet == null) {
            derefLocationSet = Collections.<Location> emptySet();
        }
        return derefLocationSet;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        if (isTop()) {
            return "[TOP]";
        }
        if (isBottom()) {
            return "[BOTTOM]";
        }

        StringBuilder buf = new StringBuilder();
        buf.append('[');
        boolean firstVN = true;
        for (int i = 0; i < numValueNumbersInMethod; i++) {
            if (!(valueNumbersUnconditionallyDereferenced.get(i))) {
                continue;
            }
            if (firstVN) {
                firstVN = false;
            } else {
                buf.append(',');
            }
            buf.append('{');
            buf.append(i);
            if (valueNumbersUnconditionallyDereferenced.get(i)) {
                buf.append(':');
            } else {
                buf.append('?');
            }
            TreeSet<Location> derefLocationSet = new TreeSet<Location>();
            derefLocationSet.addAll(getDerefLocationSet(i));
            boolean firstLoc = true;
            for (Location location : derefLocationSet) {
                if (firstLoc) {
                    firstLoc = false;
                } else {
                    buf.append(',');
                }
                buf.append("(" + location.getBasicBlock().getLabel() + ":" + location.getHandle().getPosition() + ")");
            }
            buf.append('}');
        }
        buf.append(']');
        return buf.toString();
    }

    private Set<Location> getDerefLocationSet(int vn) {
        for (Map.Entry<ValueNumber, Set<Location>> entry : derefLocationSetMap.entrySet()) {
            if (entry.getKey().getNumber() == vn) {
                return Collections.<Location> unmodifiableSet(entry.getValue());
            }
        }
        return new HashSet<Location>();
    }

    /**
     * @param location
     * @param vnaFrame
     */
    public void cleanDerefSet(@CheckForNull Location location, ValueNumberFrame vnaFrame) {

        Set<ValueNumber> valueNumbers = new HashSet<ValueNumber>(vnaFrame.allSlots());

        valueNumbers.addAll(vnaFrame.valueNumbersForLoads());

        if (UnconditionalValueDerefAnalysis.DEBUG) {
            for (ValueNumber v : getValueNumbersThatAreUnconditionallyDereferenced()) {
                if (!valueNumbers.contains(v)) {
                    System.out.println("\nWhy is " + v + " unconditionally dereferenced in #" + System.identityHashCode(this));
                    System.out.println("VN: " + vnaFrame);
                    System.out.println("UD: " + this);
                    System.out.println("Location: " + location);
                    System.out.println();
                }
            }

        }
        retainOnlyTheseValueNumbers(valueNumbers);
    }

    /**
     * @param lastUpdateTimestamp
     *            The lastUpdateTimestamp to set.
     */
    public void setLastUpdateTimestamp(int lastUpdateTimestamp) {
        this.lastUpdateTimestamp = lastUpdateTimestamp;
    }

    /**
     * @return Returns the lastUpdateTimestamp.
     */
    public int getLastUpdateTimestamp() {
        return lastUpdateTimestamp;
    }

    /**
     * @return {@code true} if the set is empty
     */
    public boolean isEmpty() {
        return valueNumbersUnconditionallyDereferenced.isEmpty();
    }
}
