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

import java.io.Reader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.TreeMap;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.IOUtils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.flex.compiler.config.ConfigurationBuffer;
import org.apache.flex.compiler.config.ConfigurationInfo;
import org.apache.flex.compiler.config.ConfigurationValue;
import org.apache.flex.compiler.exceptions.ConfigurationException;
import org.apache.flex.compiler.filespecs.IFileSpecification;
import org.apache.flex.compiler.internal.config.localization.LocalizationManager;
import com.google.common.collect.ImmutableSet;

/**
 * A utility class, which is used to parse an XML file of configuration options
 * and populate a ConfigurationBuffer. A counterpart of CommandLineConfigurator
 * and SystemPropertyConfigurator.
 * 
 * @see <a href="http://help.adobe.com/en_US/flex/using/WS2db454920e96a9e51e63e3d11c0bf67670-7ff2.html">Configuration file syntax</a>
 */
public class FileConfigurator
{

    public static class SAXConfigurationException extends SAXParseException
    {
        private static final long serialVersionUID = -3388781933743434302L;

        SAXConfigurationException(ConfigurationException e, Locator locator)
        {
            super(null, locator); // ?
            this.innerException = e;
        }

        public ConfigurationException innerException;
    }

    /**
     * Load configuration XML file into a {@link ConfigurationBuffer} object.
     * 
     * @param buffer result {@link ConfigurationBuffer} object.
     * @param fileSpec configuration XML file.
     * @param context path context used for resolving relative paths in the
     * configuration options.
     * @param rootElement expected root element of the XML DOM tree.
     * @param ignoreUnknownItems if false, unknown option will cause exception.
     * @throws ConfigurationException error.
     */
    public static void load(
            final ConfigurationBuffer buffer,
            final IFileSpecification fileSpec,
            final String context,
            final String rootElement,
            boolean ignoreUnknownItems)
            throws ConfigurationException
    {
        final String path = fileSpec.getPath();
        final Handler h = new Handler(buffer, path, context, rootElement, ignoreUnknownItems);
        final SAXParserFactory factory = SAXParserFactory.newInstance();
        Reader reader = null;
        try
        {
            reader = fileSpec.createReader();
            final SAXParser parser = factory.newSAXParser();
            final InputSource source = new InputSource(reader);
            parser.parse(source, h);
        }
        catch (SAXConfigurationException e)
        {
            throw e.innerException;
        }
        catch (SAXParseException e)
        {
            throw new ConfigurationException.OtherThrowable(e, null, path, e.getLineNumber());
        }
        catch (Exception e)
        {
            throw new ConfigurationException.OtherThrowable(e, null, path, -1);
        }
        finally
        {
            IOUtils.closeQuietly(reader);
        }
    }

    /**
     * SAX handler for configuration XML.
     */
    private static class Handler extends DefaultHandler
    {
        private static final String ATTRIBUTE_APPEND = "append";

        public Handler(ConfigurationBuffer buffer,
                       String source,
                       String contextPath,
                       String rootElement,
                       boolean ignoreUnknownItems)
        {
            this.cfgbuf = buffer;
            this.source = source;
            this.contextPath = contextPath;
            this.rootElement = rootElement;
            this.ignoreUnknownItems = ignoreUnknownItems;
        }

        private final Stack<ParseContext> contextStack = new Stack<ParseContext>();
        private final ConfigurationBuffer cfgbuf;
        private final String source;
        private final String contextPath;
        private final String rootElement;
        private final boolean ignoreUnknownItems;
        private final StringBuilder text = new StringBuilder();
        private Locator locator;

