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

import com.google.common.base.Joiner;

import org.apache.royale.compiler.Messages;
import org.apache.royale.compiler.exceptions.ConfigurationException;
import org.apache.royale.compiler.internal.config.localization.LocalizationManager;

/**
 * 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.
 */
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
     */
    public static void parse(final ConfigurationBuffer buffer,
                              final String defaultvar,
                              final String[] args)
            throws ConfigurationException
    {
        // "no-default-arg" means the application does not have a default var.
        assert defaultvar == null || buffer.isValidVar(defaultvar) || "no-default-arg".equals(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))
                            {
                                varArgCount = -1;
                                mode = EXEC;
                            }
                            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);
                    }

                    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.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 (final String varname : buffer.getVars())
        {
            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, Object> params = new HashMap<String, Object>();
        params.put("defaultVar", defaultvar);
        params.put("program", program);
        return l10n.getLocalizedTextString(l10nPrefix + ".Brief", params);
    }

    public static String usage(String program, String defaultVar, ConfigurationBuffer cfgbuf, Set<String> keywords, LocalizationManager lmgr, String l10nPrefix)
    {
        boolean isCompc = program.contains("compc");
        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)
        {
            final List<String> lines = ConfigurationBuffer.formatText(getSyntaxDescription(program, defaultVar, advanced, lmgr, l10nPrefix), 78);
            return Joiner.on("\n").join(lines);
        }
        keywords = newSet;

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

            for (final String var : cfgbuf.getVars())
            {
                ConfigurationInfo info = cfgbuf.getInfo(var);
                
                // If the client is not "compc", skip "compc-only" options.
                if (info.isCompcOnly && !isCompc)
                    continue;
                
                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())
                     && (!info.isRemoved())
                     && (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 (info.isRoyaleOnly())
                    {
                        String royaleOnlylString = Messages.getString("RoyaleOnly");
                        flags += (((flags.length() == 0) ? " (" : ", ") + royaleOnlylString);
                    }
                    if (flags.length() != 0)
                    {
                        flags += ")";
                    }
                    description.append(flags);

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

                    for (final String next : descriptionLines)
                    {
                        output.append("    ");
                        output.append(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, Object> params = new HashMap<String, Object>();
        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;
}
