/// Evolver Copyright 2000, Morgan McGuire
/// matrix@alum.mit.edu   http://graphics3d.com/matrix
import java.io.*;
import java.lang.reflect.*;
import java.util.Vector;
import java.util.HashMap;

/**
 * Contains parameters affecting mating and simulation.  These can be
 * edited using an associated GUI panel.  This class also contains
 * helper functions for generating new expressions and subexpressions.
 */
public class GeneticParameters extends ClassLoader
{
    // Mutations:
    public static final int CHANGE_OPERATOR_MUTATION = 0;
    public static final int ENCLOSE_IN_NEW_CALL_MUTATION = 1;
    public static final int NEW_EXPRESSION_MUTATION = 2;
    public static final int SIMPLIFY_MUTATION = 3;
    public static final int CLONE_SIBLING_MUTATION = 4;
    public static final int SCALAR_CONSTANT_MUTATION = 5;
    public static final int VECTOR_CONSTANT_MUTATION = 6;

    static final int NUM_MUTATIONS = VECTOR_CONSTANT_MUTATION + 1;
  
    /**
     * Descriptions and details about each kind of mutation.  The order
     * of the values in this array must match the mutation constants.  
     */
    static MutationDescription mutationDescription[];

    /**
     * Maximum change in a scalar due to mutation.
     */
    double scalarDisturbance = 0.2;

    /**
     * Maximum change in a vector's individual components due to
     * mutation.  
     */
    double vectorDisturbance = 0.2;

    /**
     * Probability of a mutatation before accounting for parental
     * complexity.  
     */
    double baseMutationProbability = 0.01;

    /**
       Number of behaviors in a single generation.
     */
    int generationSize = 30;

    /**
       How many of the parents will be allowed to breed.
     */
    int numBreeders = 8;

    /**
       Minimum quality of match acceptable for matching a texture for
       an individual segment.
    */
    double minimumSegmentQuality = 0.95;

    OperatorDescription operatorDescription[];

    /**
     * A hash table mapping operator names (as strings) to ELOperators
     * to allow fast parsing of expressions.
     */
    HashMap operatorsByName;
  
    /**
   * The probability that any mating will be crossover (vs a tree
   * walk).  The probability of a tree walk is 1 -
   * crossoverProbability.
   */
    double crossoverProbability = 0.5;

    int numSpatialSegmentationBins = 4;

    double scalarFrequency = 0.3;
    double vectorFrequency = 0.3;
    double callFrequency = 0.6;

    double callProbability, scalarProbability, vectorProbability;

    /**
     * @param operatorPath a path that can be searched for .class files
     * implementing ELOperator.
     */
    public GeneticParameters(String operatorPath)
    {
        createMutationDescriptions();
        loadOperators(operatorPath);
        loadSettings("default.gp");
        computeProbabilities();
    }


    /**
   * Computes the normalized mutation and operator probabilities.
   * Call this whenever frequencies change.
   */
    void computeProbabilities()
    {
        double sum = 0.0;

        for(int m = 0; m < mutationDescription.length; m++) {
            sum += mutationDescription[m].frequency;
        }

        for(int m = 0; m < mutationDescription.length; m++) {
            mutationDescription[m].probability = mutationDescription[m].frequency / sum;
        }

        sum = 0.0;

        for(int o = 0; o < operatorDescription.length; o++) {
            if (operatorDescription[o].enabled) {
                sum += operatorDescription[o].frequency;
            }
        }

        for(int o = 0; o < operatorDescription.length; o++) {
            if (operatorDescription[o].enabled) {
                operatorDescription[o].probability = operatorDescription[o].frequency / sum;
            }
        }

        sum = scalarFrequency + vectorFrequency + callFrequency;
    
        callProbability = callFrequency / sum;
        vectorProbability = vectorFrequency / sum;
        scalarProbability = scalarFrequency / sum;
    }

