package org.apache.commons.events.observable ; 

import java.beans.PropertyChangeSupport ; 
import java.beans.PropertyChangeListener ; 
import java.beans.PropertyChangeEvent ; 
import java.util.Map ; 
import java.util.Set ;
import java.util.Collection ;

import org.apache.commons.collections.map.AbstractMapDecorator ; 

/**
 * <p>
 * Decorates a <code>Map</code> implementation with a <b>bound 
 * property</b> named &quot;collection&quot;.
 * </p>
 * <p>
 * Each modifying method call made on this <code>Map</code> is 
 * handled as a change to the &quot;collection&quot; property.  This 
 * facility serves to notify subscribers of a change to the collection
 * but does not allow users the option of vetoing the change.  To 
 * gain the ability to veto the change, use a {@link ConstrainedMap}
 * decorater.
 * </p>
 * <p>
 * Due to the fact that a Map offers several &quot;views&quot; of the 
 * same data, some confusion may arise as to what action caused a 
 * particular event.  For instance, entries in the map may be 
 * deleted by <code>Map.values().remove(object)</code>.  There is no
 * known method, short of digging into the implementation of a Map,
 * to determine <i>which</i> key-value pair was deleted by this operation,
 * particularly if there is more than one occurence of a specific 
 * value.  (Conversely, there is also no means of controlling which 
 * key-value pair is deleted by this method; therefore it's not a 
 * terribly smart thing to do.) 
 * </p>
 * <p> 
 * This implementation of a bound map makes no attempt to interpret 
 * events generated by these alternate views of the data.  It merely
 * ensures that registered listeners receive <i>all</i> events generated 
 * by <i>any</i> view of the Map's data.  It is left to the client
 * to interpret the events generated by their map's usage.
 * </p>
 * 
 * @since Commons Events 1.0
 * @author Bryce Nordgren / USDA Forest Service
 */

public class BoundMap extends AbstractMapDecorator {

    // Fields
    //-----------------------------------------------------------------------
    /** 
     * Child-accessible factory used to construct 
     * {@link CollectionChangeEvent}s. 
     * This field is <code>final</code> and is set by the constructor, so 
     * while the children may <i>use</i> it to instantiate events, they 
     * may not <i>change</i> it.  
     */
    protected final CollectionChangeEventFactory eventFactory ; 
    /** Utility to support listener registry and event dispatch. */
    private final PropertyChangeSupport changeSupport ; 


    // Constructors
    //-----------------------------------------------------------------------
    /**
     * Constructor that wraps (not copies) and takes a
     * {@link CollectionChangeEventFactory}.
     * <p>
     * This should be used if the client wants to provide a user-specific
     * CollectionChangeEventFactory implementation.  Note that the 
     * same instance of the factory may not be used with multiple 
     * collection decorators.
     * 
     * @param map  the map to decorate, must not be null
     * @param eventFactory the factory which instantiates 
     *    {@link CollectionChangeEvent}s.  
     * @throws IllegalArgumentException if the map or event factory
     *     is null.
     * @throws UnsupportedOperationException if the <code>eventFactory</code>
     *     has already been used with another decorator.
     */
    protected BoundMap(
            final Map map, 
            CollectionChangeEventFactory eventFactory) { 

        // initialize parent
        super(map) ; 

        // Make a default event factory if necessary
        if (eventFactory == null) { 
            eventFactory = new DefaultCollectionChangeEventFactory() ; 
        }
            
        // install the event factory
        eventFactory.setCollection(this) ; 
        this.eventFactory = eventFactory ; 

        // initialize property change support.
        changeSupport = new PropertyChangeSupport(this) ; 
    }

    /**
     * Constructor that wraps (not copies) and uses the 
     * {@link DefaultCollectionChangeEventFactory}.
     * <p>
     * This should be used if the default change events are considered 
     * adequate to the task of monitoring changes to the collection.
     * 
     * @param map  the map to decorate, must not be null
     * @throws IllegalArgumentException if the map is null
     */
    protected BoundMap(final Map map) { 
        this(map, null) ; 
    }


