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

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.startup;

import gecco.server.startup.*;

import java.io.*;
import java.util.*;

/**
 *  Description of the Class
 *
 * @author     Karl Hasselström
 * @created    June 27, 2001
 */
public class PropertyTree implements DeepCloneable {
    /**
     *  Description of the Field
     */
    private DeepCloneHashMap propertyMap;
    /**
     *  Description of the Field
     */
    private DeepCloneHashMap variableMap;
    /**
     *  Description of the Field
     */
    private LinkedList parents;


    /**
     *  Constructor for the PropertyTree object
     */
    public PropertyTree() {
        this(null);
    }


    /**
     *  Constructor for the PropertyTree object
     *
     * @param  parent  Description of Parameter
     */
    private PropertyTree(PropertyTree parent) {
        propertyMap = new DeepCloneHashMap();
        variableMap = new DeepCloneHashMap();
        parents = new DeepCloneLinkedList();
        if (parent != null) {
            parents.add(parent);
        }
    }


    /**
     *  Description of the Method
     *
     * @param  property  Description of Parameter
     * @return           Description of the Returned Value
     */
    public List get(String property) {

        // Fetch list of values
        DeepCloneLinkedList ll;
        try {
            ll = (DeepCloneLinkedList) propertyMap.get(property);
        } catch (ClassCastException e) {
            ll = null;
        }

        // If there were none, create an empty list
        if (ll == null) {
            ll = new DeepCloneLinkedList();
        }

        return ll;
    }


    /**
     *  Gets the Variable attribute of the PropertyTree object
     *
     * @param  variable  Description of Parameter
     * @return           The Variable value
     */
    public List getVariable(String variable) {

        // Fetch list of values and deep clone it
        List l;
        try {
            l = (List) ((DeepCloneable) variableMap.get(variable)).deepClone();
        } catch (ClassCastException e) {
            // This means that l was null
            l = null;
        }

        // If there were none, try parents
        if (l == null) {
            Iterator iter = parents.iterator();
            while (iter.hasNext()) {
                l = ((PropertyTree) iter.next()).getVariable(variable);
                if (l.size() == 0) {
                    l = null;
                }
            }
        }

        // If that failed too, create an empty list
        if (l == null) {
            l = new DeepCloneLinkedList();
        }

        return l;
    }


    /**
     *  Gets the Single attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The Single value
     * @exception  ParseErrorException  Description of Exception
     */
    public Object getSingle(String property) throws ParseErrorException {
        List l = get(property);

        // Make sure there is exactly one element
        if (l.size() > 1) {
            throw new ParseErrorException("Multiple definitions of " + property + "!");
        } else if (l.size() == 0) {
            throw new ParseErrorException(property + " not defined!");
        }

        // Return it
        return l.get(0);
    }


    /**
     *  Gets the SingleInt attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The SingleInt value
     * @exception  ParseErrorException  Description of Exception
     */
    public int getSingleInt(String property) throws ParseErrorException {
        try {
            return Integer.parseInt((String) getSingle(property));
        } catch (NumberFormatException e) {
            throw new ParseErrorException(property + " is not an integer!");
        } catch (ClassCastException e) {
            throw new ParseErrorException(property + " is not an integer!");
        }
    }


    /**
     *  Gets the SingleDouble attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The SingleDouble value
     * @exception  ParseErrorException  Description of Exception
     */
    public double getSingleDouble(String property) throws ParseErrorException {
        try {
            return Double.parseDouble((String) getSingle(property));
        } catch (NumberFormatException e) {
            throw new ParseErrorException(property + " is not a number!");
        } catch (ClassCastException e) {
            throw new ParseErrorException(property + " is not a number!");
        }
    }


    /**
     *  Gets the SingleString attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The SingleString value
     * @exception  ParseErrorException  Description of Exception
     */
    public String getSingleString(String property) throws ParseErrorException {
        try {
            return (String) getSingle(property);
        } catch (ClassCastException e) {
            throw new ParseErrorException(property + " is not a string!");
        }
    }


    /**
     *  Gets the SingleSubtree attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The SingleSubtree value
     * @exception  ParseErrorException  Description of Exception
     */
    public PropertyTree getSingleSubtree(String property) throws ParseErrorException {
        try {
            return (PropertyTree) getSingle(property);
        } catch (ClassCastException e) {
            throw new ParseErrorException(property + " is not a subtree!");
        }
    }


