/**
 *   Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com>
 */
package com.typesafe.config.impl;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import com.typesafe.config.*;

final class ConfigParser {
    static AbstractConfigValue parse(ConfigNodeRoot document,
                                     ConfigOrigin origin, ConfigParseOptions options,
                                     ConfigIncludeContext includeContext) {
        ParseContext context = new ParseContext(options.getSyntax(), origin, document,
                SimpleIncluder.makeFull(options.getIncluder()), includeContext);
        return context.parse();
    }

    static private final class ParseContext {
        private int lineNumber;
        final private ConfigNodeRoot document;
        final private FullIncluder includer;
        final private ConfigIncludeContext includeContext;
        final private ConfigSyntax flavor;
        final private ConfigOrigin baseOrigin;
        final private LinkedList<Path> pathStack;

        // the number of lists we are inside; this is used to detect the "cannot
        // generate a reference to a list element" problem, and once we fix that
        // problem we should be able to get rid of this variable.
        int arrayCount;

        ParseContext(ConfigSyntax flavor, ConfigOrigin origin, ConfigNodeRoot document,
                FullIncluder includer, ConfigIncludeContext includeContext) {
            lineNumber = 1;
            this.document = document;
            this.flavor = flavor;
            this.baseOrigin = origin;
            this.includer = includer;
            this.includeContext = includeContext;
            this.pathStack = new LinkedList<Path>();
            this.arrayCount = 0;
        }

        // merge a bunch of adjacent values into one
        // value; change unquoted text into a string
        // value.
        private AbstractConfigValue parseConcatenation(ConfigNodeConcatenation n) {
            // this trick is not done in JSON
            if (flavor == ConfigSyntax.JSON)
                throw new ConfigException.BugOrBroken("Found a concatenation node in JSON");

            List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>(n.children().size());

            for (AbstractConfigNode node : n.children()) {
                AbstractConfigValue v = null;
                if (node instanceof AbstractConfigNodeValue) {
                    v = parseValue((AbstractConfigNodeValue)node, null);
                    values.add(v);
                }
            }

            return ConfigConcatenation.concatenate(values);
        }

        private SimpleConfigOrigin lineOrigin() {
            return ((SimpleConfigOrigin) baseOrigin).withLineNumber(lineNumber);
        }

        private ConfigException parseError(String message) {
            return parseError(message, null);
        }

        private ConfigException parseError(String message, Throwable cause) {
            return new ConfigException.Parse(lineOrigin(), message, cause);
        }

        private Path fullCurrentPath() {
            // pathStack has top of stack at front
            if (pathStack.isEmpty())
                throw new ConfigException.BugOrBroken("Bug in parser; tried to get current path when at root");
            else
                return new Path(pathStack.descendingIterator());
        }

        private AbstractConfigValue parseValue(AbstractConfigNodeValue n, List<String> comments) {
            AbstractConfigValue v;

            int startingArrayCount = arrayCount;

            if (n instanceof ConfigNodeSimpleValue) {
                v = ((ConfigNodeSimpleValue) n).value();
            } else if (n instanceof ConfigNodeObject) {
                v = parseObject((ConfigNodeObject)n);
            } else if (n instanceof ConfigNodeArray) {
                v = parseArray((ConfigNodeArray)n);
            } else if (n instanceof ConfigNodeConcatenation) {
                v = parseConcatenation((ConfigNodeConcatenation)n);
            } else {
                throw parseError("Expecting a value but got wrong node type: " + n.getClass());
            }

            if (comments != null && !comments.isEmpty()) {
                v = v.withOrigin(v.origin().prependComments(new ArrayList<String>(comments)));
                comments.clear();
            }

            if (arrayCount != startingArrayCount)
                throw new ConfigException.BugOrBroken("Bug in config parser: unbalanced array count");

            return v;
        }

        private static AbstractConfigObject createValueUnderPath(Path path,
                AbstractConfigValue value) {
            // for path foo.bar, we are creating
            // { "foo" : { "bar" : value } }
            List<String> keys = new ArrayList<String>();

            String key = path.first();
            Path remaining = path.remainder();
            while (key != null) {
                keys.add(key);
                if (remaining == null) {
                    break;
                } else {
                    key = remaining.first();
                    remaining = remaining.remainder();
                }
            }

            // the withComments(null) is to ensure comments are only
            // on the exact leaf node they apply to.
            // a comment before "foo.bar" applies to the full setting
            // "foo.bar" not also to "foo"
            ListIterator<String> i = keys.listIterator(keys.size());
            String deepest = i.previous();
            AbstractConfigObject o = new SimpleConfigObject(value.origin().withComments(null),
                    Collections.<String, AbstractConfigValue> singletonMap(
                            deepest, value));
            while (i.hasPrevious()) {
                Map<String, AbstractConfigValue> m = Collections.<String, AbstractConfigValue> singletonMap(
                        i.previous(), o);
                o = new SimpleConfigObject(value.origin().withComments(null), m);
            }

            return o;
        }

