/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.royale.linter.config;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.royale.compiler.exceptions.ConfigurationException;
import org.apache.royale.compiler.internal.config.IConfigurationFilter;
import org.apache.royale.compiler.internal.config.annotations.ArgumentNameGenerator;
import org.apache.royale.compiler.internal.config.annotations.Arguments;
import org.apache.royale.compiler.internal.config.annotations.Config;
import org.apache.royale.compiler.internal.config.annotations.RoyaleOnly;
import org.apache.royale.compiler.internal.config.annotations.InfiniteArguments;
import org.apache.royale.compiler.internal.config.annotations.Mapping;
import org.apache.royale.compiler.internal.config.annotations.SoftPrerequisites;
import org.apache.royale.compiler.problems.ConfigurationProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.utils.Trace;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;

/**
 * The basic idea here is to let you keep all your configuration knowledge in
 * your configuration object, and to automate as much as possible. Reflection is
 * used to convert public fields and setters on your configuration object into
 * settable vars. There are a few key concepts:
 * <p>
 * - You should be able to configure absolutely any object.<br>
 * - Child configuration variables in your config become a dotted hierarchy of
 * varnames<br>
 * - All sources of configuration data are buffered and merged (as string
 * var/vals) before committing to the final configuration. This class acts as
 * the buffer.<br>
 * - Hyphenated variables (i.e. "some-var") are automatically configured by
 * calling your matching setter (i.e. setSomeVar)<br>
 * - Implementing an getSomeVarInfo() method on your class lets you set up more
 * complicated config objects<br>
 * - You can make variables depend on other variables having been set first.
 * This lets you set a root directory in one var and then use its value in
 * another.<br>
 * - Per-variable validation can be performed in setters. Overall validation
 * should take place as a post-process step.<br>
 * - You can keep ConfigurationBuffers around and merge multiple buffers
 * together before committing. Most recent definitions always win.<br>
 * <p>
 * The contract with your configuration class:
 * <p>
 * - You must provide a method with the signature
 * "void setYourVar(ConfigurationValue val)" to set your config var. Your setter
 * method should accept either a single arg of type List or String[], or else an
 * arglist of simple types. For example
 * "void myvar(int a, boolean b, String c")".<br>
 * - You can implement a function with the signature "int yourvar_argcount()" to
 * require a different number of arguments. This limit will be enforced by
 * configurators (command line, file, etc.)<br>
 * - If you provide a setter and explicit parameters (i.e. not List or String[])
 * the number of arguments will be automatically determined.<br>
 * - Each argument to your configuration variable is assumed to have a
 * (potentially non-unique) name. The default is the simple type of the argument
 * (boolean, int, string). If the var takes an undetermined number of args via
 * List or String[], the argname defaults to string.<br>
 * - You can implement a function with the signature
 * "String yourvar_argnames(int)" to provide names for each of the parameters.
 * The integer passed in is the argument number. Return the same name (i.e.
 * "item") for infinite lists.<br>
 * - You can implement a function with the signature "String[] yourvar_deps()"
 * to provide a list of other prerequisites for this var. You will be guaranteed
 * that the deps are committed before your var, or else a configurationexception
 * will be thrown if a prerequsite was unset. (Note that infinite cycles are not
 * checked, so be careful.)<br>
 */
public final class ConfigurationBuffer
{
    public ConfigurationBuffer(Class<? extends Configuration> configClass)
    {
        this(configClass, new HashMap<String, String>());
    }

    public ConfigurationBuffer(Class<? extends Configuration> configClass, Map<String, String> aliases)
    {
        this(configClass, aliases, null);
    }

    /**
     * Create a configuration buffer with an optional filter. The filter can be
     * used to remove unwanted options from a super class.
     * 
     * @param filter if null there is no filter, otherwise the set of
     * configuration options is filtered.
     */
    public ConfigurationBuffer(Class<? extends Configuration> configClass, Map<String, String> aliases, IConfigurationFilter filter)
    {
        this.configClass = configClass;
        this.varMap = new HashMap<String, List<ConfigurationValue>>();
        this.committed = new HashSet<String>();

        loadCache(configClass, filter);
        assert (varCache.size() > 0) : "coding error: nothing was configurable in the provided object!";
        for (Map.Entry<String, String> e : aliases.entrySet())
        {
            addAlias(e.getKey(), e.getValue());
        }
    }

    public ConfigurationBuffer(ConfigurationBuffer copyFrom, boolean copyCommitted)
    {
        this.configClass = copyFrom.configClass;
        this.varMap = new HashMap<String, List<ConfigurationValue>>(copyFrom.varMap);
        this.committed = copyCommitted ? new HashSet<String>(copyFrom.committed) : new HashSet<String>();
        this.varCache = copyFrom.varCache; // doesn't change after creation
        this.varList = copyFrom.varList; // doesn't change after creation
        this.tokens = new HashMap<String, String>(copyFrom.tokens);
    }

