package org.freehep.util.parameterdatabase;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;

import java.io.IOException;
import java.io.FileWriter;
import java.io.Writer;

import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.Set;
import java.util.TreeSet;
import java.util.HashSet;

import org.freehep.xml.util.XMLWriter;

public class ParameterDatabase {

    /**
     * The hashtable which contains all of the parameters. */
    protected Hashtable database;

    /**
     * This list contains all of the database listeners. */
    protected LinkedList databaseListeners = new LinkedList();

    /**
     * Constructor creates the hashtable to hold all of the
     * parameters.  The default size is 10000 entries. */
    public ParameterDatabase() {
        this(10000);
    }

    /**
     * Constructor creates the hashtable of the given initial size to hold all
     * of the parameters.  */
    public ParameterDatabase(int size) {
        database = new Hashtable(size);
    }

    /**
     * This method will determine if the given parameter is local.  That is,
     * that the parameter is defined on the first object returned from the
     * iterator. */
    public boolean isParameterLocal(String parameterName,
                                    Iterator iterator) {

        // Just check the first object to see if this is local.
        if (iterator.hasNext()) {

            // Create an appropriate ParameterKey.
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);

            // Extract the appropriate value.
            ParameterValue value = 
                (ParameterValue) database.get(key);

            return (value!=null);
        }

