/*
 * Bytecode Analysis Framework
 * Copyright (C) 2005,2008 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.obl;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import edu.umd.cs.findbugs.ba.Path;

/**
 * A dataflow fact used in ObligationAnalysis. It is a set of State objects,
 * plus the additional capability to represent top and bottom elements.
 *
 * <p>
 * Invariant: no StateSet may contain more than one State with the same
 * ObligationSet.
 * </p>
 *
 * <p>
 * See Weimer and Necula, <a href="http://doi.acm.org/10.1145/1028976.1029011"
 * >Finding and preventing run-time error handling mistakes</a>, OOPSLA 2004.
 * </p>
 *
 * @author David Hovemeyer
 */
public class StateSet {
    private boolean isTop;

    private boolean isBottom;

    private boolean onExceptionPath;

    private Map<ObligationSet, State> stateMap;

    public boolean isEmpty() {
        return stateMap.isEmpty();
    }

    private final ObligationFactory factory;

    public StateSet(ObligationFactory factory) {
        this.isTop = this.isBottom = false;
        this.stateMap = new HashMap<ObligationSet, State>();
        this.factory = factory;
    }

    public void setTop() {
        this.isTop = true;
        this.isBottom = false;
        this.onExceptionPath = false;
        this.stateMap.clear();
    }

    public boolean isTop() {
        return isTop;
    }

    public void setBottom() {
        this.isBottom = true;
        this.isTop = false;
    }

    public boolean isBottom() {
        return this.isBottom;
    }

    public boolean isValid() {
        return !this.isTop && !this.isBottom;
    }

    public boolean isOnExceptionPath() {
        return onExceptionPath;
    }

    public void setOnExceptionPath(boolean onExceptionPath) {
        this.onExceptionPath = onExceptionPath;
    }

    public void clear() {
        this.isTop = this.isBottom = this.onExceptionPath = false;
        stateMap.clear();
    }

    /**
     * Return an Iterator over the States in the StateSet.
     *
     * @return an Iterator over the States in the StateSet
     */
    public Iterator<State> stateIterator() {
        return stateMap.values().iterator();
    }

    /**
     * Get Set of all ObligationsSets in this StateSet.
     *
     * @return Set of all ObligationsSets in this StateSet
     */
    public Set<ObligationSet> getAllObligationSets() {
        return Collections.unmodifiableSet(stateMap.keySet());
    }

    /**
     * Get the State which has the given ObligationSet. Returns null if there is
     * no such state.
     *
     * @param obligationSet
     *            we want to get the State with this ObligationSet
     * @return the State with the given ObligationSet, or null if there is no
     *         such State
     */
    public State getStateWithObligationSet(ObligationSet obligationSet) {
        return stateMap.get(obligationSet);
    }

    // /**
    // * Initialize this object as the entry fact for a method:
    // * a single state with empty obligation set and path.
    // *
    // * @param factory the ObligationFactory used for the analysis
    // */
    // public void initEntryFact(ObligationFactory factory) {
    // this.isTop = this.isBottom = false;
    // this.stateMap.clear();
    //
    // // Add initial fact: empty obligations, empty path
    // State initState = new State(factory);
    // this.stateMap.put(initState.getObligationSet(), initState);
    // }

    /**
     * Make this StateSet an exact copy of the given StateSet.
     *
     * @param other
     *            a StateSet; this StateSet will be made identical to it
     */
    public void copyFrom(StateSet other) {
        this.isTop = other.isTop;
        this.isBottom = other.isBottom;
        this.onExceptionPath = other.onExceptionPath;
        this.stateMap.clear();
        for (State state : other.stateMap.values()) {
            State dup = state.duplicate();
            this.stateMap.put(dup.getObligationSet(), dup);
        }
    }

    /**
     * Return an exact deep copy of this StateSet.
     *
     * @return an exact deep copy of this StateSet
     */
    public StateSet duplicate() {
        StateSet dup = new StateSet(factory);
        dup.copyFrom(this);
        return dup;
    }

