/***********************************************************************

GECCO, a platform for command and control gaming

Copyright (C) 2001 Henrik Bäärnhielm, Andreas Enblom, Niklas
Hallenfur, Karl Hasselström, Henrik Hägerström, Oskar Linde, Jing Fu
Zi and Jon Åslund

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.

This program 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
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
USA.

***********************************************************************/

package gecco.server.core;

import java.util.*;

/**
 *  Maintains a mapping between names and values. The names are always Strings,
 *  but the values can be any kind of object. This class keeps track of the type
 *  of the object, and provides convenient get/set methods for certain popular
 *  types, avoiding the inconvenience and hazard of explicit type casting.
 *
 * @author     Karl Hasselström
 * @created    May 2, 2001
 */
public class TypedProperties implements Map {

    /**
     *  The <code>Map</code> used to store the properties
     */
    private Map properties;
    /**
     *  The <code>Map</code> used to store the property types
     */
    private Map propertyTypes;
    /**
     *  Predefined property type corresponding to the primitive data type <code>
     *  double</code>
     */
    public final static String TYPE_DOUBLE = "DOUBLE";
    /**
     *  Predefined property type corresponding to the primitive data type <code>
     *  int</code>
     */
    public final static String TYPE_INT = "INT";
    /**
     *  Predefined property type corresponding to the class <code>String</code>
     */
    public final static String TYPE_STRING = "STRING";
    /**
     *  Property type assigned when none was specified
     */
    public final static String TYPE_UNDEFINED = "UNDEFINED";


    /**
     *  Constructor for the <code>TypedProperties</code> object. Creates an
     *  empty mapping.
     */
    public TypedProperties() {
        properties = new HashMap();
        propertyTypes = new HashMap();
    }


    /**
     *  Constructor for the <code>TypedProperties</code> object. Creates an
     *  exact duplicate of the given <code>TypedProperties</code> .
     *
     * @param  other  The <code>TypedProperties</code> object to be duplicated
     */
    public TypedProperties(TypedProperties other) {
        this();
        loadProperties(other);
    }


    /**
     *  Creates or replaces a property. The value is a <code>double</code> .
     *
     * @param  name   The name of the property
     * @param  value  The value
     */
    public void setProperty(String name, double value) {
        setProperty(name, new Double(value), TYPE_DOUBLE);
    }


    /**
     *  Creates or replaces a property. The value is an <code>int</code> .
     *
     * @param  name   The name of the property
     * @param  value  The value
     */
    public void setProperty(String name, int value) {
        setProperty(name, new Integer(value), TYPE_INT);
    }


    /**
     *  Creates or replaces a property. The value is a <code>String</code> .
     *
     * @param  name   The name of the property
     * @param  value  The value
     */
    public void setProperty(String name, String value) {
        setProperty(name, value, TYPE_STRING);
    }


    /**
     *  Creates or replaces a property. <code>null</code> values are not
     *  permitted.
     *
     * @param  name   The name of the property
     * @param  value  The value
     * @param  type   The type
     */
    public void setProperty(String name, Object value, String type) {
        if (value == null) {
            throw new NullPointerException("You can't add a null value!");
        }
        properties.put(name, value);
        propertyTypes.put(name, type);
    }


    /**
     *  Retrieves the value associated with a name, without regard of its type.
     *
     * @param  name  The name
     * @return       The associated property value
     */
    public Object getProperty(String name) {
        return properties.get(name);
    }


    /**
     *  Retrieves the type of a property
     *
     * @param  name  The name of the property
     * @return       The type
     */
    public String getPropertyType(String name) {
        return (String) propertyTypes.get(name);
    }


    /**
     *  Retrieves the <code>double</code> value associated with a name
     *
     * @param  name  The name
     * @return       The value
     */
    public double getDoubleProperty(String name) {
        return ((Double) getPropertyOfType(name, TYPE_DOUBLE)).doubleValue();
    }


    /**
     *  Retrieves the <code>int</code> value associated with a name
     *
     * @param  name  The name
     * @return       The value
     */
    public int getIntProperty(String name) {
        return ((Integer) getPropertyOfType(name, TYPE_INT)).intValue();
    }


    /**
     *  Retrieves the <code>String</code> value associated with a name
     *
     * @param  name  The name
     * @return       The value
     */
    public String getStringProperty(String name) {
        return (String) getPropertyOfType(name, TYPE_STRING);
    }


    /**
     *  Retrieves the value associated with a name, without regard of its type.
     *  This method exists only because the <code>Map</code> interface requires
     *  it. It is recommended that you use <code>getProperty()</code> instead.
     *
     * @param  key  The object whose <code>toString()</code> method will provide
     *      the name
     * @return      The associated property value
     */
    public Object get(Object key) {
        return getProperty(key.toString());
    }


    /**
     *  Checks if any properties are defined or not.
     *
     * @return    <code>true</code> if no mappings are defined
     */
    public boolean isEmpty() {
        return properties.isEmpty();
    }