    /**
     *  Gets the IntegerList attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The IntegerList value
     * @exception  ParseErrorException  Description of Exception
     */
    public List getIntegerList(String property) throws ParseErrorException {
        List oldList = get(property);
        List newList = new DeepCloneLinkedList();

        // Cast the elements to Integers
        Iterator iter = oldList.iterator();
        while (iter.hasNext()) {
            try {
                Integer i = new Integer((String) iter.next());
                newList.add(i);
            } catch (NumberFormatException e) {
                throw new ParseErrorException(property + " is not an integer list!");
            } catch (ClassCastException e) {
                throw new ParseErrorException(property + " is not an integer list!");
            }
        }

        // Return the new list
        return newList;
    }


    /**
     *  Gets the DoubleList attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The DoubleList value
     * @exception  ParseErrorException  Description of Exception
     */
    public List getDoubleList(String property) throws ParseErrorException {
        List oldList = get(property);
        List newList = new DeepCloneLinkedList();

        // Cast the elements to Doubles
        Iterator iter = oldList.iterator();
        while (iter.hasNext()) {
            try {
                Double d = new Double((String) iter.next());
                newList.add(d);
            } catch (NumberFormatException e) {
                throw new ParseErrorException(property + " is not a number list!");
            } catch (ClassCastException e) {
                throw new ParseErrorException(property + " is not a number list!");
            }
        }

        // Return the new list
        return newList;
    }


    /**
     *  Gets the StringList attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The StringList value
     * @exception  ParseErrorException  Description of Exception
     */
    public List getStringList(String property) throws ParseErrorException {
        List oldList = get(property);
        List newList = new DeepCloneLinkedList();

        // Cast the elements to Strings
        Iterator iter = oldList.iterator();
        while (iter.hasNext()) {
            try {
                String s = (String) iter.next();
                newList.add(s);
            } catch (ClassCastException e) {
                throw new ParseErrorException(property + " is not a string list!");
            }
        }

        // Return the new list
        return newList;
    }


    /**
     *  Gets the SubtreeList attribute of the PropertyTree object
     *
     * @param  property                 Description of Parameter
     * @return                          The SubtreeList value
     * @exception  ParseErrorException  Description of Exception
     */
    public List getSubtreeList(String property) throws ParseErrorException {
        List oldList = get(property);
        List newList = new DeepCloneLinkedList();

        // Cast the elements to PropertyTrees
        Iterator iter = oldList.iterator();
        while (iter.hasNext()) {
            try {
                PropertyTree pt = (PropertyTree) iter.next();
                newList.add(pt);
            } catch (ClassCastException e) {
                throw new ParseErrorException(property + " is not a subtree list!");
            }
        }

        // Return the new list
        return newList;
    }


    /**
     *  Description of the Method
     *
     * @param  property  Description of Parameter
     * @param  value     Description of Parameter
     */
    public void put(String property, String value) {
        putObject(property, value);
    }


    /**
     *  Description of the Method
     *
     * @param  property  Description of Parameter
     * @param  subtree   Description of Parameter
     */
    public void put(String property, PropertyTree subtree) {
        putObject(property, subtree);
    }


    /**
     *  Description of the Method
     *
     * @param  property  Description of Parameter
     * @param  values    Description of Parameter
     */
    public void put(String property, List values) {
        Iterator iter = values.iterator();
        while (iter.hasNext()) {
            putObject(property, iter.next());
        }
    }


    /**
     *  Description of the Method
     *
     * @param  variable  Description of Parameter
     * @param  value     Description of Parameter
     */
    public void putVariable(String variable, String value) {
        putObjectVariable(variable, value);
    }


    /**
     *  Description of the Method
     *
     * @param  variable  Description of Parameter
     * @param  subtree   Description of Parameter
     */
    public void putVariable(String variable, PropertyTree subtree) {
        putObjectVariable(variable, subtree);
    }


    /**
     *  Description of the Method
     *
     * @param  variable  Description of Parameter
     * @param  values    Description of Parameter
     */
    public void putVariable(String variable, List values) {
        Iterator iter = values.iterator();
        while (iter.hasNext()) {
            putObjectVariable(variable, iter.next());
        }
    }