    /**
     * Add an obligation to every State in the StateSet.
     *
     * @param obligation
     *            the obligation to add
     * @param basicBlockId
     *            the id of the basic block (path component) adding the
     *            obligation
     */
    public void addObligation(final Obligation obligation, int basicBlockId) throws ObligationAcquiredOrReleasedInLoopException {
        Map<ObligationSet, State> updatedStateMap = new HashMap<ObligationSet, State>();
        if (stateMap.isEmpty()) {
            State s = new State(factory);
            s.getObligationSet().add(obligation);
            updatedStateMap.put(s.getObligationSet(), s);
        } else {
            for (State state : stateMap.values()) {
                checkCircularity(state, obligation, basicBlockId);
                state.getObligationSet().add(obligation);
                updatedStateMap.put(state.getObligationSet(), state);

            }
        }
        replaceMap(updatedStateMap);
    }

    /**
     * Remove an Obligation from every State in the StateSet.
     *
     * @param obligation
     *            the obligation to remove
     * @param basicBlockId
     *            the id of the basic block (path component) removing the
     *            obligation
     * @throws ObligationAcquiredOrReleasedInLoopException
     */
    public void deleteObligation(final Obligation obligation, int basicBlockId)
            throws ObligationAcquiredOrReleasedInLoopException {
        Map<ObligationSet, State> updatedStateMap = new HashMap<ObligationSet, State>();
        for (Iterator<State> i = stateIterator(); i.hasNext();) {
            State state = i.next();
            checkCircularity(state, obligation, basicBlockId);
            ObligationSet obligationSet = state.getObligationSet();
            obligationSet.remove(obligation);
            if (!obligationSet.isEmpty()) {
                updatedStateMap.put(obligationSet, state);
            }
        }
        replaceMap(updatedStateMap);
    }

    /**
     * Bail out of the analysis is an obligation is acquired or released in a
     * loop.
     *
     * @param state
     *            a State to which an obligation is being added or removed
     * @param obligation
     *            the Obligation being added or removed
     * @param basicBlockId
     *            the id of the BasicBlock adding or removing the obligation
     */
    private void checkCircularity(State state, Obligation obligation, int basicBlockId)
            throws ObligationAcquiredOrReleasedInLoopException {
        if (state.getPath().hasComponent(basicBlockId)) {
            throw new ObligationAcquiredOrReleasedInLoopException(obligation);
        }
    }

    /**
     * Replace the map of ObligationSets to States with the given one.
     *
     * @param stateMap
     *            enw map of ObligationSets to States
     */
    public void replaceMap(Map<ObligationSet, State> stateMap) {
        this.stateMap = stateMap;
    }

    /**
     * Get all States that have Paths which are prefixes of the given Path.
     *
     * @param path
     *            a Path
     * @return Collection of States that have Paths which are prefixes of the
     *         given Path
     */
    public List<State> getPrefixStates(Path path) {
        List<State> result = new LinkedList<State>();
        for (State state : stateMap.values()) {
            if (state.getPath().isPrefixOf(path)) {
                result.add(state);
            }
        }
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        StateSet other = (StateSet) o;
        return this.isTop == other.isTop && this.isBottom == other.isBottom
                && this.onExceptionPath == other.onExceptionPath && this.stateMap.equals(other.stateMap);
    }

    @Override
    public int hashCode() {
        throw new UnsupportedOperationException();
    }

    @Override
    public String toString() {
        if (isTop) {
            return "TOP";
        } else if (isBottom) {
            return "BOTTOM";
        } else {
            StringBuilder buf = new StringBuilder();
            buf.append(stateMap);

            if (onExceptionPath) {
                buf.append(" On exception path");
            }
            return buf.toString();
        }
    }

    /**
     * Return a newly allocated Map of ObligationSet to State that may be passed
     * to applyToAllStatesAndUpdateMap().
     */
    public Map<ObligationSet, State> createEmptyMap() {
        return new HashMap<ObligationSet, State>();
    }
}

