package org.jcon.util;

import java.io.Serializable;
import java.util.Enumeration;
import java.util.Hashtable;

/**
* This class is a set of key value pairs, aka properties.
* It's designed to make parameter driven designs easier to
* implement. It behaves like a Hashtable, except keys must
* be Strings and null values are supported. For your coding
* pleasure many convenience methods are included. :-)
* <p>
* This implementation is not thread-safe, although it may
* behave that way under certain conditions of use.
*
* @author Steve Alexander
*/
public class PropMapStd implements PropMap {

/*
  STATE INVARIANTS

  Hashtable properties will never be null.
  All keys will be Strings.
  Keys will never have a null value.
  NULL can be tested for absolute equality (==),
    even after serialization. (check this!).

  Null values and serialization: Sorted! See end of file.
*/
//---------- Internal Fields -----------------------------
protected Hashtable properties = new Hashtable();

//---------- PropMap Implementation ----------------------
//----- Methods from PropMap
// None, all methods are from what PropMap extends

//----- Methods associated with Cloneable
/**
* Returns a copy of this PropMapStd. The actual values will 
* be the same instances of those values, so this is a 
* shallow clone.
* 
* @return a shallow clone of this instance.
*/
public Object clone() {
  try {
    PropMapStd map = (PropMapStd) super.clone();
    map.properties = (Hashtable) properties.clone();
    return map;
    
  } catch (CloneNotSupportedException cnse) {
    // this should never happen, since we are cloneable
    String text = "PropMapStd.clone() failed!";
    print(".clone() - " + text);
    throw new InternalError(text);
  }
}
//----- Methods from Serializable thru PropMap ---- //
  // no methods

  // ---- methods from BasicPropMap thru PropMap ---- //

  /**
   * Sets a named property, overwriting any previous value
   * that already exists with the same key.
   *
   * @param key  The unique key, which may not be null.
   * @param value  the value, which may be null .
   *
   * @return the previous value or null if none. 
   */
  public Object set(String key, Object value) {
    return properties.put(key, encapsulateNull(value));
  }

  /**
   * Returns the named property or null if not found or
   * the value is null.
   *
   * @param key  the unique key.
   *
   * @return the value for the key or null if none.
   */
  public Object get(String key) {
    return decapsulateNull(properties.get(key));
  }

  /**
   * Removes the key and its value. This has no effect if the
   * key is not in this collection. This method should be used 
   * carefully since collaborators may be
   * relying on this collection's structure.
   *
   * @param key  the desired key to remove.
   *
   * @return the value removed or null if none or value is null.
   */
  public Object removeKey(String key) {
    return properties.remove(key);
  }

  /*
   * REMOVED 5 July 1999. Erase if still present after 20 August 1999.
   * SteveA.
   *
   * Same as removeKey(key) except it removes all keys
   * with the argument value. This is a convenience method.
   * <P>
   * There is the question of whether this method is needed.
   * At present, this method is not used. We should re-examine
   * the rationale for this method as it is used.
   *
   * @param value  the desired value to remove.
   *
   * @return the number of keys removed or zero if none.
   *
  public int removeValue(Object value) {
    // java.util.Hashtable has no removeValue(value) method.
    // perhaps, Map (from the Collections) does?
    // - ah well, we'll have to enumerate through the values for this one.

    value = (value == null) ? NULL : value;
    int removedCount = 0;
    Object keyTmp;
    Object valueTmp;
    for (Enumeration e=properties.keys(); e.hasMoreElements(); ) {
      keyTmp = e.nextElement();
      valueTmp = properties.get(keyTmp);
      if (value.equals(valueTmp)) {
        // we found it!
        properties.remove(keyTmp);
        ++removedCount;
      }
    }
    return removedCount;
  }
  */

  /**
   * Used to determine if the set has the key.
   * If get(key) returns null then containsKey(key) can be
   * used to determine if this means the set has the
   * key or the value is really null.
   *
   * @param key  the key in question.
   *
   * @return true if the set has the property, false if not.
   */
  public boolean containsKey(String key) {
    return properties.containsKey(key);
  }

  /**
   * Used to determine if the set has the value.
   *
   * @param key  the value in question.
   *
   * @return  true if the set has one or more references to
   *               the value, false if none.
   */
  public boolean containsValue(Object value) {
    return properties.contains(encapsulateNull(value));
  }

