package org.jcon.param;

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

/**
 * Converts a Param instance to and from a String. This
 * allows parameters to be represented as text, with the
 * great benefit of making text parameter files possible.
 *
 * All methods are static.
 *
 * The required format for the String representation is
 * most clearly shown in an example. Nesting may be
 * arbitrarily deep. Note the required End: keyword.<pre>
 *
 *   Title is: Edit Customers
 *   Size is: 200, 300
 *   // This is a comment line...
 *   Columns has:
 *       Pete is: Code, 6
 *       Jack is: Customer, 35, CustName
 *       Willie has:
 *           Title is: Remarks
 *           Width is: 50
 *           Movable is: false
 *           End: Willie
 *       End: Columns
 *   // These are a Vector of Param and String (comment)
 *   Family hasElements:
 *       // Comments can be intersperesed with elements
 *       Element:
 *           Name is: Pete
 *           BirthDate is: 11/4/1948
 *           End: Element
 *       Element:
 *           Name is: Jack
 *           BirthDate is: 10/7/1949
 *           End: Element
 *       End: Family
 *   Datatypes hasLines:
 *       MID,       money,          $,  None
 *       String,    varchar(254),   ',  '
 *       Mask,      varchar(254),   ',  '
 *       End: Datatypes
 *
 * Additional format rules and behavior:
 * - Key names in each param collection MUST be unique.
 *      The only exception is "//" which is stored
 *      internally with unique digit suffixes.
 * - Key names must not have a colon.
 * - Keywords and names are case sensitive. Nothing else is.
 * - Collection names must not contain a collection with
 *      the same name.
 * - The first keyword in a line determines the line type.
 *      Keywords are is:, has:, hasElements:, hasLines,
 *      End: and //. Therefore use one line per keyword.
 * - Blank lines, indentation and extra spaces are ignored
 *      in toParam()
 * - Ignored data is not restored in toString()
 * - Only String values are currently supported
 * - Comments are handled as in the example using "//"
 * - Tabs are not handled and will cause unpredictable results
 * - Vectors are supported as illustrated above.
 *
 * Future features:
 * - Comments not yet supported in Vectors
 * - Blank lines will be restored via BlankLine1, etc.
 *      Until then use "//" only for white space.
 * - Non-string datatypes, Vector collections, extend </pre>
 *
 * @author Jack Harich
 */