    /**
   * Sets all probabilities from a file.  The file has multiple clauses of the form:
   *
   * operator <operator> { <name> = <value> }
   *
   * where name = "frequency" or "enabled" and value is a number
   * between 0 and 1 or true or false.  
   */
  // The helper read functions all assume that they are to read the
  // current token, then advance the tokenizer.
    private void loadSettings(String file)
    {
        System.out.print("Loading settings (" + file + ")...");
        StreamTokenizer tokenizer = null;
    
        try {
            tokenizer = new StreamTokenizer(new BufferedReader(new FileReader(file)));

            // configure the parser
            tokenizer.parseNumbers();
            tokenizer.slashSlashComments(true);
            tokenizer.slashStarComments(true);
            tokenizer.lowerCaseMode(false);
            tokenizer.eolIsSignificant(false);

            tokenizer.nextToken();      
            while (tokenizer.ttype != StreamTokenizer.TT_EOF) {
        
                String identifier = readIdentifier(tokenizer);

                if (identifier.equals("operator")) {

                    String operatorName = readIdentifier(tokenizer);

                    readSymbol(tokenizer, '{');
          
                    double frequency = 0.5;
                    boolean enabled = false;
                    while (tokenizer.ttype != '}') {
                        identifier = readIdentifier(tokenizer);
                        readSymbol(tokenizer, '=');
            
                        if (identifier.equals("frequency")) {
                            frequency = readProbability(tokenizer);
                        } else if (identifier.equals("enabled")) {
                            enabled = readBoolean(tokenizer);
                        } else {
                            throw new ParseException("Unexpected identifier: '" + identifier + "'");
                        }
                    }
          
                    // consume the closing brace
                    tokenizer.nextToken();

                    // Write to operator description.  Ignore operators we
                    // couldn't find.  There are a lot of operators, so run this
                    // loop backwards for a bit more speed.

                    for(int od = operatorDescription.length - 1; od >= 0; od--) {
                        if (operatorDescription[od].name.equals(operatorName)) {
                            operatorDescription[od].frequency = frequency;
                            operatorDescription[od].enabled = enabled;
                            break;
                        }
                    } // for

                } else if (identifier.equals("scalarDisturbance")) {
                    readSymbol(tokenizer, '=');
                    scalarDisturbance = readNumber(tokenizer);
                } else if (identifier.equals("vectorDisturbance")) {
                    readSymbol(tokenizer, '=');
                    vectorDisturbance = readNumber(tokenizer);
                } else if (identifier.equals("mutationProbability")) {
                    readSymbol(tokenizer, '=');
                    baseMutationProbability = readProbability(tokenizer);
                } else if (identifier.equals("crossoverProbability")) {
                    readSymbol(tokenizer, '=');
                    crossoverProbability = readProbability(tokenizer);
                } else if (identifier.equals("numSpatialSegmentationBins")) {
                    readSymbol(tokenizer, '=');
                    numSpatialSegmentationBins = (int)readNumber(tokenizer);
                } else if (identifier.equals("minimumSegmentMatchQuality")) {
                    readSymbol(tokenizer, '=');
                    minimumSegmentQuality = readNumber(tokenizer);
                } else if (identifier.equals("scalarFrequency")) {
                    readSymbol(tokenizer, '=');
                    scalarFrequency = readProbability(tokenizer);
                } else if (identifier.equals("numBreeders")) {
                    readSymbol(tokenizer, '=');
                    numBreeders = (int)readNumber(tokenizer);
                } else if (identifier.equals("generationSize")) {
                    readSymbol(tokenizer, '=');
                    generationSize = (int)readNumber(tokenizer);
                } else if (identifier.equals("vectorFrequency")) {
                    readSymbol(tokenizer, '=');
                    vectorFrequency = readProbability(tokenizer);
                } else if (identifier.equals("callFrequency")) {
                    readSymbol(tokenizer, '=');
                    callFrequency = readProbability(tokenizer);
                } else if (identifier.endsWith("Frequency")) {

                    boolean found = false;
                    readSymbol(tokenizer, '=');

                    // try to find a corresponding mutation probability
                    for(int m = 0; m < mutationDescription.length; m++) {
                        if (identifier.equals(mutationDescription[m].name + "Frequency")) {
                            mutationDescription[m].frequency = readProbability(tokenizer);
                            found = true;
                            break;
                        }
                    }
          
                    if (! found) {
                        throw new ParseException("Unknown frequency '" + identifier + "'");
                    }

                } else {
                    throw new ParseException("Unknown identifier '" + identifier + "'");
                }

            } // while
     
        } catch (FileNotFoundException e) {
            System.err.println();
            System.err.println();
            System.err.println("Warning: " + file + " not found.");
            System.err.println();
        } catch (IOException e) {
            System.err.println();
            System.err.println();
            System.out.println(e);
            System.err.println();
        } catch (ParseException e) {
            System.err.println();
            System.err.println();
            System.err.println("*** Error on line " + tokenizer.lineno() + " of " + file + ": " + e + " ***");
            System.err.println();
        }
        System.out.println("done");
    }
  