        // The default return is false.
        return false;
    }

    /**
     * Retrieve a parameter from the database.  The iterator must produce a
     * sequence of Objects which correspond to the specific tables in which to
     * try to find the parameter.  The iteration will stop when the parameter
     * is first found. */
    public Object getParameter(String parameterName,
                               Iterator iterator) {

        // Loop until we find the parameter or run out of tables to check.
        // This will return as soon as the parameter is found.
        while (iterator.hasNext()) {

            // Create an appropriate ParameterKey.
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);

            // Extract the appropriate value.
            ParameterValue value = 
                (ParameterValue) database.get(key);

            if (value!=null) return value.getValue();
        }

        // Return null because the given parameter wasn't found.
        return null;
    }

    /**
     * Set a parameter.  Use the iterator to traverse a series of tables in
     * which to find the given parameter.  Only the first parameter with this
     * name will be modified.  This method returns true if the parameter was
     * found and set; it returns false otherwise. */
    public boolean setParameter(String parameterName,
                                Object value,
                                Iterator iterator) {

        // Parameter name and value cannot be null.
        if (parameterName==null) throw new
            IllegalArgumentException("parameterName cannot be null.");
        if (value==null) throw new
            IllegalArgumentException("value cannot be null.");

        // Loop until we find the parameter or run out of tables to
        // check.  This will return as soon as the parameter is
        // found. 
        while (iterator.hasNext()) {

            // Create an appropriate ParameterKey.
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);

            // Extract the appropriate ParameterValue and set it.
            ParameterValue parameterValue = 
                (ParameterValue) database.get(key);
            if (parameterValue!=null) {
                parameterValue.setValue(value);

                firePropertyChange(parameterName, parameterValue);

                return true;
            }
        }

        // Indicate that the parameter was not found.
        return false;
    }

    /**
     * Return a boolean indicating whether this parameter is defined using the
     * given iterator to iterate through a set of objects. */
    public boolean isParameterDefined(String parameterName,
                                      Iterator iterator) {

        // Parameter name cannot be null.
        if (parameterName==null) throw new
            IllegalArgumentException("parameterName cannot be null.");

        // Loop until we find the parameter or run out of tables to
        // check.  This will return as soon as the parameter is
        // found. 
        while (iterator.hasNext()) {
            Object object = iterator.next();
            if (isParameterDefined(parameterName, object)) return true;
        }

        // Indicate that the parameter was not found.
        return false;
    }

    /**
     * Return a boolean indicating whether this parameter is defined for the
     * given object. */
    public boolean isParameterDefined(String parameterName,
                                      Object object) {

        // Parameter name and object cannot be null.
        if (parameterName==null) throw new
            IllegalArgumentException("parameterName cannot be null.");
        if (object==null) throw new
            IllegalArgumentException("object cannot be null.");

        // Create an appropriate ParameterKey.
        ParameterKey key = 
            ParameterKey.createParameterKey(object,parameterName);

        // Extract the appropriate ParameterValue and set it.
        ParameterValue parameterValue = 
            (ParameterValue) database.get(key);
        return (parameterValue!=null);
    }

    /**
     * Add a parameter to the database.  This will only add the parameter to
     * the first object returned by the given iterator.  After the parameter
     * is added, the listener will be added to the value.  This same listener
     * will be removed from the next visible parameter of the same name.  If
     * the parameter already exists, the listener is added but the value is
     * not modified. */
    public boolean addParameter(String parameterName,
                                Object value,
                                Iterator iterator,
                                PropertyChangeListener listener) {

        return addParameter(parameterName, value, iterator, listener, false);
    }

    /**
     * Add a parameter to the database.  This will only add the parameter to
     * the first object returned by the given iterator.  After the parameter
     * is added, the listener will be added to the value.  This same listener
     * will be removed from the next visible parameter of the same name.  If
     * the parameter already exists, the listener is added but the value is
     * not modified unless overwrite is true. */
    public boolean addParameter(String parameterName,
                                Object value,
                                Iterator iterator,
                                PropertyChangeListener listener,
                                boolean overwrite) {

        // Parameter name and value cannot be null.
        if (parameterName==null) throw new
            IllegalArgumentException("parameterName cannot be null.");
        if (value==null) throw new
            IllegalArgumentException("value cannot be null.");

        // We must process the first object specially.  Here we add or
        // modify the given parameter.
        if (iterator.hasNext()) {
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);

            // Create a new entry if it doesn't already exist.
            ParameterValue parameterValue = (ParameterValue) database.get(key);
            if (parameterValue!=null) {

                // Add the listener.
                if (listener!=null) 
                    parameterValue.addPropertyChangeListener(listener);

                // Only overwrite the existing value if this has been
                // requested.  
                if (overwrite) {
                    parameterValue.setValue(value);

                    firePropertyChange(parameterName, parameterValue);
                }
            } else {

                parameterValue = 
                    ParameterValue.createParameterValue(value);

                if (listener!=null) 
                    parameterValue.addPropertyChangeListener(listener);

                database.put(key,parameterValue);
            }

        } else {
            return false;
        }

        // Loop over the rest of the objects and remove this listener from the
        // first entry found.
        while (iterator.hasNext()) {

            // Create an appropriate ParameterKey.
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);

            // Extract the appropriate ParameterValue and set it.
            ParameterValue parameterValue = 
                (ParameterValue) database.get(key);
            if (parameterValue!=null) {
                parameterValue.removePropertyChangeListener(listener);
                return true;
            }
        }

        // Return indicating that the parameter has been added.
        return true;
    }

    /**
     * Remove a parameter from the database.  This will only remove the
     * parameter from the first object returned by the given iterator.  After
     * the parameter is removed, the listeners from this entry will be added
     * to the next visible value. */
    public boolean removeParameter(String parameterName,
                                   Iterator iterator) {

        // Parameter name and value cannot be null.
        if (parameterName==null) throw new
            IllegalArgumentException("parameterName cannot be null.");

        // We must process the first object specially.  We only
        // actually remove the parameter associated with the first
        // object. 
        List movedListeners = null;
        if (iterator.hasNext()) {
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);

            // Get the associated value.
            ParameterValue parameterValue = 
                (ParameterValue) database.remove(key);

            if (parameterValue!=null) {
                movedListeners = 
                    parameterValue.getPropertyChangeListeners();
            } else {
                return false;
            }

        } else {
            return false;
        }

        // Loop over the rest of the objects and see if we find another
        // version of this parameter.  If so, add the listeners to this
        // version of the parameter.
        if (movedListeners!=null && !movedListeners.isEmpty()) {
            while (iterator.hasNext()) {

                // Create an appropriate ParameterKey.
                Object object = iterator.next();
                ParameterKey key = 
                    ParameterKey.createParameterKey(object,parameterName);
                
                // Extract the appropriate ParameterValue and set it.
                ParameterValue parameterValue = 
                    (ParameterValue) database.get(key);
                if (parameterValue!=null) {

                    Iterator i = movedListeners.iterator();
                    while (i.hasNext()) {
                        PropertyChangeListener listener = 
                            (PropertyChangeListener) i.next();
                        parameterValue.addPropertyChangeListener(listener);
                    }

                    firePropertyChange(parameterName,parameterValue);

                    return true;
                }
            }
        }

        // Return indicating that the parameter has been added.
        return true;
    }

    /**
     * Purge a parameter from the list of objects defined by the iterator. */
    public void purgeParameter(String parameterName, 
                               Iterator iterator) {

        while (iterator.hasNext()) {
            Object object = iterator.next();
            ParameterKey key = 
                ParameterKey.createParameterKey(object,parameterName);
            database.remove(key);
        }
    }

    /**
     * Purge all entries based on a particular object. */
    public void purgeEntries(Object object) {

        Set keys = database.keySet();
        Iterator i = keys.iterator();
        while (i.hasNext()) {
            ParameterKey key = (ParameterKey) i.next();
            database.remove(key);
        }
    }

    /**
     * Purge all references to a particular listener. */
    public void purgePropertyChangeListener(PropertyChangeListener listener) {

        Set keys = database.keySet();
        Iterator i = keys.iterator();
        while (i.hasNext()) {
            ParameterKey key = (ParameterKey) i.next();
            ParameterValue value = 
                (ParameterValue) database.get(key);
            if (value!=null) {
                value.removePropertyChangeListener(listener);
            }
        }
    }

    /**
     * Purge all references to a particular listener. */
    public void purgePropertyChangeListener(Iterator iterator,
                                            PropertyChangeListener listener) {

        while (iterator.hasNext()) {
            ParameterKey key = (ParameterKey) iterator.next();
            ParameterValue value = 
                (ParameterValue) database.get(key);
            if (value!=null) {
                value.removePropertyChangeListener(listener);
            }
        }
    }

    /**
     * Completely clear this parameter database. */
    public void clear() {
        database.clear();
    }

    /**
     * Write out the entire database as an XML file.  Returns true if there
     * were no errors generated; returns false otherwise.  Note that this
     * simply appends a parameters element to the given XMLWriter.  The
     * instanceMap must map instances of non-Class objects to a unique
     * Integer.  The integer value is used to tag "local" copies of
     * parameters.  If null is passed in as the instanceMap none of the local
     * copies of parameters will be saved. */
    public boolean writeAsXML(XMLWriter xmlWriter, Hashtable instanceMap) {

        // Begin the list of parameters.
        xmlWriter.openTag("Parameters");
        
        // Get all of the keys in the database and iterate over them.
        Set keys = database.keySet();
        Iterator i = keys.iterator();
        while (i.hasNext()) {
            
            ParameterKey key = (ParameterKey) i.next();
            ParameterValue value = (ParameterValue) database.get(key);
            
            if (value!=null) {
                
                String type = "";
                int id = 0;
                
                boolean classParameter;
                boolean valid;
                
                Object obj = key.getObject();
                if (obj instanceof Class) {
                    classParameter = true;
                    valid = true;
                    type = ((Class) obj).getName();
                    
                } else {
                    classParameter = false;
                    
                    if (instanceMap!=null) {
                        Integer instanceId = 
                            (Integer) instanceMap.get(obj);
                        if (instanceId!=null) {
                            valid = true;
                            id = instanceId.intValue();
                        } else {
                            valid = false;
                            id = 0;
                        }
                    } else {
                        valid = false;
                    }
                }
                
                String val = value.getValue().toString();
                String valType = value.getValue().getClass().getName();
                
                // Add the parameter itself.
                if (valid) {
                    if (classParameter) {
                        xmlWriter.setAttribute("class",type);
                        xmlWriter.setAttribute("name",key.getName());
                        xmlWriter.setAttribute("value",val);
                        xmlWriter.setAttribute("type",valType);
                        xmlWriter.printTag("ClassParameter");
                    } else {
                        xmlWriter.setAttribute("id",id);
                        xmlWriter.setAttribute("name",key.getName());
                        xmlWriter.setAttribute("value",val);
                        xmlWriter.setAttribute("type",valType);
                        xmlWriter.printTag("InstanceParameter");
                    }
                }
            }
        }

        // Write out the trailing information.
        xmlWriter.closeTag();

        return true;
    }

    /**
     * Send off the property change event. */
    public void firePropertyChange(String parameterName, 
                                   ParameterValue parameterValue) {

        // Now create the property change event.
        PropertyChangeEvent pcEvent = 
            new PropertyChangeEvent(this,parameterName,null,
                                    parameterValue.getValue());
        
        // Send off the notification of the new value.
        Iterator i = 
            parameterValue.getPropertyChangeListeners().iterator();
        while (i.hasNext()) {
            PropertyChangeListener listener = 
                (PropertyChangeListener) i.next();
            listener.propertyChange(pcEvent);
        }

        // Send off notification to the database listeners. 
        i = databaseListeners.iterator();
        while (i.hasNext()) {
            DatabaseListener listener =  (DatabaseListener) i.next();
            listener.databaseUpdated();
        }
    }

    /**
     * Add a database listener. */
    public void addDatabaseListener(DatabaseListener listener) {
        if (listener!=null) databaseListeners.add(listener);
    }

    /**
     * Remove a database listener */
    public void removeDatabaseListener(DatabaseListener listener) {
        if (listener!=null) databaseListeners.remove(listener);
    }

    /**
     * Get a string array which contains the names of all parameters defined
     * by this iterator.  Note that all parameters in the database are checked
     * to see if they match the given objects. */
    public String[] getCurrentParameterSet(Iterator iterator) {

        TreeSet parameterNames = new TreeSet();

        // Make a set of all of the objects in the iterator, so that the
        // database itself only needs to be searched once.
        HashSet objects = new HashSet();
        while (iterator.hasNext()) {
            objects.add(iterator.next());
        }

        // Now loop over all of the keys in the database.
        Set keys = database.keySet();
        Iterator i = keys.iterator();
        while (i.hasNext()) {
            ParameterKey key = (ParameterKey) i.next();
            if (objects.contains(key.getObject())) {
                parameterNames.add(key.getName());
            }
        }

        // Make an array out of the tree set.
        String[] names = new String[parameterNames.size()];
        names = (String[]) parameterNames.toArray(names);

        return names;
    }

    /**
     * Get a string array which gives all of the parameters which are defined
     * locally on the given object. */
    public String[] getLocalParameterSet(Object object) {

        TreeSet parameterNames = new TreeSet();

        // Iterate over all of the keys in the database.
        Set keys = database.keySet();
        Iterator i = keys.iterator();
        while (i.hasNext()) {
            ParameterKey key = (ParameterKey) i.next();
            if (object.equals(key.getObject())) {
                parameterNames.add(key.getName());
            }
        }

        // Make an array out of the tree set.
        String[] names = new String[parameterNames.size()];
        names = (String[]) parameterNames.toArray(names);

        return names;
    }

    /**
     * Return a hash table in which the keys are the parameter names and the
     * values are clones of those values in the database. */
    public Hashtable cloneLocalParameters(Object object) {

        Hashtable clonedParameters = new Hashtable();

        // Iterate over all of the keys in the database.
        Set keys = database.keySet();
        Iterator i = keys.iterator();
        while (i.hasNext()) {
            ParameterKey key = (ParameterKey) i.next();
            if (object.equals(key.getObject())) {
                String parameterName = key.getName();
                ParameterValue value = (ParameterValue) database.get(key);
                Object parameterValue = value.getValue();

                try {
                    Object clonedValue = cloneObject(parameterValue);
                    clonedParameters.put(parameterName,clonedValue);
                } catch (Exception e) {
                }
            }
        }

        return clonedParameters;
    }

    /**
     * This will "clone" the given object.  This method makes the assumption
     * that the objects stored in the database produce a result from the
     * toString() method which will then produce an identical object when the
     * single-string constructor is called.  (Note: this is a general
     * assumption of the database as a whole.) */
    protected Object cloneObject(Object originalObject) 
        throws NoSuchMethodException,
               InstantiationException,
               IllegalAccessException,
               InvocationTargetException {

        // Setup the call parameters for a constructor which takes a String as
        // the only argument.
        Object[] parameters = new Object[1];
        parameters[0] = originalObject.toString();
        Class[] parameterTypes = new Class[1];
        parameterTypes[0] = String.class;
                
        // Construct the new value Object based on the given String value.
        Class valueClass = originalObject.getClass();
        Constructor constructor = 
            valueClass.getConstructor(parameterTypes);
        Object newValue = constructor.newInstance(parameters);

        return newValue;
    }

}