    public final List<String> dump()
    {
        final List<String> dump = new ArrayList<String>(varCache.size());
        for (final Map.Entry<String, ConfigurationInfo> entry : varCache.entrySet())
        {
            dump.add(entry.getKey() + "," + entry.getValue().toString());
        }
        Collections.sort(dump);
        return dump;
    }

    public void setVar(String var, String val, String source, int line) throws ConfigurationException
    {
        List<String> list = new LinkedList<String>();
        list.add(val);
        setVar(var, list, source, line, null, false);
    }

    public void setVar(String var, List<String> vals, String source, int line) throws ConfigurationException
    {
        setVar(var, vals, source, line, null, false);
    }

    public void setVar(String avar, List<String> vals, String source, int line, String contextPath, boolean append) throws ConfigurationException
    {
        String var = unalias(avar);
        if (!isValidVar(var))
            throw new ConfigurationException.UnknownVariable(var, source, line);

        int argCount = getVarArgCount(var);

        // -1 means unspecified length, its up to the receiving setter to validate.
        if (argCount != -1)
        {
            addAnyDefaultArgValues(var, argCount, vals);

            if (vals.size() != argCount)
            {
                throw new ConfigurationException.IncorrectArgumentCount(argCount, // expected
                        vals.size(), //passed
                var, source, line);
            }
        }

        ConfigurationValue val = new ConfigurationValue(this, var,
                                                         vals, //processValues( var, vals, source, line ),
                                                         source, line, contextPath);
        storeValue(var, val, append);
        committed.remove(var);
    }

    public void clearVar(String avar, String source, int line) throws ConfigurationException
    {
        String var = unalias(avar);
        if (!isValidVar(var))
            throw new ConfigurationException.UnknownVariable(var, source, line);
        varMap.remove(var);
        committed.remove(var);
    }

    /**
     * Remove the configuration values came from the given source.
     * 
     * @param source source name
     * @see CommandLineConfigurator#SOURCE_COMMAND_LINE
     */
    public void clearSourceVars(String source)
    {
        List<String> remove = new LinkedList<String>();
        for (Map.Entry<String, List<ConfigurationValue>> e : varMap.entrySet())
        {
            String var = e.getKey();
            List<ConfigurationValue> vals = e.getValue();

            List<ConfigurationValue> newvals = new LinkedList<ConfigurationValue>();
            for (ConfigurationValue val : vals)
            {
                if (!val.getSource().equals(source))
                {
                    newvals.add(val);
                }
            }
            if (newvals.size() > 0)
                varMap.put(var, newvals);
            else
                remove.add(var);
        }
        for (Iterator<String> it = remove.iterator(); it.hasNext();)
        {
            varMap.remove(it.next());
        }
    }

    public List<String> processValues(String var, List<String> args, String source, int line) throws ConfigurationException
    {
        List<String> newArgs = new LinkedList<String>();
        for (Iterator<String> it = args.iterator(); it.hasNext();)
        {
            String arg = it.next();

            int depth = 100;
            while (depth-- > 0)
            {
                int o = arg.indexOf("${");
                if (o == -1)
                    break;

                int c = arg.indexOf("}", o);

                if (c == -1)
                {
                    throw new ConfigurationException.Token(ConfigurationException.Token.MISSING_DELIMITER,
                                                           null, var, source, line);
                }
                String token = arg.substring(o + 2, c);
                String value = getToken(token);

                if (value == null)
                {
                    if (value == null)

                    {
                        throw new ConfigurationException.Token(ConfigurationException.Token.UNKNOWN_TOKEN,
                                                                token, var, source, line);
                    }

                }
                arg = arg.substring(0, o) + value + arg.substring(c + 1);

            }
            if (depth == 0)
            {
                throw new ConfigurationException.Token(ConfigurationException.Token.RECURSION_LIMIT,
                                                        null, var, source, line);
            }

            newArgs.add(arg);
        }
        return newArgs;
    }

    public void setToken(String token, String value)
    {
        tokens.put(token, value);
    }

    public String getToken(String token)
    {
        if (tokens.containsKey(token))
            return tokens.get(token);
        else
        {
            try
            {
                return System.getProperty(token);
            }
            catch (SecurityException se)
            {
                return null;
            }
        }
    }

    private void storeValue(String avar, ConfigurationValue val, boolean append) throws ConfigurationException
    {
        String var = unalias(avar);
        ConfigurationInfo info = getInfo(var);

        List<ConfigurationValue> vals;
        if (varMap.containsKey(var))
        {
            vals = varMap.get(var);
            assert (vals.size() > 0);
            ConfigurationValue first = vals.get(0);
            if (!append && !first.getSource().equals(val.getSource()))
                vals.clear();
            else if (!info.allowMultiple())
                throw new ConfigurationException.IllegalMultipleSet(
                                                  var,
                                                  val.getSource(), val.getLine());
        }
        else
        {
            vals = new LinkedList<ConfigurationValue>();
            varMap.put(var, vals);
        }
        vals.add(val);
    }

