// SelectionSet.java

import java.util.*;


/**
 * a "set" container which notifies listeners of membership changes
 * for example as a central container for coordinated selection 
 * between a group of viewer/listeners.<br>
 *
 * this class is somewhat similar to javax.swing.DefaultListSelectionModel
 * except that its for sets instead of lists. in other words, this class
 * makes up for swing's lack of a DefaultSetSelectionModel class.<br>
 *
 * editing methods all take a "byWhom" parameter taken to be the
 * object performing the editing. this value is used to catch
 * illegal states when more than one object attempts to edit the set
 * at the same time. the "byWhom" parameter is also the object
 * sent to any registered SelectionSetListeners as the "source"
 * parameter.<br>
 *
 * another purpose for having an editing mode is so that listener
 * notification can be delayed until all edits are completed.<br>
 *
 * all editing methods may be called atomically where "atomically"
 * means that the there is no current editor. in these cases
 * all listeners are notified immediately rather then only when 
 * endEditing() is called. this is mostly meant as a convienience
 * for cases when only a single editing operation is needed.<br>
 *
 * @author Melinda Green
 */
public class SelectionSet {
    private Set selectedObjects = new HashSet();
    private List selectionSetListeners = new ArrayList();
    private Object currentEditor = null;
    private Class contentType;

    /**
     * constructs a SelectionSet capable of holding objects
     * of the given type.
     */
    public SelectionSet(Class contentType) {
        this.contentType = contentType;
    }

    /**
     * no-arg constructor for convienience. the resulting
     * selection set will be capable of holding objects
     * of any type.
     */
    public SelectionSet() {
        this(Object.class);
    }

    //
    // Editing mode control methods
    //

    /**
     * reserves the selection set for editing only by the given object.
     * @param byWhom the editor-to-be
     * @param clearFirst an optional flag which when true will remove any
     * previous content.
     * @throws IllegalStateException if another object has reserved the
     * selection set.
     */
    public void beginEditing(Object byWhom, boolean clearFirst) throws IllegalStateException {
        if (currentEditor != null)
            throw new IllegalStateException("SelectionSet: already being edited");
        currentEditor = byWhom;    
        if(clearFirst)
            clear(byWhom);
    }

    /**
     * convenience method of the two-argument version which always
     * clears the selection set contents.
     */
    public void beginEditing(Object byWhom) throws IllegalStateException {
        beginEditing(byWhom, true);
    }

    /**
     * releases a previously reserved selection set.
     * @throws IllegalStateException the selection set 
     * is not already reserved by the given object.
     */
    public void endEditing(Object byWhom) throws IllegalStateException {
        if (currentEditor != byWhom)
            throw new IllegalStateException("SelectionSet: not owner");
        Object oldEditor = currentEditor;
        currentEditor = null;
        fireSelectionChanged(oldEditor); // notify all listeners
    } 

    //
    // Editing methods
    //

    public void addElement(Object element, Object byWhom) throws IllegalStateException, IllegalArgumentException {
        preEdit(byWhom);
        testType(element);
        selectedObjects.add(element);
        postEdit(byWhom);
    }
    
    public void clear(Object byWhom) throws IllegalStateException  {
        preEdit(byWhom);
        selectedObjects.clear();
        postEdit(byWhom);
    }

    public Object getCurrentEditor() {
        return currentEditor;
    }

    /**
     * convienience method sets all selected elements in one shot 
     * and notifies any selection listeners.<br>
     */
    public void setElements(Object newElements[], Object byWhom) throws IllegalStateException, IllegalArgumentException {
        preEdit(byWhom);
        selectedObjects.clear();
        for(int i=0; i<newElements.length; i++) {
            testType(newElements[i]);
            selectedObjects.add(newElements[i]);
        }
        postEdit(byWhom);
    }
    /**
     * convienience method sets the list to contain only the given element
     * and notifies listeners.<br>
     */
    public void setElements(Object element, Object byWhom) throws IllegalStateException {
        // any argument exceptions are generated by array version called below
        setElements(new Object[] { element }, byWhom);
    }

    public void removeElement(Object element, Object byWhom) throws IllegalStateException, IllegalArgumentException {
        preEdit(byWhom);
        testType(element);
        selectedObjects.remove(element);
        postEdit(byWhom);
    }

    /**
     * adds the given element if not already contained,
     * removes it if it is. possibly useful for Window's style 
     * <code><ctrl>+<left click></code> toggling.
     */
    public void toggle(Object element, Object byWhom) throws IllegalStateException, IllegalArgumentException {
        preEdit(byWhom);
        testType(element);
        if(selectedObjects.contains(element))
            selectedObjects.remove(element);
        else
            selectedObjects.add(element);
        postEdit(byWhom);
    }

    //
    // Atomic editing and type support.
    // callable by subclasses when adding new editing methods.
    //

    /**
     * called at the beginning of all editing methods capable of
     * being called atomically.
     */
    protected void preEdit(Object byWhom) {
        if (currentEditor != null && currentEditor != byWhom)
            throw new IllegalStateException("SelectionSet: not owner");
    }
    /**
     * called at the end of all editing methods capable of
     * being called atomically.
     */
    protected void postEdit(Object byWhom) {
        if(currentEditor == null)
            fireSelectionChanged(byWhom);
    }

    /**
     * @throws an IllegalArgumentException if the given object
     * is null or the class of the given object is not assignment 
     * compatable with the type of this selection set's content type.
     */
    protected void testType(Object obj) throws IllegalArgumentException {
        if(obj == null)
            throw new IllegalArgumentException("SelectionSet: attempt to add null element");
        if( ! contentType.isAssignableFrom(obj.getClass()))
            throw new IllegalArgumentException(
                "SelectionSet: " + obj.getClass().getName() + 
                " is not compatable with " + contentType.getName());
    }

    //
    // Selection query methods
    //

    public int getNumElements() {
        return selectedObjects.size();
    }
    
    public Object[] getElements() {
        return selectedObjects.toArray();
    }

    public boolean contains(Object obj) {
        return selectedObjects.contains(obj);
    }

    //
    // Selection Listener Methods
    //
    
    public void addSelectionSetListener(SelectionSetListener listener) {
        selectionSetListeners.add(listener);
    }
    public void removeSelectionSetListener(SelectionSetListener listener) {
        selectionSetListeners.remove(listener);
    }

    protected void fireSelectionChanged(Object byWhom) {
        for(Iterator it=selectionSetListeners.iterator(); it.hasNext(); )
            ((SelectionSetListener)it.next()).selectionSetChanged(this, byWhom);
    }
    
} // end class SelectionSet