    private double readProbability(StreamTokenizer tokenizer) throws ParseException, IOException
    {
        return Math.min(Math.max(0.0, readNumber(tokenizer)), 1.0);
    }

    private double readNumber(StreamTokenizer tokenizer) throws ParseException, IOException
    { 
        if (tokenizer.ttype != StreamTokenizer.TT_NUMBER) {
            throw new ParseException("Number expected.");
        } else {
            double n = tokenizer.nval;
            tokenizer.nextToken();
            return n;
        }   
    }

    private boolean readBoolean(StreamTokenizer tokenizer) throws ParseException, IOException
    { 
        if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
            throw new ParseException("Boolean expected.");
        } else {
            boolean b = true;

            if (tokenizer.sval.equals("true")) {
                b = true;
            } else if (tokenizer.sval.equals("false")) {
                b = false;
            } else {
                throw new ParseException("Boolean expected, '" + tokenizer.sval + "' encountered.");
            }

            tokenizer.nextToken();
            return b;
        }   
    }

    private void readKeyWord(StreamTokenizer tokenizer, String expectedValue) throws ParseException, IOException
    {
        String value = readIdentifier(tokenizer);
    
        if (! value.equals(expectedValue)) {
            throw new ParseException("Expected '" + expectedValue + "' and found '" +
                                     value + "'");

        }
    }

    private String readIdentifier(StreamTokenizer tokenizer) throws ParseException, IOException
    {
        if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
            throw new ParseException("Identifier expected.  Token type = " + tokenizer.ttype + " Token value = '" +
                                     tokenizer.sval + "' or '" + tokenizer.nval + "' or '" + 
                                     new Character((char)(tokenizer.ttype)) + "'");
        } else {
            String s = tokenizer.sval;
            tokenizer.nextToken();
            return s;
        }   
    }

    private void readSymbol(StreamTokenizer tokenizer, char symbol) throws ParseException, IOException
    {
        if (tokenizer.ttype != symbol) {
            throw new ParseException("'" + new Character(symbol) + 
                                     "' symbol expected.  Token type = " + tokenizer.ttype + " Token value = '" +
                                     tokenizer.sval + "' or '" + tokenizer.nval + "' or '" + 
                                     new Character((char)(tokenizer.ttype)) + "'");
        } else {
            tokenizer.nextToken();
        }   
    }


    private void loadOperators(String operatorPath)
    {
        //System.out.print("Finding operators... [ ");

        // Generate a list of all class files
        String classFile[] = 
            new File(operatorPath).list(new FilenameFilter() {
                    public boolean accept(File dir, String name) {
                        return name.endsWith(".class");
                    }});
    
        // This will be our result array
        Class operators[] = new Class[classFile.length];
        int numOperators = 0;

	// Generate each class name
        for (int c = 0; c < classFile.length; c++) {
            String name = classFile[c].substring(0, classFile[c].length() - 6);

            // Ignore names with a $ in them; they are inner classes.
            if (name.indexOf('$') == -1) {
                try {
                    // Load the class and see if it implements ELOperator
                    Class cls = findClass(name);
                    int modifiers = cls.getModifiers();
          
                    // don't load abstract or interface classes
                    if (! Modifier.isInterface(modifiers) && ! Modifier.isAbstract(modifiers)) {
            
                        Class interfaces[] = cls.getInterfaces();
                          
                        // Check every interface
                        for(int i = 0; i < interfaces.length; i++) {
              
                            // Try for a string match on the name
                            if (interfaces[i].getName().equals("ELOperator")) {
                                // It is a match!  Record this and exit the interface loop abruptly.
                                operators[numOperators] = cls;
                                numOperators++;
                                //System.out.print(cls.getName() + " ");
                                break;
                            }
                        }
                    }
                } catch (ClassNotFoundException e) {
                    System.err.println(e);
                }
            }
        }

        //System.out.println("] done");

        System.out.print("Loading operators... [ ");

        createOperatorDescriptions(operators, numOperators);

        System.out.println("] done");

    }

    /**
   * Creates mutation descriptions
   */
    private void createMutationDescriptions()
    {
        mutationDescription = new MutationDescription[NUM_MUTATIONS];

        mutationDescription[CHANGE_OPERATOR_MUTATION] = 
            new MutationDescription("changeOperator", "Change Operator", 0.5);

        mutationDescription[ENCLOSE_IN_NEW_CALL_MUTATION] =
            new MutationDescription("encloseInNewCall", "Enclose in new Call", 0.5);

        mutationDescription[NEW_EXPRESSION_MUTATION] =
            new MutationDescription("newExpression", "New Expression", 0.5);

        mutationDescription[SIMPLIFY_MUTATION] = 
            new MutationDescription("simplify", "Simplify", 0.5);

        mutationDescription[CLONE_SIBLING_MUTATION] = 
            new MutationDescription("cloneSibling", "Clone Sibling", 0.5);

        mutationDescription[SCALAR_CONSTANT_MUTATION] = 
            new MutationDescription("scalarValueMutation", "Scalar Mutation", 0.5);

        mutationDescription[VECTOR_CONSTANT_MUTATION] = 
            new MutationDescription("vectorValueMutation", "Vector Mutation", 0.5);
    }

    /**
   * Creates descriptions for the operators, building the operatorDescription and
   * operatorsByName fields.
   */
    private void createOperatorDescriptions(Class operator[], int numOperators) 
    {
        operatorDescription = new OperatorDescription[numOperators];
        operatorsByName = new HashMap();
        
        for (int o = 0; o < numOperators; o++) {
            System.out.print(operator[o].getName() + " ");
            operatorDescription[o] = new OperatorDescription();
            operatorDescription[o].operator = newOperatorInstance(operator[o]);
            operatorDescription[o].frequency = 0.0;
            operatorDescription[o].enabled = false;
            operatorDescription[o].name = operator[o].getName();
            operatorsByName.put(operatorDescription[o].name, operatorDescription[o].operator);
            //System.out.println(": " + operatorDescription[o].operator.description());
        }
    }

    /**
   * Instantiates an operator subclass.
   */
    private ELOperator newOperatorInstance(Class operatorClass)
    {
        ELOperator operator = null;
    
        try {
            operator = (ELOperator) operatorClass.newInstance();
        } catch (ExceptionInInitializerError e) {

            // An exception occurred inside the constructor
            System.err.println();
            System.err.println(e.getException());
            throw new Error("Cannot instantiate " + operatorClass);

        } catch (Exception e) {
            System.err.println("" + e);
            throw new Error("Cannot instantiate " + operatorClass);
        }
    
        return operator;
    }

    /**
   * Generates a random operator based the normalized operator
   * probabilities.
   */
    public ELOperator randomOperator()
    {
        // Generates a random number on the range (0,1]
        double r = 1.0 - Math.random();
        double accum = 0.0;

        for(int o = 0; o < operatorDescription.length; o++) {
            if (operatorDescription[o].enabled) {
                accum += operatorDescription[o].probability;
                if (accum >= r) {
                    return operatorDescription[o].operator;
                }
            }
        }

        throw new Error("Internal error while choosing a random operator.");
    }

    public ELExpression randomCollage() {
        return randomCall((ELOperator)operatorsByName.get("Collage"));
    }

    /**
     * Generates a new random expression of any type.
     */
    public ELExpression randomExpression() {
        double r = 1.0 - Math.random();
        double accum = 0.0;
    
        accum += callProbability;
        if (accum >= r) {
            return randomCall();
        } 
    
        accum += vectorProbability;
        if (accum >= r) {
            return new ELVector(Math.random() * 2 - 1.0, Math.random() * 2 - 1.0, Math.random() * 2 - 1.0);
        } 

        accum += scalarProbability;
        if (accum >= r) {
            return new ELScalar(Math.random() * 2 - 1.0);
        } 

        throw new Error("accum = " + accum + " r = " + r);
    }

    /**
       Returns a random call with a random operator.
     */
    public ELCall randomCall() {
        return randomCall(randomOperator());
    }

    /**
       Returns a random call that starts with this operator.
     */
    public ELCall randomCall(ELOperator operator) {
        int min = operator.getMinNumArguments();
    
        ELExpression subExpression[] = new ELExpression[min];
        int i = 0;
        while (i < min) {
            subExpression[i] = randomExpression();
            i++;
        }
    
        return new ELCall(operator, subExpression);
    }

    /**
   * Generates a random call of at least one argument, using
   * expression as the first argument.
   */
    public ELCall randomCallWithOperator(ELExpression expression)
    {
        ELOperator operator = randomOperator();

        while (operator.getMinNumArguments() < 1) {
            operator = randomOperator();
        }

        int min = operator.getMinNumArguments();
    
        ELExpression subExpression[] = new ELExpression[min];
        int i = 0;
        subExpression[i] = expression;

        i++;
        while (i < min) {
            subExpression[i] = randomExpression();
            i++;
        }
    
        return new ELCall(operator, subExpression);
    }

    /**
   * Genetically mates two expressions and produces a single
   * offspring.  The type of reproduction that occurs is determined
   * probabilistically by the crossoverProbability.
   *
   * For a crossover mating, one parent is cloned, then a randomly
   * chosen subexpression from the other parent is cloned and
   * substituted at a random location.
   */
    public ELExpression mate(ELExpression a, ELExpression b) 
    {
        if (Math.random() < crossoverProbability) {
            // Crossover
            return crossoverMate(a, b);
        } else {
            // Tree walk
            return walkMate(a, b);
        }
    }

    /**
   * Always returns a clone.
   */
    private ELExpression crossoverMate(ELExpression a, ELExpression b) 
    {

        // Choose a random parent
        ELExpression receivingParent;
        ELExpression donorParent;
    
        if (Math.random() < 0.5) {
            receivingParent = a;
            donorParent = b;
        } else {
            receivingParent = b;
            donorParent = a;
        }
    
        ELExpression child = receivingParent.geneticClone(null, this);
        ELExpression crossExpression = donorParent.randomNestedSubexpression();
    
        // Insert into child
        int position = (int)Math.floor(Math.random() * receivingParent.numNestedSubexpressions());

        return child.cloneWithReplacement(crossExpression, position);
    }

    /**
   * Always returns a clone
   */
    private ELExpression walkMate(ELExpression a, ELExpression b)
    {
        if ((a instanceof ELCall) && (b instanceof ELCall) && 
            (((ELCall) a).operator == ((ELCall) b).operator)) {
      
            ELCall ac = (ELCall) a;
            ELCall bc = (ELCall) b;

            // Choose a number of arguments between the length of a.args and
            // b.args.  When they are the same length, this will always
            // compute that length.
            double r = Math.random();
            int numArgs = (int)Math.round((ac.child.length * r) + (bc.child.length * (1.0 - r)));
            ELExpression args[] = new ELExpression[numArgs];

            for (int i = 0; i < numArgs; i++) {
                if (i >= ac.child.length) {
                    args[i] = bc.child[i];
                } else if (i >= bc.child.length) {
                    args[i] = ac.child[i];
                } else {
                    // Recursively mate the subexpressions.  If they are the
                    // same, this will return the identical value.  The arguments 
                    // will get cloned by walkmate.
                    args[i] = walkMate(ac.child[i], bc.child[i]);
                }
            }

            // Clone the result to allow mutations to occur at any point.
            return new ELCall(ac.operator, args).geneticClone(null, this);

        } else {
            if (Math.random() < 0.5) {
                return a.geneticClone(null, this);
            } else {
                return b.geneticClone(null, this);
            }
        }
    }

    /**
   * Creates a new expression containing ELMorphInterpolators that
   * will interpolate between two expressions.  The two expressions
   * are completely cloned (excepting operators).
   */
    public ELExpression createGeneticMorphExpression(ELExpression a, ELExpression b)
    {
        if ((a instanceof ELCall) && (b instanceof ELCall) && 
            (((ELCall) a).operator == ((ELCall) b).operator) &&
            (((ELCall) a).numSubexpressions() == ((ELCall) b).numSubexpressions())) {
      
            ELCall aCall = (ELCall) a;
            ELCall bCall = (ELCall) b;

            // Create new argument expressions
            ELExpression subexpression[] = new ELExpression[aCall.numSubexpressions()];
      
            for(int e = subexpression.length - 1; e >= 0; e--) {
                subexpression[e] = createGeneticMorphExpression(aCall.child[e], bCall.child[e]);
            }

            // Return the new call.
            return new ELCall(aCall.operator, subexpression);

        } else {
            // These are not the same call expression...
            // If these are the same expression, return that expression, 
            // otherwise return an interpolator.  Note that either way, '
            // the recursion stops here.
            if (a.sameExpression(b)) {
                return (ELExpression) a.clone();
            } else {
                return new ELMorphInterpolator((ELExpression)a.clone(), (ELExpression)b.clone());
            }
        }
    }

    /**
   * Randomly chooses whether a mutation will occur based on the
   * length of the parent expression and the base mutation probability
   * and returns true if a mutation will occur. This is used by
   * ELExpression implementations in the geneticClone() method.
   *
   * @param parent The <B>parent</B> of the expression that may be
   * mutated.  The probability of mutation is not directly depenent on
   * the expression itself.  */
    public boolean mutationOccurs(ELCall parent)
    {
        double mutationProbability;

        if (parent == null) {
            mutationProbability = baseMutationProbability;
        } else {
            mutationProbability = baseMutationProbability / Math.sqrt(parent.numNestedSubexpressions());
        }

        return (Math.random() <= mutationProbability);
    }

    /**
   * Chooses a random type of expression mutation based on the
   * probabilities provided.  The return value is one of the _MUTATION
   * constants this class defines.  
   */
    public int randomMutationType()
    {
        // random threshold cumulative probability must exceed
        double n = Math.random();

        // index of mutation
        int m = 0;

        // cumulative probability
        double p = mutationDescription[m].probability;;

        int length = mutationDescription.length;

        // Find the mutation assigned to this probability block.
        while (n > p) {
      
            if ((m >= length) || (p > 1.0)) {
                throw new Error("Internal error while choosing random mutation type (m = " + 
                                m + ", p = " + p + ", n = " + n + ")");
            }

            p += mutationDescription[m].probability;
            m++;
        }

        return m;
    }
  
    // inherited from ClassLoader
    protected Class findClass(String name) throws ClassNotFoundException
    {
        Class c = findSystemClass(name);

        if (c != null) {
            return c;
        } else {
            throw new ClassNotFoundException();
        }
    }

    public String unparseELExpression(ELExpression e) {
        return e.unparse();
    }

    /**
     * Parses a string of EL code.  This is used for unserializing
     * ELExpressions.  Will return the scalar 0 if anything goes wrong
     * while parsing.
     */
    // DESIGN NOTE: This method is here because it needs access to the
    // operator list.
    public ELExpression parseELExpression(String exprString)
    {
        return parseELExpression(exprString, true);
    }

    /**
     * If supressErrors is true, parse errors are printed to System.err rather
     * than thrown as exceptions.
     */
    public ELExpression parseELExpression(String exprString, boolean suppressErrors)
    {
        StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(exprString));
        // configure the parser
        tokenizer.parseNumbers();
        tokenizer.slashSlashComments(true);
        tokenizer.slashStarComments(true);
        tokenizer.lowerCaseMode(false);
        tokenizer.eolIsSignificant(false);

        try {
            tokenizer.nextToken();
            return readExpression(tokenizer);
        } catch (IOException e) {
            System.err.println("While parsing '" + exprString + "':");
            System.err.println(e);
            return null;//new ELScalar(0);
        } catch (ParseException e) {
            if (suppressErrors) {
                System.err.println("While parsing '" + exprString + "':");
                System.err.println(e);
                return null;//new ELScalar(0);
            } else {
                throw new ParseError(e.toString());
            }
        }
    }

    /**
   * Reads an ELExpression from the tokenizer and returns it, having consumed all tokens in the expression.
   */
    private ELExpression readExpression(StreamTokenizer tokenizer) throws ParseException, IOException
    {
        // Look at the next token.
        if (tokenizer.ttype == '{') {
            // Vector constant
            // Consume the open '{'
            tokenizer.nextToken();
            double r = readNumber(tokenizer);
            readSymbol(tokenizer, ',');
            double g = readNumber(tokenizer);
            readSymbol(tokenizer, ',');
            double b = readNumber(tokenizer);
            if (tokenizer.ttype != '}') {
                throw new ParseException("'}' expected at end of vector constant.");
            }
            tokenizer.nextToken();

            return new ELVector(r, g, b);
        } else if (tokenizer.ttype == '[') {
            // Parameter
            // Consume the open '['
            tokenizer.nextToken();
            // Read the operator name
            String parameterName = tokenizer.sval;
            tokenizer.nextToken();

            if (tokenizer.ttype != ']') {
                throw new ParseException("']' expected at end of parameter.");
            }
            // Consume the close ']'
            tokenizer.nextToken();

            return new ELParameter(parameterName);

        } else if (tokenizer.ttype == StreamTokenizer.TT_NUMBER) {
            // Scalar number
            return new ELScalar(readNumber(tokenizer));

        } else if (tokenizer.ttype == StreamTokenizer.TT_WORD) {
            // Call
      
	    // Read the operator name
            String operatorName = tokenizer.sval;
            tokenizer.nextToken();

	    // Look up the operator
            ELOperator operator = (ELOperator)operatorsByName.get(operatorName);
      
            if (operator == null) {
                throw new ParseException("Unrecognized operator: " + operatorName);
            }
      
            Vector subExpressions = new Vector();

	    // Consume the open paren
            readSymbol(tokenizer, '(');
            while (tokenizer.ttype != ')') {
                subExpressions.addElement(readExpression(tokenizer));

                // verify that a comma is next
                if (tokenizer.ttype == ',') {
                    // consume the comma
                    readSymbol(tokenizer, ',');
                } else if (tokenizer.ttype != ')') {
                    throw new ParseException("Expected )");
                }
            }

            // Consume the close paren
            readSymbol(tokenizer, ')');

            // Copy the subexpressions into a strongly typed array.
            ELExpression subExprELExpressions[] = new ELExpression[subExpressions.size()];

            for(int e = 0; e < subExprELExpressions.length; e++) {
                subExprELExpressions[e] = (ELExpression)subExpressions.get(e);
            }
      
            // Construct the call
            return new ELCall(operator, subExprELExpressions);
      
        } else if (tokenizer.ttype == StreamTokenizer.TT_EOF) {
            throw new ParseException("Can't parse expression; ran out of tokens");
        } else {
            throw new ParseException("Can't parse expression; unexpected " +
                                     "token detected.  Token type = " + tokenizer.ttype + " Token value = '" +
                                     tokenizer.sval + "' or '" + tokenizer.nval + "' or '" + 
                                     new Character((char)(tokenizer.ttype)) + "'");
        }
    }
}


class MutationDescription
{
    String name;
    String longDescription;
    double frequency;
    double probability;

    public MutationDescription(String name,
                               String longDescription,
                               double frequency)
    {
        this.name = name;
        this.longDescription = longDescription;
        this.frequency = frequency;
    }

}

class ParseException extends Exception
{
    public ParseException(String s) 
    {
        super(s);
    }
}