    public List<ConfigurationValue> getVar(String avar)
    {
        String var = unalias(avar);
        return varMap.get(var);
    }

    public Set<String> getVars()
    {
        return varCache.keySet();
    }

    public void merge(ConfigurationBuffer other)
    {
        assert (configClass == other.configClass);
        varMap.putAll(other.varMap);
        committed.addAll(other.committed);
    }

    private final Map<String, List<ConfigurationValue>> varMap; // list of vars that have been set
    private final Set<String> committed; // set of vars committed to backing config
    private final Class<? extends Configuration> configClass; // configuration class
    private Map<String, ConfigurationInfo> varCache // info cache
    = new HashMap<String, ConfigurationInfo>();
    private List<String> requiredList = new LinkedList<String>(); // required vars
    private List<String> varList = new LinkedList<String>(); // list of vars in order they should be set
    private Map<String, String> aliases = new HashMap<String, String>(); // variable name aliases
    private Map<String, String> tokens = new HashMap<String, String>(); // tokens for replacement
    private List<Object[]> positions = new ArrayList<Object[]>();

    private static final String SET_PREFIX = "cfg";
    private static final String GET_PREFIX = "get";
    private static final String INFO_SUFFIX = "Info";

    //-----------------------------------------------
    //

    /**
     * WORKAROUND FOR BUG CMP-396
     * 
     * <p>
     * {@link #c2h(String)} generates option names based on cfgXXX names in
     * {@code Configuration}. Since we collapsed all the sub-configurations into
     * one class, there's no longer a "base name" like "compiler.*" or
     * "compiler.fonts.*". In order to preserve the dotted naming convention, we
     * need to know which "-" separated names are actually dotted names. The
     * {@link #CONVERT_FROM} and {@link #CONVERT_TO} is an <b>ordered</b> lookup
     * table for option group base names. It's order makes sure that the longest
     * possible replacement is done.
     */
    private static final ImmutableList<String> CONVERT_FROM =
            ImmutableList.of(
                    "compiler-fonts-languages-",
                    "compiler-fonts-",
                    "compiler-namespaces-",
                    "compiler-mxml-",
                    "compiler-",
                    "metadata-",
                    "licenses-",
                    "frames-",
                    "runtime-shared-library-settings-");

    private static final ImmutableList<String> CONVERT_TO =
            ImmutableList.of(
                    "compiler.fonts.languages.",
                    "compiler.fonts.",
                    "compiler.namespaces.",
                    "compiler.mxml.",
                    "compiler.",
                    "metadata.",
                    "licenses.",
                    "frames.",
                    "runtime-shared-library-settings.");

    /**
     * convert StudlyCaps or camelCase to hyphenated
     * 
     * @param camel someVar or SomeVar
     * @return hyphen some-var
     */
    protected static String c2h(String camel)
    {
        StringBuilder b = new StringBuilder(camel.length() + 5);
        for (int i = 0; i < camel.length(); ++i)
        {
            char c = camel.charAt(i);
            if (Character.isUpperCase(c))
            {
                if (i != 0)
                    b.append('-');
                b.append(Character.toLowerCase(c));
            }
            else
            {
                b.append(camel.charAt(i));
            }
        }
        final String combined = b.toString();

        for (int i = 0; i < CONVERT_FROM.size(); i++)
        {
            if (combined.startsWith(CONVERT_FROM.get(i)))
            {
                return combined.replaceFirst(CONVERT_FROM.get(i), CONVERT_TO.get(i));
            }
        }
        return combined;
    }

    /**
     * convert hyphenated to StudlyCaps or camelCase
     * 
     * @param hyphenated some-var
     * @return result
     */
    protected static String h2c(String hyphenated, boolean studly)
    {
        StringBuilder b = new StringBuilder(hyphenated.length());
        boolean capNext = studly;
        for (int i = 0; i < hyphenated.length(); ++i)
        {
            char c = hyphenated.charAt(i);
            if (c == '-')
                capNext = true;
            else
            {
                b.append(capNext ? Character.toUpperCase(c) : c);
                capNext = false;
            }
        }
        return b.toString();
    }

    public static String varname(String membername, String basename)
    {
        return ((basename == null) ? membername : (basename + "." + membername));
    }