    // Factory Methods
    //-----------------------------------------------------------------------
    /**
     * Factory method to decorate an existing Map using a user-defined
     * {@link CollectionChangeEventFactory}.
     * <p>
     * This should be used if the client wants to provide a user-specific
     * CollectionChangeEventFactory implementation.  Note that the 
     * same instance of the factory may not be used with multiple 
     * collection decorators.
     * 
     * @param map  the map to decorate, must not be null
     * @param eventFactory the factory which instantiates 
     *    {@link CollectionChangeEvent}s.  
     * @throws IllegalArgumentException if the map or event factory
     *     is null.
     * @throws UnsupportedOperationException if the <code>eventFactory</code>
     *     has already been used with another decorator.
     */
    public static BoundMap decorate(
            final Map map, 
            final CollectionChangeEventFactory eventFactory)  {
        return new BoundMap(map, eventFactory) ; 
    }

    /**
     * Factory method to decorate an existing Map using the provided
     * {@link DefaultCollectionChangeEventFactory}.
     * <p>
     * This should be used if the default change events are considered 
     * adequate to the task of monitoring changes to the collection.
     * 
     * @param map  the map to decorate, must not be null
     * @throws IllegalArgumentException if the map is null
     */
    public static BoundMap decorate( final Map map )  { 
        return new BoundMap(map) ; 
    }


    // Listener Management
    //-----------------------------------------------------------------------
    /**
     * Registers a listener with this decorator.  The Listener must 
     * implement the <code>PropertyChangeListener</code> interface.
     * Adding a listener more than once will result in more than 
     * one notification for each change event.
     *
     * @param l The listener to register with this decorator.
     */
    public void addPropertyChangeListener(PropertyChangeListener l) { 
        changeSupport.addPropertyChangeListener(l) ; 
    }

    /**
     * Unregisters a listener from this decorator.  The Listener must 
     * implement the <code>PropertyChangeListener</code> interface.
     * If the listener was registered more than once, calling this method 
     * cancels out a single registration.  If the listener is not 
     * registered with this object, no action is taken.
     *
     * @param l The listener to unregister with this decorator.
     */
    public void removePropertyChangeListener(PropertyChangeListener l) { 
        changeSupport.removePropertyChangeListener(l) ; 
    } 

    /**
     * This is a utility method to allow subclasses to fire property change 
     * events.  
     * @param evt The pre-constructed event.
     */
    protected void firePropertyChange(PropertyChangeEvent evt) { 
        changeSupport.firePropertyChange(evt) ; 
    }

    /**
     * Package private method to create an EventRepeater from within the 
     * context of a particular BoundCollection object.  This event repeater 
     * will relay events to all property change listeners subscribed to this
     * bound collection.
     * @return the event repeater object.
     */
    EventRepeater createEventRepeater() { 
        return new EventRepeater(this) ; 
    }
 


    // Decoration of Map Methods
    //-----------------------------------------------------------------------
    public Object put(Object key, Object value) { 
        Object formerValue = map.put(key, value) ; 
        CollectionChangeEvent evt = eventFactory.createPut(
            key, value, formerValue, true) ; 
        firePropertyChange(evt) ; 

        return formerValue ; 
    }

    public void putAll(Map toPut) {
        map.putAll(toPut) ; 
        CollectionChangeEvent evt = eventFactory.createPutAll(toPut, true) ; 
        firePropertyChange(evt) ; 
    }

    public void clear() { 
        map.clear() ; 
        CollectionChangeEvent evt = eventFactory.createClear(true) ; 
        firePropertyChange(evt) ; 
    }

    public Object remove(Object key) { 
        Object formerValue = map.remove(key) ; 
        CollectionChangeEvent evt = eventFactory.createRemove(
            formerValue, true) ; 
        firePropertyChange(evt) ; 

        return formerValue ; 
    }