  /**
   * Returns an enumeration of all keys in the set.
   *
   * @return  the enumeration of keys.
   */ 
  public Enumeration enumerateKeys() {
    return properties.keys();
  }

  /*
   * This method is REMOVED: if we turn out to need it, it can be
   * reinstated. If it remains unused by 20 August 1999, consider
   * permanent removal.
   * SteveA. 5 July 1999.
   *
   * Returns an enumeration of all values in the set.
   *
   * @return  the enumeration of values, excluding nulls.
   *
  public Enumeration enumerateValues() {
    // FIX ME: need to use proxy pattern to remove nulls.
    return properties.elements();
  }
  */

  /**
   * Used to determine if the set is empty.
   *
   * @return  true if empty, false if contains one or more keys.
   */
  public boolean isEmpty() {
    return properties.isEmpty();
  }

  /**
   * Used to determine how many keys are in the set.
   *
   * @return  the number of keys in the set, which is zero or more.
   */
  public int getSize() {
    return properties.size();
  }

  /**
   * Replaces all of its own state with all state in the
   * argument PropMap.
   *
   * Note - this is implemented as a shallow copy.
   *
   * @param  propMap  the propMap to copy state from.
   */
  public void replaceAllWith(PropMap propMap) {
    if (propMap instanceof PropMapStd) {
      properties = (Hashtable) ((PropMapStd) propMap).properties.clone();
    } else {
      /* This next block could be mined as a reusable for similar
         methods of other PropMap implementations.
         REMEMBER: we cannot rely on other implementations having the
         same notion of "null".
      */
      String key;
      Object value;
      properties.clear();
      for (Enumeration keys=propMap.enumerateKeys(); keys.hasMoreElements(); ) {
        key = (String) keys.nextElement();
        properties.put(key, encapsulateNull(propMap.get(key)));
      }
    }
  }

 /**
  * Returns a comma delimited string of key/values, for
  * example: "[Name=Pantajeli, Age=11]".
  * This is designed to be easily readable.
  *
  * @return the String representation in standard format.
  */
  public String toString() {
    StringBuffer entries = new StringBuffer("[");

    String keyTmp;
    Object valueTmp;
    for (Enumeration keys=properties.keys(); keys.hasMoreElements(); ) {
      keyTmp = (String) keys.nextElement();
      valueTmp = decapsulateNull(properties.get(keyTmp));
      valueTmp = (valueTmp == null) ? "null" : valueTmp;
      entries.append(keyTmp).append("=").append(valueTmp.toString());
      if (keys.hasMoreElements()) {
        entries.append(", ");
      }
    }
    entries.append("]");
    return entries.toString();
  }

  // ---- methods from ConvenientStringMap thru PropMap ---- //

  // ----- String methods
  /**
  * Sets the key value, overwriting any that already exists.
  * @param key   the unique key, which may not be null.
  * @param value the String value, which may be null.
  */
  public void setString(String key, String value) {
    set(key, value);
  }

  /**
   * Returns the String value for the key.
   * @param key  the key to be used for the lookup.
   * @return     the key's value or null.
   */
  public String getString(String key) {
    Object value = get(key);
    return (value == null) ? null : value.toString(); 
  }

  /**
   * Same as getString(key) except allows a default to be
   * returned rather than null.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if the
   *    return would otherwise be null.
   * @return     the key's value or the defaultValue if not found.
   */
  public String getStringDefault(String key, String defaultValue) {
    Object value = get(key);
    return (value == null) ? defaultValue : value.toString();
  }

  // ----- boolean methods
  /**
   * Sets the key value, overwriting any that already exists.
   * @param key   the unique key, which may not be null.
   * @param value the boolean value.
   */
  public void setBoolean(String key, boolean value) {
    set(key, value ? Boolean.TRUE : Boolean.FALSE);
  }

  /**
   * Same as setBoolean(key, true).
   */
  public void setTrue(String key) {
    set(key, Boolean.TRUE);
  }

  /**
   * Same as setBoolean(key, false).
   */
  public void setFalse(String key) {
    set(key, Boolean.FALSE);
  }