        private void parseInclude(Map<String, AbstractConfigValue> values, ConfigNodeInclude n) {
            boolean isRequired = n.isRequired();
            ConfigIncludeContext cic = includeContext.setParseOptions(includeContext.parseOptions().setAllowMissing(!isRequired));

            AbstractConfigObject obj;
            switch (n.kind()) {
                case URL:
                    URL url;
                    try {
                        url = new URL(n.name());
                    } catch (MalformedURLException e) {
                        throw parseError("include url() specifies an invalid URL: " + n.name(), e);
                    }
                    obj = (AbstractConfigObject) includer.includeURL(cic, url);
                    break;

                case FILE:
                    obj = (AbstractConfigObject) includer.includeFile(cic,
                            new File(n.name()));
                    break;

                case CLASSPATH:
                    obj = (AbstractConfigObject) includer.includeResources(cic, n.name());
                    break;

                case HEURISTIC:
                    obj = (AbstractConfigObject) includer
                            .include(cic, n.name());
                    break;

                default:
                    throw new ConfigException.BugOrBroken("should not be reached");
            }

            // we really should make this work, but for now throwing an
            // exception is better than producing an incorrect result.
            // See https://github.com/lightbend/config/issues/160
            if (arrayCount > 0 && obj.resolveStatus() != ResolveStatus.RESOLVED)
                throw parseError("Due to current limitations of the config parser, when an include statement is nested inside a list value, "
                        + "${} substitutions inside the included file cannot be resolved correctly. Either move the include outside of the list value or "
                        + "remove the ${} statements from the included file.");

            if (!pathStack.isEmpty()) {
                Path prefix = fullCurrentPath();
                obj = obj.relativized(prefix);
            }

            for (String key : obj.keySet()) {
                AbstractConfigValue v = obj.get(key);
                AbstractConfigValue existing = values.get(key);
                if (existing != null) {
                    values.put(key, v.withFallback(existing));
                } else {
                    values.put(key, v);
                }
            }
        }

        private AbstractConfigObject parseObject(ConfigNodeObject n) {
            Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
            SimpleConfigOrigin objectOrigin = lineOrigin();
            boolean lastWasNewline = false;

            ArrayList<AbstractConfigNode> nodes = new ArrayList<AbstractConfigNode>(n.children());
            List<String> comments = new ArrayList<String>();
            for (int i = 0; i < nodes.size(); i++) {
                AbstractConfigNode node = nodes.get(i);
                if (node instanceof ConfigNodeComment) {
                    lastWasNewline = false;
                    comments.add(((ConfigNodeComment) node).commentText());
                } else if (node instanceof ConfigNodeSingleToken && Tokens.isNewline(((ConfigNodeSingleToken) node).token())) {
                    lineNumber++;
                    if (lastWasNewline) {
                        // Drop all comments if there was a blank line and start a new comment block
                        comments.clear();
                    }
                    lastWasNewline = true;
                } else if (flavor != ConfigSyntax.JSON && node instanceof ConfigNodeInclude) {
                    parseInclude(values, (ConfigNodeInclude)node);
                    lastWasNewline = false;
                } else if (node instanceof ConfigNodeField) {
                    lastWasNewline = false;
                    Path path = ((ConfigNodeField) node).path().value();
                    comments.addAll(((ConfigNodeField) node).comments());

                    // path must be on-stack while we parse the value
                    pathStack.push(path);
                    if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {
                        // we really should make this work, but for now throwing
                        // an exception is better than producing an incorrect
                        // result. See
                        // https://github.com/lightbend/config/issues/160
                        if (arrayCount > 0)
                            throw parseError("Due to current limitations of the config parser, += does not work nested inside a list. "
                                    + "+= expands to a ${} substitution and the path in ${} cannot currently refer to list elements. "
                                    + "You might be able to move the += outside of the list and then refer to it from inside the list with ${}.");

                        // because we will put it in an array after the fact so
                        // we want this to be incremented during the parseValue
                        // below in order to throw the above exception.
                        arrayCount += 1;
                    }

                    AbstractConfigNodeValue valueNode;
                    AbstractConfigValue newValue;

                    valueNode = ((ConfigNodeField) node).value();

                    // comments from the key token go to the value token
                    newValue = parseValue(valueNode, comments);

                    if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {
                        arrayCount -= 1;

                        List<AbstractConfigValue> concat = new ArrayList<AbstractConfigValue>(2);
                        AbstractConfigValue previousRef = new ConfigReference(newValue.origin(),
                                new SubstitutionExpression(fullCurrentPath(), true /* optional */));
                        AbstractConfigValue list = new SimpleConfigList(newValue.origin(),
                                Collections.singletonList(newValue));
                        concat.add(previousRef);
                        concat.add(list);
                        newValue = ConfigConcatenation.concatenate(concat);
                    }

                    // Grab any trailing comments on the same line
                    if (i < nodes.size() - 1) {
                        i++;
                        while (i < nodes.size()) {
                            if (nodes.get(i) instanceof ConfigNodeComment) {
                                ConfigNodeComment comment = (ConfigNodeComment) nodes.get(i);
                                newValue = newValue.withOrigin(newValue.origin().appendComments(
                                            Collections.singletonList(comment.commentText())));
                                break;
                            } else if (nodes.get(i) instanceof ConfigNodeSingleToken) {
                                ConfigNodeSingleToken curr = (ConfigNodeSingleToken) nodes.get(i);
                                if (curr.token() == Tokens.COMMA || Tokens.isIgnoredWhitespace(curr.token())) {
                                    // keep searching, as there could still be a comment
                                } else {
                                    i--;
                                    break;
                                }
                            } else {
                                i--;
                                break;
                            }
                            i++;
                        }
                    }

                    pathStack.pop();

                    String key = path.first();
                    Path remaining = path.remainder();

                    if (remaining == null) {
                        AbstractConfigValue existing = values.get(key);
                        if (existing != null) {
                            // In strict JSON, dups should be an error; while in
                            // our custom config language, they should be merged
                            // if the value is an object (or substitution that
                            // could become an object).

                            if (flavor == ConfigSyntax.JSON) {
                                throw parseError("JSON does not allow duplicate fields: '"
                                    + key
                                    + "' was already seen at "
                                    + existing.origin().description());
                            } else {
                                newValue = newValue.withFallback(existing);
                            }
                        }
                        values.put(key, newValue);
                    } else {
                        if (flavor == ConfigSyntax.JSON) {
                            throw new ConfigException.BugOrBroken(
                                    "somehow got multi-element path in JSON mode");
                        }

                        AbstractConfigObject obj = createValueUnderPath(
                                remaining, newValue);
                        AbstractConfigValue existing = values.get(key);
                        if (existing != null) {
                            obj = obj.withFallback(existing);
                        }
                        values.put(key, obj);
                    }
                }
            }

            return new SimpleConfigObject(objectOrigin, values);
        }