    /**
     *  Description of the Method
     *
     * @param  other  Description of Parameter
     */
    public void putAll(PropertyTree other) {

        // Put all properties
        Iterator iter = other.propertyMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry me = (Map.Entry) iter.next();
            String property = (String) me.getKey();
            List values = (List) me.getValue();
            put(property, values);
        }

        // Put all variables
        iter = other.variableMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry me = (Map.Entry) iter.next();
            String variable = (String) me.getKey();
            List values = (List) me.getValue();
            putVariable(variable, values);
        }
    }


    /**
     *  Description of the Method
     *
     * @param  in                        Description of Parameter
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     */
    public void parseStream(Reader in) throws IOException, SyntaxErrorException {
        StreamTokenizer tok = new StreamTokenizer(in);
        tok.resetSyntax();

        // Ignore linebreaks and whitespace.
        tok.whitespaceChars(0, 32);
        tok.eolIsSignificant(false);

        // Allow C/C++ style comments.
        tok.slashStarComments(true);
        tok.slashSlashComments(true);

        // # makes the rest of the line a comment.
        tok.commentChar('#');

        // Characters that form words.
        tok.wordChars('a', 'z');
        tok.wordChars('A', 'Z');
        tok.wordChars('0', '9');
        tok.wordChars('_', '_');
        tok.wordChars('.', '.');
        tok.wordChars('-', '-');

        // String quotes.
        tok.quoteChar('"');

        // Special characters.
        tok.ordinaryChar('{');
        tok.ordinaryChar('}');
        tok.ordinaryChar(',');
        tok.ordinaryChar(';');
        tok.ordinaryChar('=');
        tok.ordinaryChar('$');
        tok.ordinaryChar('+');

        // Start parsing
        parse(tok);
    }


    /**
     *  Description of the Method
     *
     * @return    Description of the Returned Value
     */
    public String toString() {
        StringWriter outString = new StringWriter();
        PrintWriter out = new PrintWriter(outString);
        out.println("{");
        Iterator iter = propertyMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry e = (Map.Entry) iter.next();
            out.print(e.getKey().toString() + " = ");
            out.println(e.getValue().toString() + ";");
        }
        iter = variableMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry e = (Map.Entry) iter.next();
            out.print("$" + e.getKey().toString() + " = ");
            out.println(e.getValue().toString() + ";");
        }
        out.println("}");
        return outString.toString();
    }


    // Make a deep clone
    /**
     *  Description of the Method
     *
     * @return    Description of the Returned Value
     */
    public DeepCloneable deepClone() {
        PropertyTree copy = new PropertyTree();
        copy.propertyMap = (DeepCloneHashMap) propertyMap.deepClone();
        copy.variableMap = (DeepCloneHashMap) variableMap.deepClone();
        copy.parents = new LinkedList(parents);
        return copy;
    }


    /**
     *  Description of the Method
     *
     * @param  property  Description of Parameter
     * @param  value     Description of Parameter
     */
    private void putObject(String property, Object value) {

        // Fetch list of previous values
        DeepCloneLinkedList ll;
        try {
            ll = (DeepCloneLinkedList) propertyMap.get(property);
        } catch (ClassCastException e) {
            ll = null;
        }

        // If there were none, create a new list and add it
        if (ll == null) {
            ll = new DeepCloneLinkedList();
            propertyMap.put(property, ll);
        }

        // Add the value to the list
        ll.add(value);
    }


    /**
     *  Description of the Method
     *
     * @param  variable  Description of Parameter
     * @param  value     Description of Parameter
     */
    private void putObjectVariable(String variable, Object value) {

        // Fetch list of previous values
        DeepCloneLinkedList ll;
        try {
            ll = (DeepCloneLinkedList) variableMap.get(variable);
        } catch (ClassCastException e) {
            ll = null;
        }

        // If there were none, create a new list and add it
        if (ll == null) {
            ll = new DeepCloneLinkedList();
            variableMap.put(variable, ll);
        }

        // Add the value to the list
        ll.add(value);
    }


    /**
     *  Description of the Method
     *
     * @param  tok                       Description of Parameter
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     */
    private void parse(StreamTokenizer tok)
             throws IOException, SyntaxErrorException {

        // Parse properties.
        tok.nextToken();
        while (tok.ttype != tok.TT_EOF && tok.ttype != '}') {
            tok.pushBack();
            parseProperty(tok);
            tok.nextToken();
        }

        // Make sure the first unused token is not used up.
        tok.pushBack();
    }


    /**
     *  Description of the Method
     *
     * @param  tok                       Description of Parameter
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     */
    private void parseProperty(StreamTokenizer tok)
             throws IOException, SyntaxErrorException {

        // Is this a variable?
        boolean isVariable = false;
        tok.nextToken();
        if (tok.ttype == '$') {
            isVariable = true;
        } else {
            tok.pushBack();
        }

        // First comes the name
        tok.nextToken();
        if (tok.ttype != tok.TT_WORD && tok.ttype != '"') {
            throw new SyntaxErrorException("Name expected!", tok.lineno());
        }
        String name = tok.sval;

        // Second comes an equals sign
        tok.nextToken();
        if (tok.ttype != '=') {
            throw new SyntaxErrorException("= expected!", tok.lineno());
        }

        // Third comes the value
        DeepCloneLinkedList ll = new DeepCloneLinkedList();
        ll.add(parseValue(tok));
        tok.nextToken();
        while (tok.ttype == ',') {
            ll.add(parseValue(tok));
            tok.nextToken();
        }
        tok.pushBack();

        // Fourth comes a semicolon
        tok.nextToken();
        if (tok.ttype != ';') {
            throw new SyntaxErrorException("; expected!", tok.lineno());
        }

        // Put the property or variable
        if (isVariable) {
            putVariable(name, ll);
        } else {
            put(name, ll);
        }
    }


    /**
     *  Description of the Method
     *
     * @param  tok                       Description of Parameter
     * @return                           Description of the Returned Value
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     */
    private Object parseValue(StreamTokenizer tok)
             throws IOException, SyntaxErrorException {
        Object first = parseSingleValue(tok);
        tok.nextToken();
        while (tok.ttype == '+') {
            // There should be a tree before the +
            PropertyTree pt;
            try {
                pt = (PropertyTree) first;
            } catch (ClassCastException e) {
                throw new SyntaxErrorException("Tree expected before +!", tok.lineno());
            }

            // And a tree after the +
            PropertyTree next;
            try {
                next = (PropertyTree) parseSingleValue(tok);
            } catch (ClassCastException e) {
                throw new SyntaxErrorException("Tree expected after +!", tok.lineno());
            }

            // Merge the trees
            pt.putAll(next);
            pt.parents.addAll(next.parents);

            // Read next token
            tok.nextToken();
        }

        // Push back the first unused token
        tok.pushBack();

        // Return the (possibly compound) object
        return first;
    }


    /**
     *  Description of the Method
     *
     * @param  tok                       Description of Parameter
     * @return                           Description of the Returned Value
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     */
    private Object parseSingleValue(StreamTokenizer tok)
             throws IOException, SyntaxErrorException {
        Object value;
        tok.nextToken();
        if (tok.ttype == tok.TT_WORD || tok.ttype == '"') {
            // We have a string value!
            value = tok.sval;
        } else if (tok.ttype == '{') {
            // We have a subtree!
            PropertyTree subtree = new PropertyTree(this);
            subtree.parse(tok);
            value = subtree;

            // Subtrees are closed with a curly brace
            tok.nextToken();
            if (tok.ttype != '}') {
                throw new SyntaxErrorException("} expected!", tok.lineno());
            }
        } else if (tok.ttype == '$') {
            // We have a variable!
            tok.nextToken();
            if (tok.ttype == tok.TT_WORD || tok.ttype == '"') {
                List variable = getVariable(tok.sval);
                if (variable.size() > 1) {
                    // It's a list
                    value = variable;
                } else if (variable.size() == 1) {
                    // It's a single value
                    value = variable.get(0);
                } else {
                    // It's not defined
                    throw new SyntaxErrorException("Variable '" + tok.sval +
                            "' is not defined!", tok.lineno());
                }
            } else {
                throw new SyntaxErrorException("Variable name expected!", tok.lineno());
            }
        } else {
            throw new SyntaxErrorException("Value expected!", tok.lineno());
        }
        return value;
    }
}