  /**
   * Returns the boolean value for the key.
   * We assume that the setBoolean method was used to set this value.
   * If a non-Boolean value is found, an IllegalStateException is thrown.
   * If no value is set, an IllegalStateException is thrown.
   * @param key  the key to be used for the lookup.
   * @return     the key's value or an exception if not found.
   */
  public boolean getBoolean(String key) {
    // optimistic implementation :)
    try {
      return ((Boolean) get(key)).booleanValue();
    } catch (ClassCastException cce) {
      String s1 = "Stored value was not a Boolean";
      throw new IllegalStateException(s1);
    } catch (NullPointerException npe) {
      String s2;
      if  (containsKey(key)) {
        s2 = "Stored value was null";
      } else {
        s2 = "Key "+key+" has no associated value";
      }
      throw new IllegalStateException(s2);
    }    
  }

  /**
   * Used to determine if the key value is true.
   * @param key  the key in question.
   * @return     true if the key's value is true, false if
   *    it's false, or an exception if not found.
   */
  public boolean isTrue(String key) {
    return getBoolean(key) == true;
  }

  /**
   * Same as isTrue(key) except allows a default to be
   * returned if not found rather than an exception.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if the
   *    value is not found.
   * @return     true if the key's value is true, false if
   *    it's false, or the defaultValue if not found.
   */
  public boolean isTrueDefault(String key, boolean defaultValue) {
    Object value = get(key);
    if (value != null && value instanceof Boolean) {
      return ((Boolean) value).booleanValue();
    } else {
      return defaultValue;
    }
  }

  /**
   * Used to determine if the key value is false.
   * @param key  the key in question.
   * @return     true if the key's value is false, false if
   *    it's true, or an exception if not found.
   */
  public boolean isFalse(String key) {
    return !isTrue(key);
  }

  /**
   * Same as isFalse(key) except allows a default to be
   * returned if not found rather than an exception.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if the
   *    value is not found.
   * @return     true if the key's value is false, false if
   *    it's true, or the defaultValue if not found.
   */
  public boolean isFalseDefault(String key, boolean defaultValue) {
    // watch out for this... it is a bit tricky!
    return !isTrueDefault(key, !defaultValue);
  }

  // ----- int methods
  /**
   * Sets the key value, overwriting any that already exists.
   * @param key   the unique key, which may not be null.
   * @param value the int value.
   */
  public void setInt(String key, int value) {
    set(key, new Integer(value));
  }

  /**
   * Returns the int value for the key.
   * @param key  the key to be used for the lookup.
   * @return     the key's value or an exception if not found.
   */
  public int getInt(String key) {
    // optimistic implementation :)
    try {
      return ((Integer) get(key)).intValue();
    } catch (ClassCastException cce) {
      String s1 = "Stored value was not an Integer";
      throw new IllegalStateException(s1);
    } catch (NullPointerException npe) {
      String s2;
      if  (containsKey(key)) {
        s2 = "Stored value was null";
      } else {
        s2 = "Key "+key+" has no associated value";
      }
      throw new IllegalStateException(s2);
    }    
  }

  /**
   * Same as getInt(key) except allows a default to be
   * returned rather than an exception if not found.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if not found.
   * @return the key's value or the defaultValue if not found.
   */
  public int getIntDefault(String key, int defaultValue) {
    Object value = get(key);
    if (value != null && value instanceof Integer) {
      return ((Integer) value).intValue();
    } else {
      return defaultValue;
    }
  }

  // ----- long methods
  /**
   * Sets the key value, overwriting any that already exists.
   * @param key   the unique key, which may not be null.
   * @param value the long value.
   */
  public void setLong(String key, long value) {
    set(key, new Long(value));
  }

  /**
   * Returns the long value for the key.
   * @param key  the key to be used for the lookup.
   * @return     the key's value or an exception if not found.
   */
  public long getLong(String key) {
    // optimistic implementation :)
    try {
      return ((Long) get(key)).longValue();
    } catch (ClassCastException cce) {
      String s1 = "Stored value was not a Long";
      throw new IllegalStateException(s1);
    } catch (NullPointerException npe) {
      String s2;
      if  (containsKey(key)) {
        s2 = "Stored value was null";
      } else {
        s2 = "Key "+key+" has no associated value";
      }
      throw new IllegalStateException(s2);
    }    
  }