    private static ConfigurationInfo createInfo(Method setterMethod)
    {
        ConfigurationInfo info = null;

        String infoMethodName = GET_PREFIX + setterMethod.getName().substring(SET_PREFIX.length()) + INFO_SUFFIX;
        String getterMethodName = GET_PREFIX + setterMethod.getName().substring(SET_PREFIX.length());
        @SuppressWarnings("unchecked")
        Class<? extends Configuration> cfgClass = (Class<? extends Configuration>)setterMethod.getDeclaringClass();

        Method infoMethod = null, getterMethod = null;
        if (!setterMethod.isAnnotationPresent(Config.class))
        {
            try
            {
                infoMethod = cfgClass.getMethod(infoMethodName);
    
                if (!Modifier.isStatic(infoMethod.getModifiers()))
                {
                    assert false : ("coding error: " + cfgClass.getName() + "." + infoMethodName + " needs to be static!");
                    infoMethod = null;
                }
    
                info = (ConfigurationInfo)infoMethod.invoke(null, (Object[])null);
    
            }
            catch (SecurityException e)
            {
                e.printStackTrace();
            }
            catch (NoSuchMethodException e)
            {
            }
            catch (IllegalArgumentException e)
            {
                e.printStackTrace();
            }
            catch (IllegalAccessException e)
            {
                e.printStackTrace();
            }
            catch (InvocationTargetException e)
            {
                e.printStackTrace();
            }
        }
        
        if (info == null)
            info = new ConfigurationInfo();

        try
        {
            getterMethod = cfgClass.getMethod(getterMethodName, (Class[])null);
        }
        catch (SecurityException e)
        {
            e.printStackTrace();
        }
        catch (NoSuchMethodException e)
        {
        }
        info.setSetterMethod(setterMethod);
        info.setGetterMethod(getterMethod);

        return info;
    }

    /**
     * load - prefetch all the interesting names into a dictionary so that we
     * can find them again more easily. At the end of this call, we will have a
     * list of every variable and their associated method.
     * 
     * @param filter if null there is no filter, otherwise the set of
     * configuration options is filtered.
     */
    private boolean loadCache(Class<? extends Configuration> cfg, IConfigurationFilter filter)
    {
        int count = 0;

        // First, find all vars at this level.
        for (final Method method : cfg.getMethods())
        {
            if (method.getName().startsWith(SET_PREFIX) ||
                method.isAnnotationPresent(Config.class))
            {
                String configName = null;

                final Class<?>[] pt = method.getParameterTypes();
                assert pt.length > 1 : "Expected at least one parameters on setter.";

                // Collect configuration info from getXXXInfo() static methods.
                final ConfigurationInfo info = createInfo(method);

                // Collect configuration info from annotations.
                final Config config = method.getAnnotation(Config.class);
                if (config != null)
                {
                    info.isAdvanced = config.advanced();
                    info.isHidden = config.hidden();
                    info.isRemoved = config.removed();
                    info.allowMultiple = config.allowMultiple();
                    info.isPath = config.isPath();
                    info.isDisplayed = config.displayed();
                    info.isCompcOnly = config.compcOnly();
                    info.isRequired = config.isRequired();
                    
                    // Argument name generator class
                    final ArgumentNameGenerator argumentNameGeneratorClass = 
                        method.getAnnotation(ArgumentNameGenerator.class);
                    if (argumentNameGeneratorClass != null)
                    {
                        info.argNameGeneratorClass = argumentNameGeneratorClass.value();
                    }
                    else
                    {
                        // Argument names
                        final Arguments arguments = method.getAnnotation(Arguments.class);
                        if (arguments != null)
                            info.argnames = arguments.value();
                    }
                    
                    // Argument count
                    final InfiniteArguments infinite = method.getAnnotation(InfiniteArguments.class);
                    if (infinite != null)
                        info.argcount = ConfigurationInfo.INFINITE_ARGS;

                    // Soft Prerequisites
                    final SoftPrerequisites softPre = method.getAnnotation(SoftPrerequisites.class);
                    if (softPre != null)
                        info.softPrerequisites = softPre.value();
                    
                    // XML element name for configuration
                    final Mapping mapping = method.getAnnotation(Mapping.class);
                    if (mapping != null)
                        configName = Joiner.on(".").skipNulls().join(mapping.value());
                    
                    // Is this a Flex only option?
                    final RoyaleOnly royaleOnly = method.getAnnotation(RoyaleOnly.class);
                    if (royaleOnly != null)
                        info.isRoyaleOnly = true;
                }

                // Fall back to naming convention for configuration names.
                if (configName == null)
                    configName = c2h(method.getName().substring(SET_PREFIX.length()));

                if( filter == null || filter.select(configName) )
                {
                    varCache.put(configName, info);
                    varList.add(configName);
                    if (info.isRequired())
                    {
                        requiredList.add(configName);
                    }
                    ++count;
                }
            }
        }

        assert (count > 0 || filter != null) : "coding error: config class " + cfg.getName() + " did not define any setters or child configs";
        return (count > 0);
    }