        private SimpleConfigList parseArray(ConfigNodeArray n) {
            arrayCount += 1;

            SimpleConfigOrigin arrayOrigin = lineOrigin();
            List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();

            boolean lastWasNewLine = false;
            List<String> comments = new ArrayList<String>();

            AbstractConfigValue v = null;

            for (AbstractConfigNode node : n.children()) {
                if (node instanceof ConfigNodeComment) {
                    comments.add(((ConfigNodeComment) node).commentText());
                    lastWasNewLine = false;
                } else if (node instanceof ConfigNodeSingleToken && Tokens.isNewline(((ConfigNodeSingleToken) node).token())) {
                    lineNumber++;
                    if (lastWasNewLine && v == null) {
                        comments.clear();
                    } else if (v != null) {
                        values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
                        comments.clear();
                        v = null;
                    }
                    lastWasNewLine = true;
                } else if (node instanceof AbstractConfigNodeValue) {
                    lastWasNewLine = false;
                    if (v != null) {
                        values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
                        comments.clear();
                    }
                    v = parseValue((AbstractConfigNodeValue)node, comments);
                }
            }
            // There shouldn't be any comments at this point, but add them just in case
            if (v != null) {
                values.add(v.withOrigin(v.origin().appendComments(new ArrayList<String>(comments))));
            }
            arrayCount -= 1;
            return new SimpleConfigList(arrayOrigin, values);
        }

        AbstractConfigValue parse() {
            AbstractConfigValue result = null;
            ArrayList<String> comments = new ArrayList<String>();
            boolean lastWasNewLine = false;
            for (AbstractConfigNode node : document.children()) {
                if (node instanceof ConfigNodeComment) {
                    comments.add(((ConfigNodeComment) node).commentText());
                    lastWasNewLine = false;
                } else if (node instanceof ConfigNodeSingleToken) {
                    Token t = ((ConfigNodeSingleToken) node).token();
                    if (Tokens.isNewline(t)) {
                        lineNumber++;
                        if (lastWasNewLine && result == null) {
                            comments.clear();
                        } else if (result != null) {
                            result = result.withOrigin(result.origin().appendComments(new ArrayList<String>(comments)));
                            comments.clear();
                            break;
                        }
                        lastWasNewLine = true;
                    }
                } else if (node instanceof ConfigNodeComplexValue) {
                    result = parseValue((ConfigNodeComplexValue)node, comments);
                    lastWasNewLine = false;
                }
            }
            return result;
        }
    }
}