public class ParamConverter {

//---------- Public Fields -------------------------------
// A simple approach instead of the XML way
// Referenced by param.build.ParamBuilder
public static final String START_INCLUDE_MARK = "[[";
public static final String END_INCLUDE_MARK   = "]]";

//---------- Private Fields ------------------------------
private static final String IS = "is:";
private static final String HAS = "has:";
private static final String END = "End:";
private static final String COMMENT = "//";
private static final String HAS_ELEMENTS = "hasElements:";
private static final String HAS_LINES = "hasLines:";

private static final String ELEMENT_KEY = "Element";

private static int commentCounter;

//---------- Public Methods ------------------------------
public synchronized static String toString(Param param) {
    //return convertToString(param, 0); // 0 level no indent
    // We are now using text to preserve blanks line
    return param.getText();
}
/**
 * Converts the text to a Param. Throws an 
 * IllegalArgumentException if duplicate property names 
 * are encountered within a single param's properties.
 * Here the original and full text are the same.
 * <p>
 * If the text contains includes then the Param state is
 * not fully built. Only the text is set and we assume that
 * before the Param is used it will be built, ie the includes
 * will be expanded and then the text will be used to
 * create the normal Param.
 */
public synchronized static Param toParam(String text) {
    Param param = null;
    if (text.indexOf(START_INCLUDE_MARK) > -1) {
        // Text contains one or more includes
        param = new Param();
    } else {
        // Text contains no includes
        commentCounter = 0;
        StringTokenizer lines = new StringTokenizer(text, "\n");
        param = convertToParam(lines, null);
    }
    param.setText(text); // Which leaves the fullText null
    return param;
}
/**
* Same as toParam(text) except the full and original text
* are different, and we assume that includes have been
* expanded. Note the full text is the first argument.
*/
public synchronized static Param toParam(
        String fullText, String originalText) {
    Param param = toParam(fullText);
    param.setFullText(fullText);
    param.setText(originalText);
    return param;
}
/**
* Builds the original text in the param by converting the
* param to a String and using that to set the param's
* original text.
*/
public synchronized static void buildText(Param param) {
    String text = convertToString(param, 0);
    param.setText(text);
}
//---------- Package Methods -----------------------------
// Has data if not a String or not a comment
static boolean keyHasData(Object key) {
    if (! (key instanceof String) ) {
        return true; // Probably Param
    } else {
        String keyString = (String)key;
        return (keyString.startsWith(COMMENT) ? false : true);
    }
}
//---------- Private Methods -----------------------------
//----- Supporting toParam()
// intern() greatly reduces Strings in memory
// This is done by Param itself
private static Param convertToParam(  // Recursive
        StringTokenizer lines, String levelKey) {

Param param = new Param();
while (lines.hasMoreTokens() ) {
    String line = lines.nextToken().trim(); // Note trim
    String key = null;
    Object previousValue = null;
    // Determine line type
    if(line.startsWith(COMMENT) ) {
        commentCounter++;
        key = COMMENT + commentCounter;
        String value = "";
        if (line.length() > 2) value = line.substring(2);
        previousValue = param.put(key, value);

    } else if (line.indexOf(IS) > -1) {
        key = getLineFirstWord(line);
        String value = getLineIsValue(line);
        previousValue = param.put(key, value);

    } else if (line.indexOf(HAS) > -1) { // RECURSION
        key = getLineFirstWord(line);
        Param thisParam = convertToParam(lines, key);
        previousValue = param.put(key, thisParam);

    } else if (line.indexOf(HAS_ELEMENTS) > -1) {
        key = getLineFirstWord(line); // VECTOR key
        Vector vector = convertToElementsVector(lines, key);
        previousValue = param.put(key, vector);

    } else if (line.indexOf(HAS_LINES) > -1) {
        key = getLineFirstWord(line); // VECTOR key
        StringVector vector = convertToLinesVector(lines, key);
        previousValue = param.put(key, vector);

    } else if (line.indexOf(END + " " + levelKey) > -1) {
        break; // End of this param and recursion

    } else if(line.length() == 0) {
        // Skip blank line

    } else {
        print(".convertToParam() - Unknown line type encountered:\n" + line);
    }
    if (previousValue != null) throw new IllegalArgumentException(
        "Duplicate property name '" + key + "'\nin line '" + line + "'.");
}
return param;
} // End method
/**
 * NOTE that comments and blank line are not yet supported
 * between HAS_ELEMENTS and ELEMENT_KEY, and between
 * End: ELEMENT_KEY and ELEMENT_KEY. *** MOD
 */
// Returns Vector containing Params and Strings (comments)
private static Vector convertToElementsVector(
        StringTokenizer lines, String levelKey) {
    
    Vector vector = new Vector();
    while (lines.hasMoreTokens() ) {
    
        String line = lines.nextToken().trim().intern(); // Note trim
        //print(" - line = " + line);
        
        // Process each line type
        if(line.startsWith(COMMENT) ) {
            String value = "";
            if (line.length() > 2) value = line.substring(2);        
            vector.addElement(value.intern());       
            //print(" - Added comment");
            
        } else if (line.startsWith(ELEMENT_KEY + ":")) {
            Param thisParam = convertToParam(lines, ELEMENT_KEY);
            vector.addElement(thisParam); 
            //print(" - Added Element Param");
                
        } else if (line.startsWith(END + " " + levelKey)) {
            //print(" - Reached end of elements vector");
            return vector; // End of this vector
            
        } else if(line.length() == 0) {
            // Skip blank line        
            //print(" - Skipping blank line");
    
        } else {
            print(".convertToElementsVector() - Unknown line type encountered:\n" + line);
        }
    }
    GenLib.error("Converter.convertToParamVector()",
        "Did not find the end of param vector named '" + levelKey + "'.");
    return null;
}
private static StringVector convertToLinesVector(
        StringTokenizer lines, String levelKey) {

    StringVector vector = new StringVector();
    while (lines.hasMoreTokens() ) {
        String line = lines.nextToken().trim().intern(); // Note trim
        // Determine line type
        if (line.startsWith(END + " " + levelKey)) {
            return vector; // End of this vector
    
        } else if (line.equals("")) {
            // Skip blank line
    
        } else {
            vector.addElement(line);
        }
    }
    GenLib.error("Converter.convertToLinesVector()",
        "Did not find the end of lines vector named '" + levelKey + "'.");
    return null;
}
private static String getLineFirstWord(String line) {
    int position = line.indexOf(" ");
    return line.substring(0, position);
}
private static String getLineIsValue(String line) {
    int position = line.indexOf(IS);
    return line.substring(position + IS.length()).trim();
}
//----- Supporting toString()
private static String convertToString(Param param,
        int level) { // RECURSIVE
    String text = "";
    Vector keys = param.getAllKeys();
    for (int i = 0; i < keys.size(); i++) {
        String key;
        try {
            key = (String)keys.elementAt(i);
        } catch(ClassCastException ex) {
            GenLib.exception("ParamConverter.convertToString()",
                "Element " + keys.elementAt(i) +
                " is not a String. All keys must be a String.", ex);
            return text + "\n#INCOMPLETE TEXT#";
        }
        Object parameter = param.get(key);
        // Add one line
        text += getIndent(level) + getParamText(key, parameter, level);
        if (i < keys.size() - 1) text += "\n";
    }
    return text;
}
private static String getParamText(String key,
    Object parameter, int level) {
try {
    if (key.startsWith(COMMENT)) {
        return COMMENT + (String)parameter;

    } else if (parameter instanceof String) {
        return key + " " + IS + " " + (String)parameter;

    } else if (parameter instanceof Param) {
        String text = key + " " + HAS + "\n" +
            // RECURSE
            convertToString((Param)parameter, level + 1);
        text += "\n" + getIndent(level + 1) + END + " " + key;
        return text;

    } else if (parameter instanceof StringVector) {
        Vector vector = (Vector)parameter;
        String text = key + " " + HAS_LINES;
        for (int i = 0; i < vector.size(); i++) {
            text += "\n" + getIndent(level + 1) + (String)vector.elementAt(i);
        }
        text += "\n" + getIndent(level + 1) + END + " " + key;
        return text;

    } else if (parameter instanceof Vector) {
        Vector vector = (Vector)parameter;        
        String text = key + " " + HAS_ELEMENTS;        
        for (int i = 0; i < vector.size(); i++) {

            Object element = vector.elementAt(i);
//print(" - Converting ElementsVector to text, element = " + element);
            if (element instanceof Param) {
                text += appendElementKey(level);
                // RECURSE on next line
                text += "\n" + convertToString((Param)element, level + 2);
                text += "\n" + getIndent(level + 2) + END + " " + ELEMENT_KEY;
            } else if (element instanceof String) {
                text += "\n" + getIndent(level + 1) + "//" + (String)element;
            } else {
                throw new IllegalArgumentException("Elements vector " +
                "can only contain Param or String, found " + element);
            }
        }
        text += "\n" + getIndent(level + 1) + END + " " + key;        
        return text;
    } else {
        throw new IllegalArgumentException("key: '" + key + "' contains unsupported type: "
            + parameter.getClass().getName());
    }
} catch(Exception ex) {
    GenLib.exception("ParamConverter.getParamText()",
        "Key '" + key + "', parameter '" + parameter + "' had a problem.", ex);

    return "#INCOMPLETE TEXT#";
}
} // End method

private static String appendElementKey(int level) {
    return "\n" + getIndent(level + 1) + ELEMENT_KEY + ":";
}
private static String getIndent(int level) {
    String indent = "";
    for (int i = 0; i < level; i++) {
        indent += "    "; // <---- 4 spaces
    }
    return indent;
}
//--- Std
private static void print(String text) {
    System.out.println("ParamConverter" + text);
}

} // End class