  /**
   * Same as getLong(key) except allows a default to be
   * returned rather than an exception if not found.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if not found.
   * @return the key's value or the defaultValue if not found.
   */
  public long getLongDefault(String key, long defaultValue) {
    Object value = get(key);
    if (value != null && value instanceof Long) {
      return ((Long) value).longValue();
    } else {
      return defaultValue;
    }
  }

  // ----- float methods
  /**
   * Sets the key value, overwriting any that already exists.
   * @param key   the unique key, which may not be null.
   * @param value the float value.
   */
  public void setFloat(String key, float value) {
    set(key, new Float(value));
  }

  /**
   * Returns the float value for the key.
   * @param key  the key to be used for the lookup.
   * @return     the key's value or an exception if not found.
   */
  public float getFloat(String key) {
    // optimistic implementation :)
    try {
      return ((Float) get(key)).floatValue();
    } catch (ClassCastException cce) {
      String s1 = "Stored value was not a Float";
      throw new IllegalStateException(s1);
    } catch (NullPointerException npe) {
      String s2;
      if  (containsKey(key)) {
        s2 = "Stored value was null";
      } else {
        s2 = "Key "+key+" has no associated value";
      }
      throw new IllegalStateException(s2);
    }    
  }

  /**
   * Same as getFloat(key) except allows a default to be
   * returned rather than an exception if not found.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if not found.
   * @return the key's value or the defaultValue if not found.
   */
  public float getFloatDefault(String key, float defaultValue) {
    Object value = get(key);
    if (value != null && value instanceof Float) {
      return ((Float) value).floatValue();
    } else {
      return defaultValue;
    }
  }

  // ----- double methods
  /**
   * Sets the key value, overwriting any that already exists.
   * @param key   the unique key, which may not be null.
   * @param value the double value.
   */
  public void setDouble(String key, double value) {
    set(key, new Double(value));
  }

  /**
   * Returns the double value for the key.
   * @param key  the key to be used for the lookup.
   * @return     the key's value or an exception if not found.
   */
  public double getDouble(String key) {
    // optimistic implementation :)
    try {
      return ((Double) get(key)).doubleValue();
    } catch (ClassCastException cce) {
      String s1 = "Stored value was not a Double";
      throw new IllegalStateException(s1);
    } catch (NullPointerException npe) {
      String s2;
      if  (containsKey(key)) {
        s2 = "Stored value was null";
      } else {
        s2 = "Key "+key+" has no associated value";
      }
      throw new IllegalStateException(s2);
    }    
  }

  /**
   * Same as getDouble(key) except allows a default to be
   * returned rather than an exception if not found.
   * @param key  the key to be used for the lookup.
   * @param defaultValue the default to be used if not found.
   * @return the key's value or the defaultValue if not found.
   */
  public double getDoubleDefault(String key, double defaultValue) {
    Object value = get(key);
    if (value != null && value instanceof Double) {
      return ((Double) value).doubleValue();
    } else {
      return defaultValue;
    }
  }


  // ---- null value support classes ---- //

  // guarenteed to be run before any instances of class are created.
  private static final Object nullValue = new NullValue();

  /**
   * Prepares a user object for storage in a Hashtable.
   * Essentially, we return the object as-is, unless it is null.
   * If the object is null, we replace it with a special NullValue.
   */
  protected static final Object encapsulateNull(Object obj) {
    return (obj == null) ? nullValue : obj;
  }

  /**
   * Undoes the effect of encapsulateNull(). 
   * We return the object as-is, unless it is a NullValue.
   * If the object is a NullValue, return null.
   */ 
  protected static final Object decapsulateNull(Object obj) {
    return (nullValue.equals(obj)) ? null : obj;
  }

  /**
   * This class represents a null value. It is converted into a proper
   * "null" for presentation to users of PropMapStd.
   * This class is for internal use only.
   */
  protected static final
  class NullValue implements Serializable {
    /** All NullValue instances are equal to each other. */
    public boolean equals(Object obj) {
      if (obj == this || obj != null && obj instanceof NullValue) {
        return true;
      } else {
        return false;
      }
    }
    /** This should never get printed to the screen. */
    public String toString() {
      return "** instance of NullValue **";
    }    
  }

  // ---- standard convenience methods ---- //
  private static void print(String text) {
    System.out.println(text);
  }

}
