/*
 *
 *  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 flex2.compiler.config;

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.InvocationTargetException;
import java.io.UnsupportedEncodingException;

import flex2.compiler.io.VirtualFile;

/**
 * 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>
 *
 * @author Roger Gonzalez
 */
@SuppressWarnings("rawtypes")
public final class ConfigurationBuffer
{
    public ConfigurationBuffer( Class configClass )
    {
        this( configClass, new HashMap<String, String>() );
    }

    public ConfigurationBuffer( Class 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 configClass, Map<String, String> aliases, ConfigurationFilter filter )
    {
        this.configClass = configClass;
        this.varMap = new HashMap<String, List>();
        this.committed = new HashSet<String>();

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

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

    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)
        {
            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 );
    }

    public void clearSourceVars( String source )
    {
        List<String> remove = new LinkedList<String>();
        for (Iterator it = varMap.entrySet().iterator(); it.hasNext();)
        {
            Map.Entry e = (Map.Entry) it.next();
            String var = (String) e.getKey();
            List vals = (List) e.getValue();

            List<ConfigurationValue> newvals = new LinkedList<ConfigurationValue>();
            for (Iterator vi = vals.iterator(); vi.hasNext();)
            {
                ConfigurationValue val = (ConfigurationValue) vi.next();

                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 (false && isValidVar( token ))
                    {
                        if (varMap.containsKey( token ))
                        {
                            List vals = varMap.get( token );
                            assert ( vals.size() > 0 );
                            if (vals.size() > 1)
                            {
                                throw new ConfigurationException.Token( ConfigurationException.Token.MULTIPLE_VALUES,
                                                                        token, var, source, line );
                            }
                            ConfigurationValue first = (ConfigurationValue) vals.get( 0 );
                            if (first.getArgs().size() != 1)
                            {
                                throw new ConfigurationException.Token( ConfigurationException.Token.MULTIPLE_VALUES,
                                                                        token, var, source, line );

                            }
                            value = first.getArgs().get( 0 );
                        }
                    }
                    */
                    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;
            }
        }
    }

    @SuppressWarnings("unchecked")
    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 getVar( String avar )
    {
        String var = unalias( avar );
        return varMap.get( var );
    }

    public Iterator<String> getVarIterator()
    {
        return varCache.keySet().iterator();
    }

    /*
    private Iterator<String> getSetVarIterator()
    {
        return varMap.keySet().iterator();
    }
    */
    
    public void merge( ConfigurationBuffer other )
    {
        assert ( configClass == other.configClass );
        varMap.putAll( other.varMap );
        committed.addAll( other.committed );
    }

    public void mergeChild( String prefix, ConfigurationBuffer child )
    {
        assert isChildConfig( prefix ) : "coding error: " + prefix + " is not a child configuration object.";

        for (Iterator<Map.Entry<String, List>> it = child.varMap.entrySet().iterator(); it.hasNext();)
        {
            Map.Entry<String, List> e = it.next();

            varMap.put( prefix + "." + e.getKey(), e.getValue() );
        }
        for (Iterator<String> it = child.committed.iterator(); it.hasNext();)
        {
            String var = it.next();

            committed.add( prefix + "." + var );
        }
    }

    private final Map<String, List> varMap;                                 // list of vars that have been set
    private final Set<String> committed;                                    // set of vars committed to backing config
    private final Class 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, Class> childCache = new HashMap<String, Class>();   // child configuration objects
    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 CONFIGURATION_SUFFIX = "Configuration";
    private static final String INFO_SUFFIX = "Info";

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

    /**
     * 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) );
            }
        }
        return b.toString();
    }

    /**
     * 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();
    }

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

    @SuppressWarnings("unchecked")
    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() );
        Class cfgClass = setterMethod.getDeclaringClass();

        Method infoMethod, getterMethod = null;
        try
        {
            infoMethod = cfgClass.getMethod( infoMethodName, (Class[])null);

            if (Modifier.isStatic( infoMethod.getModifiers() ) ) {
                info = (ConfigurationInfo) infoMethod.invoke(null, (Object[]) null);

                getterMethod = cfgClass.getMethod(getterMethodName, (Class[]) null);
            }
            else {
                assert false : ( "coding error: " + cfgClass.getName() + "." + infoMethodName + " needs to be static!" );
            }
        }
        catch (Exception e)
        {
            // Ignore
        }

        if (info == null)
        {
            info = new ConfigurationInfo();
        }
        info.setSetterMethod( setterMethod );
	    info.setGetterMethod( getterMethod );

        return info;
    }

    /*
    private static ConfigurationInfo createChildInfo( Method childGetMethod )
    {
        ConfigurationInfo info = null;
        int cfgIndex = childGetMethod.getName().lastIndexOf( CONFIGURATION_SUFFIX );
        assert cfgIndex != -1;
        String infoMethodName = childGetMethod.getName().substring( 0, cfgIndex ) + INFO_SUFFIX;
        Class cfgClass = childGetMethod.getDeclaringClass();

        Method infoMethod = null;
        try
        {
            infoMethod = cfgClass.getMethod( infoMethodName, (Class[])null);

            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 );

            info.setSetterMethod( null );

            assert info.getAliases() == null : "coding error: child configurations cannot have aliases.";
            assert info.getArgCount() == 0 : "coding error: child configurations do not have arguments";
            assert info.getArgName( 0 ) == null : "coding error: child configuraitons do not have argnames";

        }
        catch (Exception e)
        {
            return null;
        }

        if (info == null)
        {
            info = new ConfigurationInfo();
        }
        
        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 cfg, String basename, ConfigurationFilter filter)
    {
        int count = 0;

        // First, find all vars at this level.
        Method methods[] = cfg.getMethods();
        for (int m = 0; m < methods.length; ++m)
        {
            Method method = methods[m];

            if (method.getName().startsWith( SET_PREFIX ))
            {
                Class[] pt = method.getParameterTypes();

                if ((pt.length > 1) && (pt[0] == ConfigurationValue.class))
                {
                    // This is an autoconfiguration setter!

                    ConfigurationInfo info = createInfo( method );

                    String leafname = c2h( method.getName().substring( SET_PREFIX.length() ) );
                    String name = varname( leafname, basename );

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

        // Now find all children.
        for (int m = 0; m < methods.length; ++m)
        {
            Method method = methods[m];

            if (method.getName().startsWith( GET_PREFIX )
                    && method.getName().endsWith( CONFIGURATION_SUFFIX ))
            {
                String leafname = c2h( method.getName().substring( GET_PREFIX.length(),
                                                                   method.getName().length() - CONFIGURATION_SUFFIX.length()));
                String fullname = varname( leafname, basename );

                if (loadCache( method.getReturnType(), fullname, filter ))
                {
                    childCache.put( fullname, method.getReturnType() );
                    ++count;
                }
            }
            else
            {
                continue;
            }
        }

        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 );
    }

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

        assert info == null : ( "must call isValid to check vars!" );
        if (info != null) {
            return info.getArgName( argnum );
        }
        return null;
    }

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

    public boolean isChildConfig( String var )
    {
        return childCache.keySet().contains( var );
    }

    public Class getChildConfigClass( String var )
    {
        return childCache.get( var );
    }

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

    /**
     * commit - bake the resolved map to the configuration
     */
    public void commit( Object config ) throws ConfigurationException
    {
        assert ( config.getClass() == configClass ) : ( "coding error: configuration " + config.getClass() + " != template " + configClass );
        Set<String> done = new HashSet<String>();

        for (Iterator<String> vars = varList.iterator(); vars.hasNext(); )
        {
            String var = vars.next();
            if (varMap.containsKey( var ))
            {
                commitVariable( config, var, done );
            }
        }

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

            if (!committed.contains( req ))
            {
                throw new ConfigurationException.MissingRequirement( req, null, null, -1 );
            }
        }
    }

    /**
     * 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 vals = varMap.get( var );

        if (vals.size() > 1)
        {
            assert ( info.allowMultiple() );   // assumed to have been previously checked
        }
        for (Iterator valit = vals.iterator(); valit.hasNext();)
        {
            ConfigurationValue val = (ConfigurationValue) valit.next();

            try
            {
                Object targetconfig = getParentConfiguration( config, var );
                Object[] args = buildArgList( info, val );

                info.getSetterMethod().invoke( targetconfig, args );

	            calculateChecksum(targetconfig, info, var, 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 );

					depvar = car + "." + depvar;
				}

				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 ))
					{
                        // FIXME - can we get source/line for this?
                        throw new ConfigurationException.MissingRequirement(depvar, var, null, -1);
					}
				}
			}
		}
	}

	Object getParentConfiguration( Object config, String varname )
	{
		int dot = varname.indexOf( '.' );   // FIXME? should be lastIndexOf? --rg

		String getConfigName;
		if (dot < 0)
		{
			// varname is in current config.

			return config;
		}
		else
		{
			String car = varname.substring( 0, dot );
			String cdr = varname.substring( dot + 1 );

			getConfigName = GET_PREFIX + h2c( car, true ) + CONFIGURATION_SUFFIX;

			try
			{
				Method getCfgMethod = config.getClass().getMethod( getConfigName, (Class[])null );

				Object child = getCfgMethod.invoke( config, (Object[])null );

				return getParentConfiguration( child, cdr );
			}
			catch (NoSuchMethodException e)
			{
				assert false : ( "impossible: should have already confirmed this!" );
			}
			catch (InvocationTargetException e)
			{
				assert false : ( "coding error: bad child config getter" );
			}
			catch (IllegalAccessException e)
			{
				assert false : ( "coding error: bad child config getter" );
			}
			return null;
		}
	}

    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 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 srcList = getVar(avar);
        if (srcList == null)
            return null;

        List<ConfigurationValue> dstList = new LinkedList<ConfigurationValue>();
        for (Iterator it = srcList.iterator(); it.hasNext();)
        {
			ConfigurationValue srcVal = (ConfigurationValue) it.next();
            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;
	}
	
    // C: checksum calculation for Configuration. checksum() is based on config values. checksum_ts() is
	//    based on config values + timestamps from VirtualFile-based config values.

	public void setDefaultVar(String var)
	{
		defaultVar = var;
	}
	
	private String defaultVar;
	private StringBuilder compile_checksum = new StringBuilder();
	private StringBuilder compile_checksum_ts = new StringBuilder();
	private StringBuilder link_checksum = new StringBuilder();
	private StringBuilder link_checksum_ts = new StringBuilder();

	private void calculateChecksum(Object targetConfig, ConfigurationInfo info, String var, Object[] args)
		throws Exception
	{
		// C: don't use default var to calculate checksum...
		if (var != null && var.equals(defaultVar))
		{
			return;
		}
		
		// C: we always update link_checksum and link_checksum_ts.

		if (info.doChecksum())
			compile_checksum.append(var);
		link_checksum.append(var);

		for (int i = 1; i < args.length; i++)
		{
			if (info.getGetterMethod() != null)
			{
				Class retType = info.getGetterMethod().getReturnType();

				if (VirtualFile.class.isAssignableFrom(retType))
				{
					VirtualFile file = (VirtualFile) info.getGetterMethod().invoke(targetConfig, (Object[])null);
					if (file != null)
					{
						if (info.doChecksum())
							compile_checksum.append(file.getName());
						link_checksum.append(file.getName());
					}
					continue;
				}
				else if (retType.isArray() && VirtualFile.class.isAssignableFrom(retType.getComponentType()))
				{
					VirtualFile[] files = (VirtualFile[]) info.getGetterMethod().invoke(targetConfig, (Object[])null);
					for (int j = 0; files != null && j < files.length; j++)
					{
						if (files[j] != null)
						{
							if (info.doChecksum())
								compile_checksum.append(files[j].getName());
							link_checksum.append(files[j].getName());
						}
					}
					continue;
				}
			}

			if (args[i] instanceof Object[])
			{
				Object[] a = (Object[]) args[i];
				for (int j = 0; j < a.length; j++)
				{
					if (info.doChecksum())
						compile_checksum.append(a[j]);
					link_checksum.append(a[j]);
				}
			}
			else if (args[i] instanceof List)
			{
				List l = (List) args[i];
				for (int j = 0; j < l.size(); j++)
				{
					if (info.doChecksum())
						compile_checksum.append(l.get(j));
					link_checksum.append(l.get(j));
				}
			}
			else
			{
				if (info.doChecksum())
					compile_checksum.append(args[i]);
				link_checksum.append(args[i]);
			}
		}

		if (info.getGetterMethod() == null)
		{
			// C: need to make sure that all the VirtualFile-based config values should have getters.
			return;
		}

		Class retType = info.getGetterMethod().getReturnType();

		if (VirtualFile.class.isAssignableFrom(retType))
		{
			VirtualFile file = (VirtualFile) info.getGetterMethod().invoke(targetConfig, (Object[])null);
			if (file != null && !file.isDirectory())
			{
				if (info.doChecksum())
					compile_checksum_ts.append(file.getLastModified());
				link_checksum_ts.append(file.getLastModified());
			}
		}
		else if (retType.isArray() && VirtualFile.class.isAssignableFrom(retType.getComponentType()))
		{
			VirtualFile[] files = (VirtualFile[]) info.getGetterMethod().invoke(targetConfig, (Object[])null);
			for (int i = 0; files != null && i < files.length; i++)
			{
				if (files[i] != null && !files[i].isDirectory())
				{
					if (info.doChecksum())
						compile_checksum_ts.append(files[i].getLastModified());
					link_checksum_ts.append(files[i].getLastModified());
				}
			}
		}
	}

	// Mxmlc.processConfiguration() derives app-config.xml from the command-line target file argument.
	// That's why this is necessary...
	public void calculateChecksum(VirtualFile f)
	{		
		compile_checksum.append(f.getName());
		compile_checksum_ts.append(f.getLastModified());
	}

	// The web tier can use this to provide the timestamps of the dependent files (e.g. remoting-service.xml)
	// referenced by the service config file.
	public void calculateChecksum(String name, Long lastModified)
	{		
		compile_checksum.append(name);
		compile_checksum_ts.append(lastModified);
	}

    // This is to allow the oem api to add archive files to the link checksum.
    public void calculateLinkChecksum(String name, Long lastModified)
    {       
        link_checksum.append(name);
        link_checksum_ts.append(lastModified);
    }

	private int calculateChecksum(String str)
	{
		byte[] b = null;

		try
		{
			b = str.getBytes("UTF8");
		}
		catch (UnsupportedEncodingException ex)
		{
			b = str.getBytes();
		}

		int checksum = 0;

        //TODO Better checksum algorithm
		for (int i = 0; i < b.length; i++)
		{
			checksum += b[i];
		}

		return checksum;
	}

	/**
	 * This value is good for naming the cache file.
	 */
	public int checksum()
	{
		return calculateChecksum(compile_checksum.toString());
	}

	/**
	 * This value takes timestamps into account and is the actual value embedded in the cache file.
	 */
	public int checksum_ts()
	{
		return calculateChecksum(compile_checksum.toString() + compile_checksum_ts.toString());
	}
	
	public int link_checksum_ts()
	{
		return calculateChecksum(link_checksum.toString() + link_checksum_ts.toString());
	}

    static public 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;
    }
}