    String classToArgName(Class<?> c)
    {
        // we only support builtin classnames!

        String className = c.getName();
        if (className.startsWith("java.lang."))
            className = className.substring("java.lang.".length());

        return className.toLowerCase();
    }

    public ConfigurationInfo getInfo(String avar)
    {
        String var = unalias(avar);
        return varCache.get(var);
    }

    public String getVarArgName(String avar, int argnum)
    {
        String var = unalias(avar);
        ConfigurationInfo info = getInfo(var);

        if (info == null)
        {
            assert false : ("must call isValid to check vars!");
        }

        return info.getArgName(argnum);
    }

    public boolean isValidVar(String avar)
    {
        String var = unalias(avar);
        ConfigurationInfo info = getInfo(var);
        return (info != null);
    }

    public int getVarArgCount(String avar)
    {
        ConfigurationInfo info = getInfo(avar);
        assert (info != null);
        return info.getArgCount();
    }

    /**
     * Add any default values to an argument, if the user did not specify them
     * on the command line.
     * 
     * @param avar the argument variable
     * @param argCount the number of argument values specified
     * @param vals Values to add any default values to
     */
    private void addAnyDefaultArgValues(String avar, int argCount, List<String> vals)
    {
        ConfigurationInfo info = getInfo(avar);
        final int missingArgsCount = argCount - vals.size();
        if (missingArgsCount == 0 || info.getDefaultArgValues() == null)
            return;

        final String[] defaultArgValues = info.getDefaultArgValues();
        final int defaultArgsCount = defaultArgValues.length;
        final int defaultArgsStart = defaultArgsCount - missingArgsCount;
        for (int i = defaultArgsStart; i < defaultArgsCount; i++)
        {
            vals.add(defaultArgValues[i]);
        }
    }

    /**
     * commit - bake the resolved map to the configuration
     * 
     * @param config The configuration to set the buffer variables into.
     * @param problems A collection where configuration problems are reported.
     * 
     * @return true if successful, false otherwise.
     */
    public boolean commit(Object config, Collection<ICompilerProblem> problems)
    {
        assert (config.getClass() == configClass) : 
            ("coding error: configuration " + config.getClass() + " != template " + configClass);
        Set<String> done = new HashSet<String>();
        boolean success = true;
        
        for (Iterator<String> vars = varList.iterator(); vars.hasNext();)
        {
            String var = vars.next();
            if (varMap.containsKey(var))
            {
                try
                {
                    commitVariable(config, var, done);
                }
                catch (ConfigurationException e)
                {
                    problems.add(new ConfigurationProblem(e));
                    success = false;
                }
            }
        }

        for (Iterator<String> reqs = requiredList.iterator(); reqs.hasNext();)
        {
            String req = reqs.next();

            if (!committed.contains(req))
            {
                ConfigurationException e = new ConfigurationException.MissingRequirement(req, null, null, -1);
                problems.add(new ConfigurationProblem(
                        null,
                        -1,
                        -1,
                        -1,
                        -1,
                        e.getMessage()));
                success = false;
            }
        }
        
        return success;
    }

    /**
     * commitVariable - copy a variable out of a state into the final config.
     * This should only be called on variables that are known to exist in the
     * state!
     * 
     * @param var variable name to lookup
     * @param done set of variable names that have been completed so far (for
     * recursion)
     */
    private void commitVariable(Object config, String var, Set<String> done) throws ConfigurationException
    {
        ConfigurationInfo info = getInfo(var);

        setPrerequisites(info.getPrerequisites(), var, done, config, true);
        setPrerequisites(info.getSoftPrerequisites(), var, done, config, false);

        if (committed.contains(var))
            return;

        committed.add(var);
        done.add(var);

        assert (varMap.containsKey(var));
        List<ConfigurationValue> vals = varMap.get(var);

        if (vals.size() > 1)
        {
            assert (info.allowMultiple()); // assumed to have been previously checked
        }
        for (ConfigurationValue val : vals)
        {
            try
            {
                Object[] args = buildArgList(info, val);

                info.getSetterMethod().invoke(config, args);
            }
            catch (Exception e)
            {
                Throwable t = e;

                if (e instanceof InvocationTargetException)
                {
                    t = ((InvocationTargetException)e).getTargetException();
                }

                if (Trace.error)
                    t.printStackTrace();

                if (t instanceof ConfigurationException)
                {
                    throw (ConfigurationException)t;
                }
                else
                {
                    throw new ConfigurationException.OtherThrowable(t, var, val.getSource(), val.getLine());
                }
            }
        }

    }

