package org.jcon.param;

import org.jcon.util.GenLib;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;

/**
* Carries a collection of named parameters. Convenience
* methods are provided for some popular types.
* <p>
* Note the powerful ability to retreive values given
* a name path. For basic use only the classes Param,
* ParamConverter and ParamStore are needed.
* <p>
* "Original" text is the text originally loaded from 
* storage or a source. This is opposed to "full" text,
* which is the full text after includes and is what is
* used to define the Param data structure. If no full
* text is supplied then the full text equals the original
* text. Normally the user should think of the original
* text and the Param's text, and view and edit that.
* <p> <pre>
* Known weaknesses:
* - Cannot remove
* - Cannot maintain comments programatically
*      (they must be edited in text)
* - Comments cannot be between Elements (fixed Sept 98)
* </pre> <p>
* Note - ParamAccessor will become String paramID, for use
* as a URL or such. Also, we are not yet using defaults.
*
* @author Jack Harich
*/
public class Param implements java.io.Serializable, Cloneable {

//---------- Private Fields ------------------------------
// Key = name, Object = object, usually a primitive wrapper
private Hashtable params = new Hashtable();

// Contains parms keys in order added
private Vector    paramKeys = new Vector();

// Contains zero or more params to be searched if a
// parameter is not found in the Hashtable.
private Vector    defaults;

private ParamAccessor accessor; // *** Will be changed to be String paramID
private String originalText;
private String fullText; // null if none
private ParamDriven paramDriven;

//---------- Superclass Overrides ------------------------
/**
* Returns the original text for this Param.
*/
public String toString() {
    try {

        if (originalText == null) ParamConverter.buildText(this);
        return ParamConverter.toString(this);

    } catch(Exception ex) {
        GenLib.exception("Param.toString()",
            "Cannot convert to String.", ex);
        return null;
    }
}
/**
* Creates a shallow clone which has the same internal state
* instances. This should not usually cause a problem.
*/
public Object clone() {
    Param newParam = new Param();
    newParam.setParams(params);
    newParam.setParamKeys(paramKeys);
    newParam.setDefaults(defaults);
    transferState(newParam);
    return newParam;
}
// Closely related methods:
/**
* Returns a clone using new text. This is useful
* when editing a param's text and one wishes to create a
* new Param.
*/
public Param cloneUsingNewText(String newText) {
    Param newParam = ParamConverter.toParam(newText);
    transferState(newParam);
    return newParam;
}
public Param cloneUsingFullOriginalText(String fullText, String originalText) {
    Param newParam = ParamConverter.toParam(fullText, originalText);
    transferState(newParam);
    return newParam;
}
private void transferState(Param newParam) {
    if (accessor != null) {
        newParam.accessor = (ParamAccessor)accessor.clone();
    }
    newParam.setParamDriven(paramDriven);
}
/**
 * Turns itself into a shallow copy of otherParam.
 */
public void copy(Param otherParam) {
    // Copy all fields
    this.params       = otherParam.params;
    this.paramKeys    = otherParam.paramKeys;
    this.defaults     = otherParam.defaults;
    this.accessor     = otherParam.accessor;
    this.originalText = otherParam.originalText;
    this.fullText     = otherParam.fullText;
    this.paramDriven  = otherParam.paramDriven;
}
//---------- Properties ----------------------------------
/**
 * Sets the object that is driven by this Param. This is
 * for use in later calling applyNewParam().
 */
public void setParamDriven(ParamDriven paramDriven) {
    this.paramDriven = paramDriven;
}
public ParamDriven getParamDriven() {
    return paramDriven;
}
//----- Other
public ParamAccessor getAccessor() {
    if (accessor == null) accessor = new ParamAccessor();
    return accessor;
}
//---------- Package Methods -----------------------------
//--- Params
Hashtable getParams() {
    return params;
}
void setParams(Hashtable params) {
    this.params = params;
}
//--- ParamKeys
Vector getParamKeys() {
    return paramKeys;
}
void setParamKeys(Vector paramKeys) {
    this.paramKeys = paramKeys;
}
//--- Defaults
Vector getDefaults() {
    return defaults;
}
void setDefaults(Vector defaults) {
    this.defaults = defaults;
}
//--- OriginalText
/**
* Sets the "original" text.
*/
void setText(String originalText) {
    this.originalText = originalText;
}
/**
 * Returns the current originalText that was used to initialize
 * this Param. This text will be out-of-date if the Param's
 * state has been manipulated. See buildText().
 * This method is useful for getting rollback state, which
 * changes when buildText() is called.
 */
public String getText() {
    return originalText;
}
//----- FullText
/**
* Sets the "full" text.
*/
void setFullText(String fullText) {
    this.fullText = fullText;
}
/**
* Returns the full text. If the internal full text is null
* then this returns the original text.
*/
public String getFullText() {
    return (fullText != null ? fullText : originalText);
}
/**
* Returns true if the param has full text, false if not.
*/
public boolean hasFullText() {
    return (fullText != null ? true : false);
}
//---------- Public Methods ------------------------------
//----- Parameter mutators
/**
 * Puts a named value into this Param.
 * If already there an overwrite occurs. The name may be
 * a period delimited path or a single name.
 * No spaces are allowed in the name. RECURSIVE.
 * <p>
 * To store an empty String use "". null values
 * are not supported and will cause an exception. If the
 * value is a String it is interned to greatly reduce
 * Strings in memory. All names are interned.
 * <p>
 * To create a "hasLines:" entry, put a org.jcon.param.StringVector
 * into a param using the preferred name.
 *
 * @param name  the property name, unique per Param.
 * @param value the property value, must be a String, Param,
 *              StringVector or Vector of Params. 
 * @return the previous Object for this name or null if none.
 *         This can be used to prevent duplicate names.
 */
public Object put(String name, Object value) {
    // Validate name argument
    if (name.indexOf(" ") > -1) throw new IllegalArgumentException(
        "Spaces are not allowed in names.");
    // Validate value argument
    if (! (value instanceof String)       &&
        ! (value instanceof Param)        &&
        ! (value instanceof StringVector) &&
        ! (value instanceof Vector)) {
        throw new IllegalArgumentException("The value must"
            + " be a String, Param, StringVector or Vector.");    
    }
    // Proceed
    Object previousValue = null;
    int delimIndex = name.indexOf(".");
    if (delimIndex > -1) {
        // name is a path
        String firstName = name.substring(0, delimIndex);
        String remainderName = name.substring(delimIndex + 1);
        Param firstParam = (Param)params.get(firstName);
        // Create firstParam if necessary
        if (firstParam == null) {
            firstParam = new Param();
            putKey(firstName, firstParam);
        }
        // put remaining path into firstParam - RECURSE
        firstParam.put(remainderName, value);

    } else {
        // name is not a path. Recursion ends here.
        previousValue = putKey(name, value);
    }
    return previousValue;
}  
// put() helper
private Object putKey(String key, Object value) {
    //print(".putKey() - key: " + key + ", value: " + value);
    // Intern Strings
    key = key.intern();
    if (value instanceof String) value = ((String)value).intern();

    if (params.containsKey(key) == false) {
        paramKeys.addElement(key);
    }
    return params.put(key, value);
}
/**
* A convenience method for putting ints.
*/
public Object putInt(String name, int value) {
    return put(name, String.valueOf(value));   
}
/**
* A convenience method for putting booleans.
*/
public Object putBoolean(String name, boolean value) {
    return put(name, String.valueOf(value));   
}
/**
 * Defaults are optional. If added they are searched in the
 * order added if a value is not found in the main params.
 */
public void addDefaultParam(Param defaultParam) {
    if (defaults == null) defaults = new Vector(); // Memory optimization
    defaults.addElement(defaultParam);
}
//----- Parameter readers
//      Note all but get() may throw runtime exceptions
/**
 * For all getters -
 * @param name may be a single name or a period delimited
 * path of names such as "Column.3.Width" or "Title".
 * NOTE - Vector not yet supported in path, this will be
 * "VectorName(index)" format. *********
 * <p>
 * Returns null if not found. All getters except get() will
 * cause exceptions if the requested type is incorrect.
 */
public Object get(String name) {
    return readPath(name);
}
/**
 * Same as get() except returns an interned String.
 * Returns null if the property doesn't exist.
 */
public String getString(String name) {
    return (String)readPath(name);
}
/**
* Same as getString() except returns the defaultValue if not found.
*/ 
public String getStringDefault(String name, String defaultValue) {
    String value = (String)readPath(name);
    return (value != null ? value : defaultValue);
}
public Param getParam(String name) {
    return (Param)readPath(name);
}
/**
* Returns a Vector that may contain: <p> <pre>
* - Strings only, This is a hasLines: Vector
* - Params and Strings. This is a hasElements: Vector </pre>
* <p> </pre>
* See getLinesVector() for the best way to get a StringVector.
* Returns null if not found.
*/
public Vector getVector(String name) {
    return (Vector)readPath(name);
}
/**
* Returns a vector of Params from a hasElements: Vector.
* This is a convenience method for getVector(), which
* requires skipping String elements which are comments.
* We return a Vector instead of an Enumeration since this
* is not used much, and we are used to Vectors.
* Returns empty Vector if not found.
*/
public Vector getParamVector(String name) {
    Vector params = new Vector();
    Vector elements = (Vector)readPath(name);
    if (elements == null) return params;
    
    int size = elements.size();
    for (int i = 0; i < size; i++) {
        Object element = elements.elementAt(i);
        if (element instanceof Param) {
            params.addElement(element);
        }
    }
    return params;
}
/**
* Returns a vector of Strings from a hasLines: Vector,
* excluding comments. This is preferred to getVector() if
* a StringVector. If not found returns an empty Vector.
*/
public Vector getLinesVector(String name) {
    Vector fullLines = getVector(name);   
    Vector lines = new Vector();
    if (fullLines == null) return lines;
    
    int size = fullLines.size();
    
    for (int i = 0; i < size; i++) {
        String line = (String)fullLines.elementAt(i);
        if (! line.startsWith("//")) lines.addElement(line);
    }
    return lines;
}
/**
* Returns a String from a hasLines: Vector,
* excluding comments. This is a convenience method.
* The lineSeparator may be " " or "\n" or such.
* Returns null if not found.
*/
public String getLinesVectorString(String name, String lineSeparator) {
    Vector fullLines = getVector(name);
    if (fullLines == null) return null;
    
    StringBuffer buffer = new StringBuffer();
    if (lineSeparator == null) lineSeparator = "";
    int size = fullLines.size();
    
    for (int i = 0; i < size; i++) {
        String line = (String)fullLines.elementAt(i);
        if (! line.startsWith("//")) {
            buffer.append(line);
            if (lineSeparator != "") buffer.append(lineSeparator);
        }
    }
    // Remove last lineSeparator
    if (lineSeparator == "") {
        return buffer.toString();    
    } else {
        String line = buffer.toString();
        if (line.endsWith(lineSeparator)) {
            return line.substring(0, line.length() - lineSeparator.length());
        } else {
            return line;
        }
    }
}
/**
 * Returns the int value or throws NumberFormatException
 * if not found or illegal value. See getIntDefaultZero().
 */
public int getInt(String name) {
    return Integer.parseInt(getString(name));
}
/**
 * Returns the int value or zero if not found. This is
 * for optimization, to skip hasProperty().
 */
public int getIntDefaultZero(String name) {
    try {
        return Integer.parseInt(getString(name), 10);
    } catch(Exception ex) {
        return 0;
    }
}
/**
 * Returns the long value or throws NumberFormatException
 * if not found or illegal value.
 */
public long getLong(String name) {
    return Long.valueOf(getString(name)).longValue();
}
/**
 * Returns the double value or throws NumberFormatException
 * if not found or illegal value.
 */
public double getDouble(String name) {
    return Double.valueOf(getString(name)).doubleValue();
}
/**
 * Returns the boolean value or throws NullPointerException
 * if not found. See isTrueDefaultFalse().
 */
public boolean isTrue(String name) {
    return (getString(name).equals("true") ? true : false);
}
/**
 * Returns the boolean value or false if not found. This is
 * for optimization, to skip hasProperty().
 */
public boolean isTrueDefaultFalse(String name) {
    if ("true".equals(getString(name))) {
        return true;
    } else {
        return false; 
    }
}
/**
 * Returns the boolean value or true if not found. This is
 * for optimization, to skip hasProperty().
 */
public boolean isTrueDefaultTrue(String name) {
    if ("false".equals(getString(name))) {
        return false;
    } else {
        return true; 
    }
}
// RECURSIVE read using a name path. Returns null if not found.
private Object readPath(String path) { // Here for clarity
    // Optimization
    if (path.indexOf(".") == -1) return getParameter(path);

    // Path contains multiple keys
    Param currentParams = this;
    StringTokenizer names = new StringTokenizer(path, ".");
    while (names.hasMoreTokens()) {
        String name = names.nextToken();
        if (names.hasMoreTokens()) {
            currentParams = currentParams.getParam(name);
        } else {
            if (currentParams == null) return null;
            return currentParams.getParameter(name);
        }
    }
    return "#Error - Params.readPath()#";
}
//----- Other
/**
 * Builds the originalText in a Param. This needs to be 
 * done if a Param is created in code, rather than loading
 * from a text file or other source. Using this method will
 * build and return text representing the Param's current
 * state.
 */
public String buildText() {
    ParamConverter.buildText(this);
    return originalText;
}
public boolean hasProperty(String name) {
    return (readPath(name) == null ? false : true);
}
/**
 * Returns all key names in the order in which they were
 * originally added. This is useful, for example for
 * ordered columns, or keeping the keys in order through
 * ParamConverter conversions.
 *
 * This method is designed for use by the ParamConverter.
 * **** Change to return String[] ****
 */
public Vector getAllKeys() {
    return (Vector)paramKeys.clone();
}
/**
 * Returns only those keys representing data, which excludes
 * elements such as comments and blank lines. The keys are
 * in their original order and are interned.
 * <p>
 * This method is designed for data access, ie normal use.
 */
public String[] getDataKeys() {
    Vector keys = new Vector();
    for (int i = 0; i < params.size(); i++) {
        Object key = paramKeys.elementAt(i);
        if (ParamConverter.keyHasData(key)) keys.addElement(key);
    }
    String[] stringKeys = new String[keys.size()];
    for (int i = 0; i < keys.size(); i++) {
        stringKeys[i] = ((String)keys.elementAt(i)).intern();
    }
    return stringKeys;
}
/**
 * Performs the crucial act of applying the new Param to
 * this Param's holder. Note that no state changes occur
 * in this Param. This is merely a two way link.
 * <p>
 * *** Note - This is bad practice, needs changing. JH
 * The paramDriven must be Serializable, this class's
 * respoinsiblities are muddied and more dependencies are 
 * created.
 */
public boolean applyNewParam(Param newParam) {
    if (paramDriven == null) {
        GenLib.error("Param.applyNewParam()", "Cannot" +
            " apply since paramDriven is null.");
        return false;
    } else {
        return paramDriven.applyNewParam(newParam);
    }
}
/**
 * Returns a comma delimited String of the "data keys".
 * This is for debugging.
 */
public String getDelimitedDataKeys() {
    String[] keys = getDataKeys();
    StringBuffer text = new StringBuffer();
    for (int i = 0; i < keys.length; i++) {
        text.append(keys[i]);
        if (i < keys.length - 1) text.append(", ");
    }
    return text.toString();
}
//---------- Package Methods -----------------------------
/**
 * Checks main and then default params, returns first one
 * found. Returns null if not found. Package scope to
 * allow recursive call among different instances.
 */
Object getParameter(String name) {
    Object value = params.get(name);
    if (value != null) return value;

    if (defaults != null) {
        for (int i = 0; i < defaults.size(); i++) {
            Param defaultParams = (Param)defaults.elementAt(i);
            value = defaultParams.get(name);
            if (value != null) return value;
        }
    }
    return value;
}
//---------- Private Methods -----------------------------
//--- Std
private void print(String text) {
    System.out.println("Param" + text);
}

} // End class