        @Override
        public void startElement(final String uri, final String localName, final String qname, final Attributes attributes) throws SAXException
        {
            // Verify and initialize the context stack at root element.
            if (contextStack.size() == 0)
            {
                if (!qname.equals(rootElement))
                {
                    throw new SAXConfigurationException(
                            new ConfigurationException.IncorrectElement(rootElement, qname, this.source, locator.getLineNumber()),
                            locator);
                }
                final ParseContext ctx = new ParseContext();
                contextStack.push(ctx);
                return;
            }

            final ParseContext ctx = contextStack.peek();

            if (ctx.ignore)
            {
                // ignore starting new elements
                return;
            }

            if (text.length() > 0)
            {
                // Only leave nodes can have CDATA as option values.
                throw new SAXConfigurationException(
                        new ConfigurationException.UnexpectedCDATA(this.source, locator.getLineNumber()),
                        locator);
            }

            final String fullname = ConfigurationBuffer.varname(qname, ctx.base);

            if (ctx.item != null)
            {
                throw new SAXConfigurationException(
                        new ConfigurationException.UnexpectedElement(qname, contextPath, locator.getLineNumber()),
                        locator);
            }
            else if (ctx.var != null)
            {
                // we're setting values for a variable

                if (ctx.varArgCount == 1)
                {
                    // oops, we weren't expecting more than one value!

                    throw new SAXConfigurationException(
                            new ConfigurationException.UnexpectedElement(qname, source, locator.getLineNumber()),
                            locator);
                }
                ctx.item = qname;
            }
            else if (cfgbuf.isValidVar(fullname))
            {
                ctx.var = fullname;
                ctx.varArgCount = cfgbuf.getVarArgCount(ctx.var);
                ctx.append = false;
                final String append = attributes.getValue(ATTRIBUTE_APPEND);
                if (append != null)
                {
                    if (append.equalsIgnoreCase("true") || append.equalsIgnoreCase("false"))
                        ctx.append = Boolean.valueOf(append).booleanValue();
                    else
                        throw new SAXConfigurationException(
                                new ConfigurationException.BadAppendValue(
                                        ctx.var,
                                        source,
                                        locator.getLineNumber()),
                                locator);
                }
            }
            else if (isSubTree(fullname))
            {
                final ParseContext newctx = new ParseContext();
                newctx.base = fullname;
                contextStack.push(newctx);
            }
            else
            {
                if (ignoreUnknownItems)
                {
                    // push a new context and ignore everything until we get the end 
                    // of this element.
                    ParseContext newctx = new ParseContext();
                    newctx.item = qname;
                    newctx.ignore = true;
                    contextStack.push(newctx);
                    return;
                }
                System.err.println("Unknown tag:" + fullname);
                throw new SAXConfigurationException(
                        new ConfigurationException.UnknownVariable(
                                                    fullname, source, locator.getLineNumber()),
                        locator);
            }
        }

        @Override
        public void endElement(String uri, String localName, String qname) throws SAXException
        {
            final ParseContext ctx = contextStack.peek();

            if (ctx.ignore)
            {
                // if found the matching end element, then pop the context and stop ignoring input
                if (ctx.item.equals(qname))
                {
                    contextStack.pop();
                    text.setLength(0); // ignore any text read
                }

                return;
            }

            // There are four possible states here;
            // 1. localname==rootElement -> end of file, pop, we're done
            // 2. localname==itemElement -> finished gathering text, push onto arglist
            // 2. var is set -> set the var to the argList, pop
            // 3. var is null -> we're finishing a child config, pop

            if (qname.equals(rootElement))
            {
                // Finished with the file!
            }
            else if (ctx.item != null)
            {
                // Finished with the current item.
                final ParseValue v = new ParseValue();
                v.name = qname;
                v.value = text.toString();
                v.line = locator.getLineNumber();
                ctx.argList.add(v);
                text.setLength(0);
                ctx.item = null;
            }
            else if (ctx.var != null)
            {
                if ((ctx.varArgCount > 1) && (ctx.argList.size() == 0))
                {
                    throw new SAXConfigurationException(
                            new ConfigurationException.IncorrectArgumentCount(ctx.varArgCount, 0,
                                                                               ctx.var, source, locator.getLineNumber()),
                            locator);
                }
                if (ctx.varArgCount == 1)
                {
                    ParseValue v = new ParseValue();
                    v.name = null;
                    v.value = text.toString();
                    v.line = locator.getLineNumber();
                    ctx.argList.add(v);
                    text.setLength(0);
                }
                else
                {
                    if (text.length() > 0)
                    {
                        // "unexpected CDATA encountered, " + ctx.var + " requires named arguments.", locator );
                        throw new SAXConfigurationException(
                                new ConfigurationException.UnexpectedCDATA(source, locator.getLineNumber()),
                                locator);

                    }
                }
                // Finished with the current var, save the current list
                try
                {
                    setVar(ctx.var, ctx.argList, locator.getLineNumber(), ctx.append);
                    ctx.var = null;
                    ctx.argList.clear();
                    ctx.item = null;
                    ctx.append = false;
                }
                catch (ConfigurationException e)
                {
                    throw new SAXConfigurationException(e, locator);
                }
            }
            else
            {
                // done with a child config
                contextStack.pop();
            }
        }