    private void setPrerequisites(String[] prerequisites, String var, Set<String> done, Object config, boolean required)
            throws ConfigurationException
    {
        if (prerequisites != null)
        {
            for (int p = 0; p < prerequisites.length; ++p)
            {
                String depvar = prerequisites[p];

                // Dependencies can only go downward.
                int dot = var.lastIndexOf('.');

                if (dot >= 0)
                {
                    String car = var.substring(0, dot);
                    //String cdr = var.substring( dot + 1 );

                    String newDepvar = car + "." + depvar;

                    // Since in royale we have collapsed sub-configurations into one
                    // configuration, some options that were in sub-configurations now
                    // have prerequisites on options in the same configuration. We
                    // need to keep the old configuration mappings so old configurations
                    // options will still work. So a simple thing we can do is if the 
                    // dependency variable is invalid (presumably because the 
                    // dependency is really on a parent configuration option), 
                    // then use the dependency as is depvar) and see if it is 
                    // valid. If depvar ends up not being valid then set depvar
                    // to newDepvar so error reporting isn't changed by the new
                    // fall-back behavior.
                    if (isValidVar(newDepvar) || !isValidVar(depvar))
                        depvar = newDepvar;

                }

                if (!done.contains(depvar))
                {
                    if (!isValidVar(depvar))
                    {
                        assert false : ("invalid " + var + " dependency " + depvar);
                        continue;
                    }
                    if (varMap.containsKey(depvar))
                    {
                        commitVariable(config, depvar, done);
                    }
                    else if (required && !committed.contains(depvar))
                    {
                        // TODO - can we get source/line for this?
                        throw new ConfigurationException.MissingRequirement(depvar, var, null, -1);
                    }
                }
            }
        }
    }

    private String[] constructStringArray(List<String> args)
    {
        String[] sa = new String[args.size()];

        int i = 0;
        for (Iterator<String> it = args.iterator(); it.hasNext();)
            sa[i++] = it.next();

        return sa;
    }

