/*
 *
 *  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 flash.localization.LocalizationManager;

import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.Set;
import java.util.Iterator;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.io.File;

/**
 * A utility class, which is used to parse an array of command line
 * args and populate a ConfigurationBuffer.  It also contains some
 * associated methods like brief() and usage().  A counterpart of
 * FileConfigurator and SystemPropertyConfigurator.
 *
 * @author Roger Gonzalez
 */
public class CommandLineConfigurator
{
	public static final String SOURCE_COMMAND_LINE = "command line";
	
    /**
     * parse - buffer up configuration vals from the command line
     *
     * @param buffer        the configuration buffer to hold the results
     * @param defaultvar    the variable name where the trailing loose args go
     * @param args          the command line
     * @throws ConfigurationException
     */
    public static void parse( final ConfigurationBuffer buffer,
                              final String defaultvar,
                              final String[] args)
            throws ConfigurationException
    {
        assert defaultvar == null || buffer.isValidVar( defaultvar ) : "coding error: config must provide default var " + defaultvar;

        Map<String, String> aliases = getAliases( buffer );
        final int START = 1;
        final int ARGS = 2;
        final int EXEC = 3;
        final int DONE = 4;


        int i = 0, iStart = 0, iEnd = 0;
        String var = null;
        int varArgCount = -2;
        List<String> argList = new LinkedList<String>();
        Set<String> vars = new HashSet<String>();
        boolean append = false;
        boolean dash = true;

        int mode = START;

        while (mode != DONE)
        {
            switch (mode)
            {
                case START:
                {
                	iStart = i;
                	
                    if (args.length == i)
                    {
                        mode = DONE;
                        break;
                    }
                    // expect -var, --, or the beginning of default args

                    mode = ARGS;
                    varArgCount = -2;

                    if (args[i].equals("--"))
                    {
                        dash = false;
                        if (defaultvar != null)
                            var = defaultvar;
                        else
                            mode = START;
                        ++i;
                    }
                    else if (dash && args[i].startsWith("+"))
                    {
                        String token = null;
                        int c = (args[i].length() > 1 && args[i].charAt( 1 ) == '+')? 2 : 1;    // gnu-style?

                        int equals = args[i].indexOf( '=' );
                        String rest = null;
                        if (equals != -1)
                        {
                            rest = args[i].substring( equals + 1 );
                            token = args[i++].substring( c, equals );
                        }
                        else
                        {
                            token = args[i++].substring( c );
                        }
                        if (equals != -1)
                        {
                        	iEnd = i;
                            buffer.setToken( token, rest );
                            buffer.addPosition(token, iStart, iEnd);
                        }
                        else
                        {
                            if (i == args.length)
                            {
                                throw new ConfigurationException.Token( ConfigurationException.Token.INSUFFICIENT_ARGS,
                                                                        token, var, source, -1 );
                            }
                            rest = args[i++];
                            iEnd = i;
                            buffer.setToken( token, rest );
                            buffer.addPosition(token, iStart, iEnd);
                        }
                        mode = START;
                        break;
                    }
                    else if (dash && isAnArgument(args[i]))
                    {
                        int c = (args[i].length() > 1 && args[i].charAt( 1 ) == '-')? 2 : 1;    // gnu-style?

                        int plusequals = args[i].indexOf( "+=" );
                        int equals = args[i].indexOf( '=' );
                        String rest = null;
                        if (plusequals != -1)
                        {
                            rest = args[i].substring( plusequals + 2 );
                            var = args[i++].substring( c, plusequals );
                            append = true;
                        }
                        else if (equals != -1)
                        {
                            rest = args[i].substring( equals + 1 );
                            var = args[i++].substring( c, equals );
                        }
                        else
                        {
                            var = args[i++].substring( c );
                        }

                        if (aliases.containsKey( var ))
                            var = aliases.get( var );

                        if (!buffer.isValidVar( var ))
                        {
                            throw new ConfigurationException.UnknownVariable( var, source, -1 );
                        }

                        if (equals != -1)
                        {
                            if ((rest == null) || (rest.length() == 0))
                            {
                            	iEnd = i;
                                buffer.clearVar( var, source, -1 );
                                buffer.addPosition(var, iStart, iEnd);
                                mode = START;
                            }
                            else
                            {
                                String seps = null;
                                if (buffer.getInfo(var).isPath())
                                {
                                    seps = "[," + File.pathSeparatorChar + "]";
                                }
                                else {
                                	seps = ",";
                                }
                                
                                String[] tokens = rest.split(seps);
                                argList.addAll(Arrays.asList(tokens));
                                varArgCount = buffer.getVarArgCount( var );
                                mode = EXEC;
                            }
                        }

                    }
                    else
                    {
                        // asdoc sets default var as no-default-arg - it has no default vars
                        if (defaultvar != null  && !defaultvar.equals("no-default-arg"))
                        {
                            // don't increment i, let ARGS pick it up.
                            var = defaultvar;
                        }
                        else
                        {
                            throw new ConfigurationException.UnexpectedDefaults( null, null, -1 );
                        }
                    }
                    break;
                }
                case ARGS:
                {
                    if (varArgCount == -2)
                    {
                        if (isBoolean( buffer, var ))
                        {
                            varArgCount = 0;
                            mode = EXEC;
                            break;
                        }
                        else
                        {
                            varArgCount = buffer.getVarArgCount( var );
                        }
                    }
                    assert varArgCount >= -1;   // just in case the getVarArgCount author was insane.

                    if (args.length == i)
                    {
                        mode = EXEC;
                        break;
                    }

                    boolean greedy = buffer.getInfo( var ).isGreedy();

                    // accumulating non-command arguments...

                    
                    // check for a terminator on our accumulated parameter list
                    if (!greedy && dash && isAnArgument(args[i]))
                    {
                        if (varArgCount == -1)
                        {
                            // we were accumulating an unlimited set of args, a new var terminates that.
                            mode = EXEC;
                            break;
                        }
                        throw new ConfigurationException.IncorrectArgumentCount( varArgCount, argList.size(), var, source, -1 );
                    }

                    // this test is a little hairy:
                    //    "The key is that the parameter before the "default" parameter takes an
                    //     unlimited number of parameters: mxmlc -rsl 1.swf 2.swf test.mxml" -dloverin
                    if ((varArgCount == -1)
                            && !greedy
                            && (defaultvar != null)
                            && !defaultvar.equals(var)
                            && !vars.contains( defaultvar )
                            && ((args.length - i) > 1)
                            && buffer.getInfo( defaultvar ) != null)
                    {
                        // look for a terminating argument, if none,
                        // then the end of the list cannot be determined (it's ambiguous)
                        boolean ok = false;
                        for (int j = i + 1; j < args.length; ++j)
                        {
                            if (dash && isAnArgument(args[j]))
                            {
                                ok = true;
                                break;
                            }
                        }
                        if (!ok)
                        {
                            throw new ConfigurationException.AmbiguousParse( defaultvar, var, source, -1 );
                        }
                    }

                    argList.add( args[i++] );
                    if (argList.size() == varArgCount)
                    {
                        mode = EXEC;
                    }

                    break;
                }
                case EXEC:
                {
                    if ((varArgCount != -1) && (argList.size() != varArgCount))
                    {
                        throw new ConfigurationException.IncorrectArgumentCount( varArgCount, argList.size(), var, source, -1 );
                    }
                    if (varArgCount == 0)       // boolean flag fakery...
                        argList.add( "true" );

                    if (vars.contains( var ))
                    {
                        if ((defaultvar != null) && (var != null) && var.equals( defaultvar ))
                        {
                            // we could perhaps accumulate the defaults spread out through
                            // the rest of the flags, but for now we'll call this illegal.
                            throw new ConfigurationException.InterspersedDefaults( var, source, -1 );
                        }
                    }
                    iEnd = i;
                    buffer.setVar( var, new LinkedList<String>( argList ), source, -1, null, append );
                    buffer.addPosition(var, iStart, iEnd);
                    append = false;
                    vars.add( var );
                    argList.clear();
                    mode = START;
                    break;
                }
                case DONE:
                {
                    assert false;
                    break;
                }
            }
        }
    }
    