    /**
     *  Copies all properties (and their types) from the given <code>
     *  TypedProperties</code> to this one. Existing properties with the same
     *  name are overwritten, as usual.
     *
     * @param  other  The <code>TypedProperties</code> containing the properties
     *      to be copied
     */
    public void loadProperties(TypedProperties other) {
        properties.putAll(other.properties);
        propertyTypes.putAll(other.propertyTypes);
    }


    /**
     *  Removes a property
     *
     * @param  name  The name of the property to be removed
     * @return       The value previously associated with the name, or <code>
     *      null</code> if the name was not bound
     */
    public Object removeProperty(String name) {
        Object property = getProperty(name);
        properties.remove(name);
        propertyTypes.remove(name);
        return property;
    }


    /**
     *  Removes all properties
     */
    public void clear() {
        properties.clear();
        propertyTypes.clear();
    }


    /**
     *  Checks whether an <code>Object</code> is a key in a mapping, that is, if
     *  there is a property of that name.
     *
     * @param  key  The object whose <code>toString()</code> method will provide
     *      the name
     * @return      <code>true</code> if the name is mapped to a value
     */
    public boolean containsKey(Object key) {
        return properties.containsKey(key);
    }


    /**
     *  Checks whether an <code>Object</code> is a value in a mapping, that is,
     *  if there is a property with that value.
     *
     * @param  value  The value
     * @return        <code>true</code> if any name is mapped to that value
     */
    public boolean containsValue(Object value) {
        return properties.containsValue(value);
    }


    /**
     *  Returns the set of mappings.
     *
     * @return    The set of mappings.
     */
    public Set entrySet() {
        return properties.entrySet();
    }


    /**
     *  Tests the given <code>Object</code> for equality with this one.
     *
     * @param  o  The other object
     * @return    <code>true</code> if and only if the other object is a <code>
     *      TypedProperties</code> containing the exact same properties as this
     *      one.
     */
    public boolean equals(Object o) {
        if (o instanceof TypedProperties) {
            TypedProperties other = (TypedProperties) o;
            return this.properties.equals(other.properties) &&
                    this.propertyTypes.equals(other.propertyTypes);
        } else {
            return false;
        }
    }


    /**
     *  Computes a hash code for this object.
     *
     * @return    An integer suitable for hashing this object
     */
    public int hashCode() {
        return properties.hashCode() ^ propertyTypes.hashCode();
    }


    /**
     *  Returns the set of all names of properties that have been defined
     *
     * @return    The set of names
     */
    public Set keySet() {
        return properties.keySet();
    }


    /**
     *  Creates or replaces a property. Its type is marked as undefined. This
     *  method exists only because the <code>Map</code> interface requires it.
     *  It is recommended that you use one of the <code>setProperty()</code>
     *  methods instead.
     *
     * @param  key    The object whose <code>toString()</code> method will
     *      provide the name
     * @param  value  The value
     * @return        The value previously associated with the name, or <code>
     *      null</code> if the name was unbound
     */
    public Object put(Object key, Object value) {
        String name = key.toString();
        Object ret = getProperty(name);
        setProperty(name, value, TYPE_UNDEFINED);
        return ret;
    }


    /**
     *  Copies all the key/value pairs from a map as properties in this mapping.
     *  The names are given by the keys' <code>toString()</code> methods. The
     *  types are marked as undefined.
     *
     * @param  t  The map with the properties to insert
     */
    public void putAll(Map t) {
        Iterator iter = t.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            put(entry.getKey(), entry.getValue());
        }
    }


    /**
     *  Removes a property This method exists only because the <code>Map</code>
     *  interface requires it. It is recommended that you use <code>
     *  removeProperty()</code> instead.
     *
     * @param  key  The object whose <code>toString()</code> method will provide
     *      the name
     * @return      The value previously associated with the name, or <code>null
     *      </code>if the name was not bound
     */
    public Object remove(Object key) {
        return removeProperty(key.toString());
    }


    /**
     *  Calculates the number of properties stored in this mapping
     *
     * @return    The number of properties
     */
    public int size() {
        return properties.size();
    }


    /**
     *  Returns a <code>Collection</code> view of all values stored in this
     *  mapping
     *
     * @return    A collection of values
     */
    public Collection values() {
        return properties.values();
    }


    /**
     *  Retrieves the value associated with a name. If its type is not as
     *  specified, or if the name is not bound, a <code>NoSuchElementException
     *  </code>is thrown.
     *
     * @param  name  The name
     * @param  type  The type
     * @return       The associated value
     */
    private Object getPropertyOfType(String name, String type) {
        String actualType = getPropertyType(name);
        if (type.equals(actualType)) {
            return getProperty(name);
        } else {
            throw new NoSuchElementException("There was no property named "
                     + name + " of type " + type + ".");
        }
    }
}