        public void setVar(String var, List<ParseValue> argList, int line, boolean append) throws ConfigurationException
        {
            int varArgCount = cfgbuf.getVarArgCount(var);

            Map<String, String> items = new HashMap<String, String>();

            boolean byName = (varArgCount > 1);

            if (byName)
            {
                for (Iterator<ParseValue> it = argList.iterator(); it.hasNext();)
                {
                    ParseValue v = it.next();

                    if (items.containsKey(v.name))
                    {
                        byName = false; // can't support byName, duplicate item name!
                        break;
                    }
                    else
                    {
                        items.put(v.name, v.value);
                    }
                }
            }
            List<String> args = new LinkedList<String>();

            if (byName)
            {
                int argc = 0;

                while (args.size() < items.size())
                {
                    String name = cfgbuf.getVarArgName(var, argc++);
                    String val = items.get(name);
                    if (val == null)
                    {
                        throw new ConfigurationException.MissingArgument(name, var, source, line);
                    }
                    args.add(val);
                }
            }
            else
            {
                Iterator<ParseValue> it = argList.iterator();
                int argc = 0;
                while (it.hasNext())
                {
                    ParseValue v = it.next();
                    String name = cfgbuf.getVarArgName(var, argc++);
                    if ((v.name != null) && !name.equals(v.name))
                    {
                        throw new ConfigurationException.UnexpectedArgument(name, v.name, var, source, v.line);
                    }
                    args.add(v.value);
                }
            }
            cfgbuf.setVar(var, args, source, line, contextPath, append);
        }

        @Override
        public void characters(char ch[], int start, int length)
        {
            String chars = new String(ch, start, length).trim();
            text.append(chars);
        }

        @Override
        public void setDocumentLocator(Locator locator)
        {
            this.locator = locator;
        }
    }

    private static class ParseContext
    {
        ParseContext()
        {
            this.base = null;
            this.var = null;
            this.varArgCount = -2;
            this.argList = new LinkedList<ParseValue>();
            this.append = false;
            this.ignore = false;
        }

        public String var;
        public String base;
        public String item;
        public int varArgCount;
        public boolean append;
        public List<ParseValue> argList;
        public boolean ignore; // ignore this variable, do not put in config buffer
    }

    private static class ParseValue
    {
        public String name;
        public String value;
        public int line;
    }

    private static class FormatNode
    {
        public String fullname;
        public String shortname;
        public ConfigurationInfo info;
        public List<ConfigurationValue> values;

        public TreeMap<String, FormatNode> children; // only for configs
    }

    static final String pad = "   ";

    /**
     * These XML nodes can have subtrees of configurations.
     */
    protected static final ImmutableSet<String> VALID_SUBTREE_TAG = ImmutableSet.of(
            "compiler",
            "compiler.namespaces",
            "compiler.fonts",
            "compiler.fonts.languages",
            "compiler.mxml",
            "compiler.mxml.imports",
            "metadata",
            "licenses",
            "frames",
            "runtime-shared-library-settings");

    /**
     * @param fullname
     * @return
     */
    private static boolean isSubTree(String fullname)
    {
        return VALID_SUBTREE_TAG.contains(fullname);
    }