    /**
     * Given a string like "-foo" or "-5" or "-123.mxml", this determines whether
     * the string is an argument or... not an argument (e.g. numeral)
     */
    private static boolean isAnArgument(final String arg)
    {
        return (arg.startsWith("-") &&
                // if the first character after a dash is numeric, this is not
                // an argument, it is a parameter (and therefore non-terminating)
               (arg.length() > 1) && !Character.isDigit(arg.charAt(1)));
    }
    
    private static Map<String, String> getAliases( ConfigurationBuffer buffer )
    {
        Map<String, String> aliases = new HashMap<String, String>();
        aliases.putAll( buffer.getAliases() );
        for (Iterator it = buffer.getVarIterator(); it.hasNext(); )
        {
            String varname = (String) it.next();

            if (varname.indexOf( '.' ) == -1)
                continue;

            String leafname = varname.substring( varname.lastIndexOf( '.' ) + 1 );
            if (aliases.containsKey( leafname ))
                continue;
            aliases.put( leafname, varname );
        }

        return aliases;
    }

    private static boolean isBoolean( ConfigurationBuffer buffer, String var )
    {
        ConfigurationInfo info = buffer.getInfo( var );

        if (info.getArgCount() > 1)
            return false;

        Class c = info.getArgType( 0 );

        return ((c == boolean.class) || (c == Boolean.class));
    }