    /**
     * <p>
     * Returns a Set view of the Map entries.  This set is backed by the 
     * map, so changes to either view is reflected in the other.  
     * Accordingly, our CollectionChangeEvents are sent to the 
     * Set's subscribers, and the Set's CollectionChangeEvents are sent 
     * to our subscribers.
     * </p>
     * <p>
     * The interface contract for this method specifies that most 
     * methods of removal are supported, but no methods of addition.
     * </p>
     * @return An entry set view decorated with a BoundSet.
     */
    public Set entrySet() { 
        Set entries = map.entrySet() ; 

        // make a copy of the event factory
        CollectionChangeEventFactory factoryCopy = 
            (CollectionChangeEventFactory)(eventFactory.clone()) ;

        // bind the entry set.
        BoundSet boundEntries = BoundSet.decorate(entries, factoryCopy) ; 

        // relay "boundEntries" changes to our listeners
        boundEntries.addPropertyChangeListener(createEventRepeater()) ; 

        // relay our changes to "boundEntries" listeners
        addPropertyChangeListener(boundEntries.createEventRepeater()) ; 

        return boundEntries ; 
    }

    /**
     * <p>
     * Returns a Set view of the Map's keys.  This set is backed by the 
     * map, so changes to either view is reflected in the other.  
     * Accordingly, our CollectionChangeEvents are sent to the 
     * Set's subscribers, and the Set's CollectionChangeEvents are sent 
     * to our subscribers.
     * </p>
     * <p>
     * The interface contract for this method specifies that most 
     * methods of removal are supported, but no methods of addition.
     * </p>
     * @return A set view of the keys decorated with a BoundSet.
     */
    public Set keySet() { 
        Set keys = map.keySet() ; 
        
        // make a copy of the event factory
        CollectionChangeEventFactory factoryCopy = 
            (CollectionChangeEventFactory)(eventFactory.clone()) ;

        // bind the set of keys.
        BoundSet boundKeys = BoundSet.decorate(keys, factoryCopy) ; 

        // relay "boundKeys" changes to our listeners
        boundKeys.addPropertyChangeListener(createEventRepeater()) ; 

        // relay our changes to "boundKeys" listeners
        addPropertyChangeListener(boundKeys.createEventRepeater()) ; 

        return boundKeys ; 
        
    }

    /**
     * <p>
     * Returns a Collection view of the Map's values.  This collection
     * is backed by the 
     * map, so changes to either view is reflected in the other.  
     * Accordingly, our CollectionChangeEvents are sent to the 
     * Collection's subscribers, and the Collection's CollectionChangeEvents
     * are sent to our subscribers.
     * </p>
     * <p>
     * The interface contract for this method specifies that most 
     * methods of removal are supported, but no methods of addition.
     * </p>
     * @return A collection view of the values decorated with a BoundCollection.
     */
    public Collection values() { 
        Collection values = map.values() ; 

        // make a copy of the event factory
        CollectionChangeEventFactory factoryCopy = 
            (CollectionChangeEventFactory)(eventFactory.clone()) ;

        // bind the collection of values
        BoundCollection boundValues = BoundCollection.decorate(
            values, factoryCopy) ; 

        // relay "boundValues" changes to our listeners
        boundValues.addPropertyChangeListener(createEventRepeater()) ; 

        // relay our changes to "boundValues" listeners
        addPropertyChangeListener(boundValues.createEventRepeater()) ; 

        return boundValues ; 
    }
        

    // Utility classes
    //-----------------------------------------------------------------------
    /**
     * This class subscribes to events from another collection and fires them 
     * to all the subscribers of this collection. 
     */
    private class EventRepeater implements PropertyChangeListener { 
        private Object myself ; 
        public EventRepeater(Object me) { myself = me ; }
        /** 
         * Relay events which did not originate with my list.  This 
         * prevents an infinite loop.
         */
        public void propertyChange(PropertyChangeEvent evt) { 
            if ((evt != null) && (evt.getSource() != myself)) { 
                firePropertyChange(evt) ; 
            }
        }
    }
}
