package gizmoball.property;

import java.lang.reflect.*;
import java.util.*;

/**
 * An abstract base class for objects that allow property access.
 * This allows subclasses to have regular get and set methods that
 * are then presented through this generalized property interface.
 *
 * <p>For example, to add a Foo property to a Propertyified, a
 * subclass should have an <code>int getFoo()</code> method and a
 * <code>void setFoo(int)</code> method and override {@link
 * #setUpProperties()} to call {@link #addProperty(String)
 * addProperty("Foo")}.
 *
 * <p>This provides a mechanism for registering property change
 * observers that will be fired in response to a set method being
 * called.  However, this requires a bit of help from the extending
 * class.  It <i>must</i> call {@link #firePropertyObservers(String,
 * Object)} at the end of each set method that is reflected by a
 * property.
 *
 * <p>Note that the properties system is not guaranteed to be even
 * remotely fast, so it should not be relied upon for time-critical
 * operations.  However, it is guaranteed that it will have a minimal
 * effect on runtime <i>if there are no observers</i> of the object
 * whose set methods are being called.
 *
 * <p>There are certain restrictions on the get and set methods that
 * are reflected as properties.  The only get method that will be
 * honored for a given property must take zero arguments, and the
 * corresponding set method must return void and take a single
 * argument that is of the same type as the return type of the get
 * method.
 *
 * @see java.beans
 * @author Austin Clements
 * @version 1.0
 *
 * @specfield propertyObservers : List<PropertyObserver> // Observers
 * @specfield propertyNames : List<String> // Names of the properties
 *                                            of this class
 */
public abstract class Propertyified {
    private Set/*<PropertyObserver>*/ observers = null;
    private Map/*<String, Method>*/ propertyGetters;
    private Map/*<String, Method>*/ propertySetters;
    private Map/*<String, Class>*/ propertyTypes;

    private static Map/*<Class, List>*/ cache = new HashMap();

    /**
     * Creates an instance of a Propertyified object.
     *
     * @modifies this
     * @effects constructs a Propertyified object
     */
    public Propertyified() {
        // Prime the property names cache
        guaranteeCache();
    }

    /**
     * Adds a property observer.  This observer will be notified
     * whenever a set method is called on the Propertyified object.
     * Note that the value of the property may not have changed.
     *
     * @param o the observer to call when a property is set
     * @modifies observers list
     * @effects adds o to the list of property observers
     */
    public void addPropertyObserver(PropertyObserver o) {
        if (observers == null) {
            observers = new HashSet();
        }

        observers.add(o);
    }

    /**
     * Removes a property observer.  This observer will no longer be
     * notified of property updates.
     *
     * @param o the observer to remove
     * @modifies observers list
     * @effects removes o from the list of property observers
     */
    public void removePropertyObserver(PropertyObserver o) {
        if (observers != null) {
            observers.remove(o);
            if (observers.size() == 0) {
                observers = null;
            }
        }
    }

    /**
     * Fire all property observers.  This <i>must</i> be called at the
     * end of every set method that is reflected by a property of the
     * subclass.  This method is guaranteed to return quickly if there
     * are no observers.
     *
     * @param name the name of the property that was set
     * @param newValue the new value of the property (boxed if
     * necessary)
     * @throws IllegalArgumentException if the specified property
     * does not exist (note that the guarantee of returning quickly is
     * stronger than the guarantee of throwing this exception)
     */
    protected void firePropertyObservers(String name, Object newValue)
        throws IllegalArgumentException {
        if (observers == null) {
            return;
        }

        Class propType = (Class)propertyTypes.get(name);

        if (propType == null) {
            throw new IllegalArgumentException("Property does not exist");
        }

        Property prop = new Property(this, name, newValue, propType);

        for (Iterator it = observers.iterator(); it.hasNext(); ) {
            PropertyObserver o = (PropertyObserver)it.next();

            o.propertyUpdate(prop);
        }
    }

    /**
     * Get the property associated with the specified name.  This
     * captures the property's <i>current</i> value (using the
     * corresponding get method in the subclass) in the returned
     * property object.  If the type of the property is actually a
     * primitive type, it will automatically be boxed.
     *
     * @param name the name of the property to get
     * @return the requested property
     * @throws IllegalArgumentException if the specified property
     * does not exist
     * @throws GetSetMethodException if the get method throws an
     * exception.  This exception will be chained with the actual
     * exception that was thrown.
     */
    public Property getProperty(String name)
        throws IllegalArgumentException, GetSetMethodException {
        Method getter = (Method)propertyGetters.get(name);

        if (getter == null) {
            throw new IllegalArgumentException("Property does not exist");
        }

        Object result;
        try {
            result = getter.invoke(this, null);
        } catch (InvocationTargetException e) {
            throw new GetSetMethodException("Get method threw an exception",
                                            e.getCause());
        } catch (IllegalAccessException e) {
            throw new GetSetMethodException("Unable to access get method");
        }

        Class propType = (Class)propertyTypes.get(name);
        Property prop = new Property(this, name, result, propType);

        return prop;
    }

