/*
 * Bytecode Analysis Framework
 * Copyright (C) 2004 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.type;

import java.io.Serializable;
import java.util.BitSet;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.ReferenceType;
import org.apache.bcel.generic.Type;

import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.ch.Subtypes2;

/**
 * Class for keeping track of exceptions that can be thrown by an instruction.
 * We distinguish <em>explicit</em> and <em>implicit</em> exceptions. Explicit
 * exceptions are explicitly declared, thrown, or caught. Implicit exceptions
 * are runtime faults (NPE, array out of bounds) not explicitly handled by the
 * user code.
 *
 * @author David Hovemeyer
 * @see TypeAnalysis
 */
public class ExceptionSet implements Serializable {
    private static final long serialVersionUID = 1;

    private final ExceptionSetFactory factory;

    private final BitSet exceptionSet;

    private final BitSet explicitSet;

    private int size;

    private boolean universalHandler;

    private Type commonSupertype;

    /**
     * Object to iterate over the exception types in the set.
     */
    public class ThrownExceptionIterator implements Iterator<ObjectType> {
        private int last = -1, next = -1;

        ThrownExceptionIterator() {
            findNext();
        }

        @Override
        public boolean hasNext() {
            if (last == next) {
                findNext();
            }
            return next < factory.getNumTypes();
        }

        @Override
        public ObjectType next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            ObjectType result = factory.getType(next);
            last = next;
            return result;
        }

        public boolean isExplicit() {
            return explicitSet.get(last);
        }

        @Override
        public void remove() {
            exceptionSet.clear(last);
            explicitSet.clear(last);
            --size;
            commonSupertype = null;
        }

        private void findNext() {
            ++next;
            while (next < factory.getNumTypes()) {
                if (exceptionSet.get(next)) {
                    break;
                }
                ++next;
            }
        }
    }

    /**
     * Constructor. Creates an empty set.
     */
    ExceptionSet(ExceptionSetFactory factory) {
        this.factory = factory;
        this.exceptionSet = new BitSet();
        this.explicitSet = new BitSet();
        this.size = 0;
        this.universalHandler = false;
    }

    /**
     * Return an exact copy of this object.
     */
    public ExceptionSet duplicate() {
        ExceptionSet dup = factory.createExceptionSet();
        dup.exceptionSet.clear();
        dup.exceptionSet.or(this.exceptionSet);
        dup.explicitSet.clear();
        dup.explicitSet.or(this.explicitSet);
        dup.size = this.size;
        dup.universalHandler = this.universalHandler;
        dup.commonSupertype = this.commonSupertype;

        return dup;
    }

    @Override
    public int hashCode() {
        return exceptionSet.hashCode() + explicitSet.hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (o.getClass() != this.getClass()) {
            return false;
        }

        ExceptionSet other = (ExceptionSet) o;
        return exceptionSet.equals(other.exceptionSet) && explicitSet.equals(other.explicitSet)
                && universalHandler == other.universalHandler;
    }

    /**
     * Get the least (lowest in the lattice) common supertype of the exceptions
     * in the set. Returns the special TOP type if the set is empty.
     */
    public Type getCommonSupertype() throws ClassNotFoundException {
        if (commonSupertype != null) {
            return commonSupertype;
        }

        if (isEmpty()) {
            // This probably means that we're looking at an
            // infeasible exception path.
            return TypeFrame.getTopType();
        }

        // Compute first common superclass
        ThrownExceptionIterator i = iterator();
        ReferenceType result = i.next();
        while (i.hasNext()) {
            if (Subtypes2.ENABLE_SUBTYPES2_FOR_COMMON_SUPERCLASS_QUERIES) {
                result = AnalysisContext.currentAnalysisContext().getSubtypes2().getFirstCommonSuperclass(result, i.next());
            } else {
                result = result.getFirstCommonSuperclass(i.next());
            }
            if (result == null) {
                // This should only happen if the class hierarchy
                // is incomplete. We'll just be conservative.
                result = Type.THROWABLE;
                break;
            }
        }

        // Cache and return the result
        commonSupertype = result;
        return result;
    }

    /**
     * Return an iterator over thrown exceptions.
     */
    public ThrownExceptionIterator iterator() {
        return new ThrownExceptionIterator();
    }

    /**
     * Return whether or not the set is empty.
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * Checks to see if the exception set is a singleton set containing just the
     * named exception
     *
     * @param exceptionName
     *            (in dotted format)
     * @return true if it is
     */
    public boolean isSingleton(String exceptionName) {
        if (size != 1) {
            return false;
        }
        ObjectType e = iterator().next();
        return e.toString().equals(exceptionName);

    }

    /**
     * Add an explicit exception.
     *
     * @param type
     *            type of the exception
     */
    public void addExplicit(ObjectType type) {
        add(type, true);
    }

    /**
     * Add an implicit exception.
     *
     * @param type
     *            type of the exception
     */
    public void addImplicit(ObjectType type) {
        add(type, false);
    }

    /**
     * Add an exception.
     *
     * @param type
     *            the exception type
     * @param explicit
     *            true if the exception is explicitly declared or thrown, false
     *            if implicit
     */
    public void add(ObjectType type, boolean explicit) {
        int index = factory.getIndexOfType(type);
        if (!exceptionSet.get(index)) {
            ++size;
        }
        exceptionSet.set(index);
        if (explicit) {
            explicitSet.set(index);
        }

        commonSupertype = null;
    }

    /**
     * Add all exceptions in the given set.
     *
     * @param other
     *            the set
     */
    public void addAll(ExceptionSet other) {
        exceptionSet.or(other.exceptionSet);
        explicitSet.or(other.explicitSet);
        size = countBits(exceptionSet);

        commonSupertype = null;
    }

    private int countBits(BitSet bitSet) {
        int count = 0;
        for (int i = 0; i < factory.getNumTypes(); ++i) {
            if (bitSet.get(i)) {
                ++count;
            }
        }
        return count;
    }

    /**
     * Remove all exceptions from the set.
     */
    public void clear() {
        exceptionSet.clear();
        explicitSet.clear();
        universalHandler = false;
        commonSupertype = null;
        size = 0;
    }

    /**
     * Return whether or not a universal exception handler was reached by the
     * set.
     */
    public void sawUniversal() {
        clear();
        universalHandler = true;
    }

    /**
     * Mark the set as having reached a universal exception handler.
     */
    public boolean sawUniversalHandler() {
        return universalHandler;
    }

    /**
     * Return whether or not the set contains any checked exceptions.
     */
    public boolean containsCheckedExceptions() throws ClassNotFoundException {
        for (ThrownExceptionIterator i = iterator(); i.hasNext();) {
            ObjectType type = i.next();
            if (!Hierarchy.isUncheckedException(type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return whether or not the set contains any explicit exceptions.
     */
    public boolean containsExplicitExceptions() {
        for (ThrownExceptionIterator i = iterator(); i.hasNext();) {
            i.next();
            if (i.isExplicit()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append('{');
        boolean first = true;
        for (ThrownExceptionIterator i = iterator(); i.hasNext();) {
            ObjectType type = i.next();
            if (first) {
                first = false;
            } else {
                buf.append(',');
            }
            boolean implicit = !i.isExplicit();
            if (implicit) {
                buf.append('[');
            }
            buf.append(type.toString());
            if (implicit) {
                buf.append(']');
            }
        }
        buf.append('}');
        return buf.toString();
    }

    public int size() {
        return size;
    }
}