    public static String brief( String program, String defaultvar, LocalizationManager l10n, String l10nPrefix )
    {
        Map<String, String> params = new HashMap<String, String>();
        params.put( "defaultVar", defaultvar );
        params.put( "program", program );
        return l10n.getLocalizedTextString( l10nPrefix + ".Brief", params );
    }

    static public String usage( String program, String defaultVar, ConfigurationBuffer cfgbuf, Set<String> keywords, LocalizationManager lmgr, String l10nPrefix )
    {
        // FIXME (probably a FOL, unfortunately) - this is totally biased to western languages.

        Map<String, String> aliases = getAliases( cfgbuf );

        Map<String, String> sesaila = new HashMap<String, String>();
        for (Iterator<Map.Entry<String, String>> it = aliases.entrySet().iterator(); it.hasNext();)
        {
            Map.Entry<String, String> e = it.next();
            sesaila.put( e.getValue(), e.getKey() );
        }

        TreeSet<String> printSet = new TreeSet<String>();

        boolean all = false;
        boolean advanced = false;
        boolean details = false;
        boolean syntax = false;
        boolean printaliases = false;

        // figure out behavior..
        Set<String> newSet = new HashSet<String>();
        for (Iterator<String> kit = keywords.iterator(); kit.hasNext();)
        {
            String keyword = kit.next();

            if (keyword.equals( "list" ))
            {
                all = true;
                newSet.add( "*" );
            }
            else if (keyword.equals( "advanced" ))
            {
                advanced = true;
                if (keywords.size() == 1)
                {
                    all = true;
                    newSet.add( "*" );
                }
            }
            else if (keyword.equals( "details" ))
            {
                details = true;
            }
            else if (keyword.equals( "syntax" ))
            {
                syntax = true;
            }
            else if (keyword.equals( "aliases" ))
            {
                printaliases = true;
            }
            else
            {
                details = true;
                newSet.add( keyword );
            }
        }
        if (syntax)
        {
            List lines = ConfigurationBuffer.formatText( getSyntaxDescription( program, defaultVar, advanced, lmgr,  l10nPrefix ), 78 );
            StringBuilder text = new StringBuilder( 512 );
            for (Iterator it = lines.iterator(); it.hasNext();)
            {
                text.append( it.next() );
                text.append( "\n" );
            }
            return text.toString();
        }
        keywords = newSet;

        // accumulate set to print
        for (Iterator<String> kit = keywords.iterator(); kit.hasNext();)
        {
            String keyword = kit.next().toLowerCase();

            for (Iterator varit = cfgbuf.getVarIterator(); varit.hasNext(); )
            {
                String var = (String) varit.next();
                ConfigurationInfo info = cfgbuf.getInfo( var );

                String description = getDescription( cfgbuf, var, lmgr, l10nPrefix);

                if ((all
                        || (var.indexOf( keyword ) != -1)
                        || ((description != null) && (description.toLowerCase().indexOf( keyword ) != -1))
                        || (keyword.matches( var ) )
                        || ((sesaila.get( var ) != null) && (sesaila.get( var )).indexOf( keyword ) != -1))
                     && (!info.isHidden())
                     && (advanced || !info.isAdvanced()))
                {
                    if (printaliases && sesaila.containsKey( var ))
                        printSet.add( sesaila.get( var ));
                    else
                        printSet.add( var );
                }
                else
                {
                    /*
                    for (int i = 0; i < info.getAliases().length; ++i)
                    {
                        String alias = info.getAliases()[i];
                        if (alias.indexOf( keyword ) != -1)
                        {
                            printSet.add( var );
                        }
                    }
                    */
                }
            }
        }

        StringBuilder output = new StringBuilder( 1024 );

        if (printSet.size() == 0)
        {
            String nkm = lmgr.getLocalizedTextString( l10nPrefix + ".NoKeywordsMatched" );
            output.append( nkm );
            output.append( "\n" );
        }
        else for (Iterator<String> it = printSet.iterator(); it.hasNext();)
        {
            String avar = it.next();
            String var = avar;
            if (aliases.containsKey( avar ))
                var = aliases.get( avar );

            ConfigurationInfo info = cfgbuf.getInfo( var );
            assert info != null;

            output.append( "-" );
            output.append( avar );

            int count = cfgbuf.getVarArgCount( var );
            if ((count >= 1) && (!isBoolean( cfgbuf, var )))
            {
                for (int i = 0; i < count; ++i)
                {
                    output.append( " <" );
                    output.append( cfgbuf.getVarArgName( var, i ) );
                    output.append( ">" );
                }
            }
            else if (count == -1)
            {
                String last = "";
                for (int i = 0; i < 5; ++i)
                {
                    String argname = cfgbuf.getVarArgName( var, i );
                    if (!argname.equals( last ))
                    {
                        output.append( " [" );
                        output.append( argname );
                        output.append( "]" );
                        last = argname;
                    }
                    else
                    {
                        output.append( " [...]" );
                        break;
                    }
                }
            }

            output.append( "\n" );

            if (details)
            {
                StringBuilder description = new StringBuilder( 160 );
                if (printaliases)
                {
                    if (aliases.containsKey( avar ))
                    {
                        String fullname = lmgr.getLocalizedTextString( l10nPrefix + ".FullName" );
                        description.append( fullname );
                        description.append( " -" );
                        description.append( aliases.get( avar ));
                        description.append( "\n" );
                    }
                }
                else if (sesaila.containsKey( var ))
                {
                    String alias = lmgr.getLocalizedTextString( l10nPrefix + ".Alias" );
                    description.append( alias );
                    description.append( " -" );
                    description.append( sesaila.get( var ));
                    description.append( "\n" );
                }

                String d = getDescription(cfgbuf, var, lmgr, l10nPrefix);
                if (var.equals( "help" ) && (printSet.size() > 2))
                {
                    String helpKeywords = lmgr.getLocalizedTextString( l10nPrefix + ".HelpKeywords" );
                    description.append( helpKeywords );
                }
                else if (d != null)
                    description.append( d );

                String flags = "";
                if (info.isAdvanced())
                {
                    String advancedString = lmgr.getLocalizedTextString( l10nPrefix + ".Advanced" );
                    flags += (((flags.length() == 0)? " (" : ", ") + advancedString );
                }
                if (info.allowMultiple())
                {
                    String repeatableString = lmgr.getLocalizedTextString( l10nPrefix + ".Repeatable" );
                    flags += (((flags.length() == 0)? " (" : ", ") + repeatableString );
                }
                if ((defaultVar != null) && var.equals( defaultVar ))
                {
                    String defaultString = lmgr.getLocalizedTextString( l10nPrefix + ".Default" );
                    flags += (((flags.length() == 0)? " (" : ", ") + defaultString );
                }
                if (flags.length() != 0)
                {
                    flags += ")";
                }
                description.append( flags );


                List descriptionLines = ConfigurationBuffer.formatText( description.toString(), 70 );

                for (Iterator descit = descriptionLines.iterator(); descit.hasNext();)
                {
                    output.append( "    " );
                    output.append( (String) descit.next() );
                    output.append( "\n" );
                }
            }
        }
        return output.toString();
    }

    public static String getDescription( ConfigurationBuffer buffer, String var, LocalizationManager l10n, String l10nPrefix )
    {
        String key = (l10nPrefix == null)? var : (l10nPrefix + "." + var);
        String description = l10n.getLocalizedTextString( key, null );

        return description;
    }

    public static String getSyntaxDescription( String program, String defaultVar, boolean advanced, LocalizationManager l10n, String l10nPrefix )
    {
        Map<String, String> params = new HashMap<String, String>();
        params.put("defaultVar", defaultVar);
        params.put("program", program);

        String key = l10nPrefix + "." + (advanced? "AdvancedSyntax" : "Syntax");
        String text = l10n.getLocalizedTextString( key, params );

        if (text == null)
        {
            text = "No syntax help available, try '-help list' to list available configuration variables.";
            assert false : "Localized text for syntax description not found!";
        }
        return text;
    }
    
    public static final String source = SOURCE_COMMAND_LINE;
}