    private static 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();
    }

    private static String formatBuffer1(ConfigurationBuffer cfgbuf,
                                         FormatNode node,
                                         String indent,
                                         LocalizationManager lmgr,
                                         String prefix)
    {
        StringBuilder buf = new StringBuilder(1024);

        buf.append(indent + "<" + node.shortname + ">\n");
        if (node.children != null)
        {
            for (final String key : node.children.keySet())
            {
                final FormatNode child = node.children.get(key);

                if (child.children != null) // its a config
                {
                    buf.append(formatBuffer1(cfgbuf, child, indent + pad, lmgr, prefix));
                }
                else
                {
                    String description = lmgr.getLocalizedTextString(prefix + "." + child.fullname);

                    if (description != null)
                        buf.append(indent + pad + "<!-- " + child.fullname + ": " + description + "-->\n");

                    if ((child.values == null) || !child.info.isDisplayed())
                    {
                        boolean newline = false;
                        buf.append(indent + pad + "<!-- " + child.fullname + " usage:\n");
                        buf.append(indent + pad + "<" + child.shortname + ">");

                        int i = 0;
                        while (true)
                        {
                            if (child.info.getArgCount() == 1)
                            {
                                buf.append(child.info.getArgName(i));
                                break;
                            }
                            else
                            {
                                buf.append("\n" + indent + pad + pad + "<" + child.info.getArgName(i) + ">" + classToArgName(child.info.getArgType(i)) + "</" + child.info.getArgName(i) + ">");
                                newline = true;
                            }
                            if (child.info.getArgCount() == -1)
                            {
                                if (i > 0)
                                {
                                    // stop iterating thru arguments when an arg name
                                    // matches a previously used arg name.
                                    boolean found = false; // true if found argName in the arg list
                                    String argName = child.info.getArgName(i + 1);
                                    for (int j = i; j >= 0; j--)
                                    {
                                        if (child.info.getArgName(j).equals(argName))
                                        {
                                            found = true;
                                            break;
                                        }
                                    }
                                    if (found)
                                    {
                                        break;
                                    }
                                }
                            }
                            else if (i >= child.info.getArgCount())
                            {
                                break;
                            }
                            ++i;
                        }
                        if (newline)
                            buf.append("\n" + indent + pad);

                        buf.append("</" + child.shortname + ">\n");
                        buf.append(indent + pad + "-->\n");
                    }
                    else
                    {
                        // var may be set multiple times...
                        boolean newline = false;
                        for (final ConfigurationValue cv : child.values)
                        {
                            buf.append(indent + pad + "<" + child.shortname + ">");

                            int argCount = child.info.getArgCount();
                            // var may have multiple values...
                            int argc = 0;
                            for (final String arg : cv.getArgs())
                            {
                                if (argCount == 1)
                                {
                                    buf.append(arg);
                                    break;
                                }
                                else
                                {
                                    String argname = child.info.getArgName(argc++);
                                    newline = true;
                                    buf.append("\n" + indent + pad + pad + "<" + argname + ">" + arg + "</" + argname + ">");
                                }
                            }
                            if (newline)
                                buf.append("\n" + indent + pad);
                            buf.append("</" + child.shortname + ">\n");
                        }
                    }
                }
            }
        }
        buf.append(indent + "</" + node.shortname + ">\n");

        return buf.toString();
    }

    private static void addNode(ConfigurationBuffer cfgbuf, String var, FormatNode root)
    {
        String name = null;
        StringTokenizer t = new StringTokenizer(var, ".");

        FormatNode current = root;

        while (t.hasMoreTokens())
        {
            String token = t.nextToken();

            if (name == null)
                name = token;
            else
                name += "." + token;

            if (current.children == null)
                current.children = new TreeMap<String, FormatNode>();

            if (isSubTree(name))
            {
                if (!current.children.containsKey(token))
                {
                    FormatNode node = new FormatNode();
                    node.fullname = name;
                    node.shortname = token;
                    node.children = new TreeMap<String, FormatNode>();
                    current.children.put(token, node);
                    current = node;
                }
                else
                {
                    current = current.children.get(token);
                }
            }
            else if (cfgbuf.isValidVar(name))
            {
                FormatNode node = new FormatNode();
                node.fullname = name;
                node.shortname = token;
                node.info = cfgbuf.getInfo(name);
                node.values = cfgbuf.getVar(name);
                current.children.put(token, node);
            }
        }
    }

    public static String formatBuffer(ConfigurationBuffer cfgbuf,
                                       String rootElement,
                                       LocalizationManager lmgr,
                                       String prefix)
    {
        FormatNode root = new FormatNode();
        root.shortname = rootElement;
        for (final String var : cfgbuf.getVars())
        {
            // if var is a 'hidden' or a 'removed' parameter, don't dump.
            ConfigurationInfo info = cfgbuf.getInfo(var);
            if (info != null && (info.isHidden() || info.isRemoved() || !info.isDisplayed()))
            {
                continue;
            }
            addNode(cfgbuf, var, root);
        }

        return formatBuffer1(cfgbuf, root, "", lmgr, prefix);
    }

    public static String formatBuffer(ConfigurationBuffer cfgbuf, String rootElement)
    {
        return formatBuffer(cfgbuf, rootElement, null, null);
    }
}