    private Object constructValueObject(ConfigurationInfo info, ConfigurationValue cv) throws ConfigurationException
    {
        try
        {
            Class<?>[] pt = info.getSetterMethod().getParameterTypes();
            assert (pt.length == 2); // assumed to be checked upstream

            Object o = pt[1].getConstructor().newInstance();

            Field[] fields = pt[1].getFields();

            assert (fields.length == cv.getArgs().size()); // assumed to be checked upstream

            Iterator<String> argsit = cv.getArgs().iterator();
            for (int f = 0; f < fields.length; ++f)
            {
                String val = (String)argsit.next();
                Object valobj = null;
                Class<?> fc = fields[f].getType();

                assert (info.getArgType(f) == fc);
                assert (info.getArgName(f).equals(ConfigurationBuffer.c2h(fields[f].getName())));

                if (fc == String.class)
                {
                    valobj = val;
                }
                else if ((fc == Boolean.class) || (fc == boolean.class))
                {
                    // TODO - Boolean.valueOf is pretty lax.  Maybe we should restrict to true/false?
                    valobj = Boolean.valueOf(val);
                }
                else if ((fc == Integer.class) || (fc == int.class))
                {
                    valobj = Integer.decode(val);
                }
                else if ((fc == Long.class) || (fc == long.class))
                {
                    valobj = Long.decode(val);
                }
                else
                {
                    assert false; // should have checked any other condition upstream!
                }
                fields[f].set(o, valobj);
            }

            return o;
        }
        catch (NoSuchMethodException e)
        {
            assert false : ("coding error: unable to find value object constructor when trying to set var " + cv.getVar());
            throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine());
        }
        catch (InvocationTargetException e)
        {
            assert false : ("coding error: unable to invoke value object constructor when trying to set var " + cv.getVar());
            throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine());
        }
        catch (InstantiationException e)
        {
            assert false : ("coding error: unable to instantiate value object when trying to set var " + cv.getVar());
            throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine());
        }
        catch (IllegalAccessException e)
        {
            assert false : ("coding error: " + e + " when trying to set var " + cv.getVar());
            throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine());
        }
    }

    protected static boolean isSupportedSimpleType(Class<?> c)
    {
        return ((c == String.class)
                || (c == Integer.class) || (c == int.class)
                || (c == Long.class) || (c == long.class)
                || (c == Boolean.class) || (c == boolean.class));
    }

    protected static boolean isSupportedListType(Class<?> c)
    {
        return ((c == List.class) || (c == String[].class));
    }

    protected static boolean isSupportedValueType(Class<?> c)
    {
        if (isSupportedSimpleType(c))
            return false;

        Field[] fields = c.getFields();

        for (int f = 0; f < fields.length; ++f)
        {
            if (!isSupportedSimpleType(fields[f].getType()))
                return false;
        }
        return true;
    }

    private Object[] buildArgList(ConfigurationInfo info, ConfigurationValue val) throws ConfigurationException
    {
        Method setter = info.getSetterMethod();

        Class<?>[] pt = setter.getParameterTypes();

        List<String> args = processValues(val.getVar(), val.getArgs(), val.getSource(), val.getLine());

        if (info.getArgCount() == -1)
        {
            if (pt.length != 2)
            {
                assert false : ("coding error: unlimited length setter " + val.getVar() + " must take a single argument of type List or String[]");
                return null;
            }
            else if (List.class.isAssignableFrom(pt[1]))
            {
                return new Object[] {val, args};
            }
            else if (String[].class.isAssignableFrom(pt[1]))
            {
                return new Object[] {val, constructStringArray(args)};
            }
            else
            {
                assert false : ("coding error: unlimited length setter " + val.getVar() + " must take a single argument of type List or String[]");
                return null;
            }
        }
        else
        {
            assert (pt.length > 1) : ("coding error: config setter " + val.getVar() + " must accept at least one argument");
            // ok, we first check to see if the signature of their setter accepts a list.

            if (pt.length == 2)
            {
                // a variety of specialty setters here...

                if (List.class.isAssignableFrom(pt[1]))
                {
                    return new Object[] {val, args};
                }
                else if (String[].class == pt[1])
                {
                    return new Object[] {val, constructStringArray(args)};
                }
                else if (isSupportedValueType(pt[1]))
                {
                    return new Object[] {val, constructValueObject(info, val)};
                }
            }

            // otherwise, they must have a matching size parm list as the number of args passed in.

            assert (pt.length == (args.size() + 1)) : ("coding error: config setter " + val.getVar() + " does not have " + args.size() + " parameters!");

            Object[] pa = new Object[pt.length];

            pa[0] = val;

            for (int p = 1; p < pt.length; ++p)
            {
                String arg = args.get(p - 1);
                if (pt[p].isAssignableFrom(String.class))
                {
                    pa[p] = arg;
                }
                else if ((pt[p] == int.class) || (pt[p] == Integer.class))
                {
                    try
                    {
                        pa[p] = Integer.decode(arg);

                    }
                    catch (Exception e)
                    {
                        throw new ConfigurationException.TypeMismatch(ConfigurationException.TypeMismatch.INTEGER,
                                                                       arg, val.getVar(), val.getSource(), val.getLine());
                    }
                }
                else if ((pt[p] == long.class) || (pt[p] == Long.class))
                {
                    try
                    {
                        pa[p] = Long.decode(arg);

                    }
                    catch (Exception e)
                    {
                        throw new ConfigurationException.TypeMismatch(
                                ConfigurationException.TypeMismatch.LONG,
                                arg, val.getVar(), val.getSource(), val.getLine());
                    }
                }
                else if ((pt[p] == boolean.class) || (pt[p] == Boolean.class))
                {
                    try
                    {
                        arg = arg.trim().toLowerCase();
                        if (arg.equals("true") || arg.equals("false"))
                        {
                            pa[p] = Boolean.valueOf(arg);
                        }
                        else
                        {
                            throw new ConfigurationException.TypeMismatch(
                                    ConfigurationException.TypeMismatch.BOOLEAN, arg, val.getVar(), val.getSource(), val.getLine());
                        }
                    }
                    catch (Exception e)
                    {
                        throw new ConfigurationException.TypeMismatch(
                                ConfigurationException.TypeMismatch.BOOLEAN, arg, val.getVar(), val.getSource(), val.getLine());
                    }
                }
                else
                {
                    assert false : ("coding error: " + val.getVar() + " setter argument " + p + " is not a supported type");
                }
            }

            return pa;
        }
    }

    public void addAlias(String alias, String var)
    {
        if (!isValidVar(var))
        {
            assert false : ("coding error: can't bind alias " + alias + " to nonexistent var " + var);
            return;
        }
        if (aliases.containsKey(alias))
        {
            assert false : ("coding error: alias " + alias + " already defined as " + aliases.get(alias));
            return;
        }
        if (varCache.containsKey(alias))
        {
            assert false : ("coding error: can't define alias " + alias + ", it already exists as a var");
            return;
        }

        aliases.put(alias, var);
    }

    public Map<String, String> getAliases()
    {
        return aliases;
    }

    public String unalias(String var)
    {
        String realvar = aliases.get(var);
        return (realvar == null) ? var : realvar;
    }

    public String peekSimpleConfigurationVar(String avar) throws ConfigurationException
    {
        String val = null;
        List<ConfigurationValue> valList = getVar(avar);
        if (valList != null)
        {
            ConfigurationValue cv = (ConfigurationValue)valList.get(0);
            List<String> args = processValues(avar, cv.getArgs(), cv.getSource(), cv.getLine());
            val = args.get(0);
        }
        return val;
    }

    public List<ConfigurationValue> peekConfigurationVar(String avar) throws ConfigurationException
    {
        List<ConfigurationValue> srcList = getVar(avar);
        if (srcList == null)
            return null;

        List<ConfigurationValue> dstList = new LinkedList<ConfigurationValue>();
        for (ConfigurationValue srcVal : srcList)
        {
            List<String> args = processValues(avar, srcVal.getArgs(), srcVal.getSource(), srcVal.getLine());

            ConfigurationValue dstVal = new ConfigurationValue(srcVal.getBuffer(), avar, args, srcVal.getSource(), srcVal.getLine(), srcVal.getContext());
            dstList.add(dstVal);
        }
        return dstList;
    }

    public void addPosition(String var, int iStart, int iEnd)
    {
        positions.add(new Object[] {var, Integer.valueOf(iStart), Integer.valueOf(iEnd)});
    }

    public List<Object[]> getPositions()
    {
        return positions;
    }

    public static List<String> formatText(String input, int columns)
    {
        ArrayList<String> lines = new ArrayList<String>();

        if ((input == null) || (input.length() == 0))
            return lines;

        int current = 0;
        int lineStart = -1;
        int lineEnd = -1;
        int wordStart = -1;
        int wordEnd = -1;
        boolean start = true;
        boolean preserve = true;

        while (true)
        {
            if (current < input.length())
            {
                boolean newline = input.charAt(current) == '\n';
                boolean printable = (preserve && !newline) || !Character.isWhitespace(input.charAt(current));

                if (start) // find a word
                {
                    if (printable)
                    {
                        if (lineStart == -1)
                        {
                            lineStart = current;
                        }
                        wordStart = current;
                        start = false;
                    }
                    else
                    {
                        if (newline && lineStart != -1)
                        {
                            lines.add(input.substring(lineStart, current));
                            lineStart = -1;
                        }
                        else if (newline)
                        {
                            lines.add("");
                        }
                        ++current;
                    }
                }
                else
                // have a word
                {
                    preserve = false;
                    if (printable)
                    {
                        ++current;
                    }
                    else
                    {
                        wordEnd = current;
                        if (lineEnd == -1)
                        {
                            lineEnd = current;
                        }

                        // two possibilities; if the new word fits in the current line length
                        // without being too many columns, leave on current line.
                        // otherwise, set it as the start of a new line.

                        if (wordEnd - lineStart < columns)
                        {
                            if (newline)
                            {
                                lines.add(input.substring(lineStart, current));
                                lineStart = -1;
                                lineEnd = -1;
                                wordStart = -1;
                                start = true;
                                preserve = true;
                                ++current;
                            }
                            else
                            {
                                // we have room to add the current word to this line, find new word
                                start = true;
                                lineEnd = current;
                            }
                        }
                        else
                        {
                            // current word pushes things beyond the requested column limit,
                            // dump current text
                            lines.add(input.substring(lineStart, lineEnd));
                            lineStart = wordStart;
                            lineEnd = -1;
                            wordStart = -1;
                            start = true;
                            if (newline)
                                preserve = true;
                        }
                    }
                }
            }
            else
            // we're done
            {
                // a) no line yet, so don't do anything
                // b) have line and new word would push over edge, need two lines
                // c) have line and current word fits, need one line
                // d) only one word and its too long anyway, need one line

                if (lineStart != -1) // we have a line in progress
                {
                    wordEnd = current;
                    if (lineEnd == -1)
                        lineEnd = current;

                    if (((wordEnd - lineStart) < columns) // current word fits
                        || (wordEnd == lineEnd)) // or one long word
                    {
                        lineEnd = wordEnd;
                        lines.add(input.substring(lineStart, wordEnd));
                    }
                    else
                    // didn't fit, multiple words
                    {
                        lines.add(input.substring(lineStart, lineEnd));
                        lines.add(input.substring(wordStart, wordEnd));
                    }
                }
                break;
            }
        }
        return lines;
    }

    /**
     * For debugging only.
     * <p>
     * Produces an alphabetized list of this buffer's configuration options and their values.
     * An option such as
     * <pre>
     * -foo=aaa,bbb -foo+=ccc
     * </pre>
     * will appear as
     * <pre>
     * foo=aaa,bbb;ccc
     * </pre>
     */
    @Override
    public String toString()
    {
        StringBuffer sb = new StringBuffer();
        
        String[] variables = varMap.keySet().toArray(new String[0]);
        Arrays.sort(variables);
        
        for (String var : variables)
        {
            sb.append(var);
            sb.append("=");

            ArrayList<String> commaSeparatedValues = new ArrayList<String>();
            for (ConfigurationValue cv : varMap.get(var))
            {
                List<String> args = cv.getArgs();
                String joinedArgs = Joiner.on(',').join(args);
                commaSeparatedValues.add(joinedArgs);
            }
            String rhs = Joiner.on(';').join(commaSeparatedValues);
            sb.append(rhs);
            
            sb.append('\n');
        }

        return sb.toString();
    }
}