    /**
     * Set the value of the specified property.  This will call the
     * necessary set method in the object.  If the expected value is
     * actually a primitive type, value will automatically be
     * unboxed.
     *
     * <p>Warning: Because this uses the property's set method, which
     * in turn is required to call {@link
     * #firePropertyObservers(String, Object)}, care must be taken to
     * avoid infinite loops when using this method from a property
     * observer.
     *
     * @param name the name of the property to set
     * @param value the value to set the property to (unboxed if
     * necessary)
     * @throws IllegalArgumentException if the specified property
     * does not exist
     * @throws GetSetMethodException if the get method throws an
     * exception.  This exception will be chained with the actual
     * exception that was thrown.
     * @modifies the value of property name
     * @effects calls the set method for the specified property,
     * setting it to value
     */
    public void setProperty(String name, Object value)
        throws IllegalArgumentException, GetSetMethodException {
        Method setter = (Method)propertySetters.get(name);

        if (setter == null) {
            throw new IllegalArgumentException("Property does not exist");
        }

        try {
            setter.invoke(this, new Object[] {value});
        } catch (InvocationTargetException e) {
            throw new GetSetMethodException("Set method threw an exception",
                                            e.getCause());
        } catch (IllegalAccessException e) {
            throw new GetSetMethodException("Unable to access set method");
        }
    }

    /**
     * Adds the properties of this object.  This should be overridden
     * by extending classes to add the properties of this object via
     * the {@link #addProperty} method.
     *
     * <p>Note that the return value from this is cached on a per-type
     * basis, so it should function the same regardless of instance.
     *
     * <p>Also, this should always call super.setUpProperties() in
     * order to inherit the superclasses properties.
     *
     * <p><b>Do not</b> call this method directly.  It will be called
     * when needed by the property system.
     */
    protected void setUpProperties() {
        // Do nothing
    }

    /**
     * Adds a property to this object by name.  This assumes that the
     * name of the setter method will be set<name> and the name of the
     * getter method will be get<name>.  This operates on a per-class
     * basis, not a per-instance basis, because it is assumed that all
     * instances of a particular class will share the same properties.
     *
     * <p>This should only be called from {@link #setUpProperties()}.
     *
     * @param name the name of the property to add
     * @throws IllegalArgumentException if the getter and setter
     * methods do not exist
     * @modifies list of properties
     * @effects adds this property to the list of properties
     */
    protected void addProperty(String name)
        throws IllegalArgumentException {
        Class myClass = this.getClass();
        Method[] methods = myClass.getMethods();
        String getterName = "get" + name;
        String setterName = "set" + name;

        for (int i = 0; i < methods.length; ++i) {
            if (methods[i].getName().equals(getterName)) {
                Method getMethod = methods[i];
                Class propType = getMethod.getReturnType();
                Method setMethod;
                try {
                    setMethod =
                        myClass.getMethod(setterName, new Class[] {propType});
                } catch (NoSuchMethodException e) {
                    throw new IllegalArgumentException
                        ("Could not find setter");
                }

                // Box type
                if (propType.isPrimitive()) {
                    if (propType == Boolean.TYPE) {
                        propType = Boolean.class;
                    } else if (propType == Character.TYPE) {
                        propType = Character.class;
                    } else if (propType == Byte.TYPE) {
                        propType = Byte.class;
                    } else if (propType == Short.TYPE) {
                        propType = Short.class;
                    } else if (propType == Integer.TYPE) {
                        propType = Integer.class;
                    } else if (propType == Long.TYPE) {
                        propType = Long.class;
                    } else if (propType == Float.TYPE) {
                        propType = Float.class;
                    } else if (propType == Double.TYPE) {
                        propType = Double.class;
                    } else if (propType == Void.TYPE) {
                        propType = Void.class;
                    }
                }

                propertyGetters.put(name, getMethod);
                propertySetters.put(name, setMethod);
                propertyTypes.put(name, propType);
                return;
            }
        }

        throw new IllegalArgumentException("Could not find getter");
    }

    /**
     * Gets an immutable collection of the names of the properties of
     * this object.  The value of this list is cached on a per-class
     * basis.
     *
     * @return a collection of this object's property names
     */
    public Collection/*<String>*/ getPropertyNames() {
        return Collections.unmodifiableCollection(propertyTypes.keySet());
    }

    /**
     * Guarantee that the getter, setter, and type maps are filled.
     * This is pulled from the cache if available, or generated if
     * not.
     */
    private void guaranteeCache() {
        // Do I already have it?
        if (propertyGetters != null) {
            return;
        }

        // Does the cache have it?
        List propertyStuff = (List)cache.get(this.getClass());
        if (propertyStuff != null) {
            propertyGetters = (Map)propertyStuff.get(0);
            propertySetters = (Map)propertyStuff.get(1);
            propertyTypes = (Map)propertyStuff.get(2);
            return;
        }

        // Have to generate it
        propertyGetters = new HashMap();
        propertySetters = new HashMap();
        propertyTypes = new HashMap();

        setUpProperties();

        propertyStuff = new ArrayList();
        propertyStuff.add(propertyGetters);
        propertyStuff.add(propertySetters);
        propertyStuff.add(propertyTypes);

        cache.put(this.getClass(), propertyStuff);
    }
}
