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

import groovy.json.JsonDateHandling;

import java.util.ArrayList;
import java.util.List;

/**
 * JSON parser variant that also accepts comments, single quotes, and unquoted strings.
 */
public class JsonParserLax extends JsonParserCharArray {

    private final boolean useValues;
    private final boolean chop;
    private final boolean lazyChop;
    private final JsonDateHandling defaultDateHandling;

    /**
     * Creates a lax parser with eager value containers and lazy chopping.
     */
    public JsonParserLax() {
        this(true);
    }

    /**
     * Creates a lax parser with the supplied value-container mode.
     *
     * @param useValues whether to use eager {@link Value} containers
     */
    public JsonParserLax(boolean useValues) {
        this(useValues, false);
    }

    /**
     * Creates a lax parser with explicit chopping behavior.
     *
     * @param useValues whether to use eager {@link Value} containers
     * @param chop whether to eagerly copy overlay slices
     */
    public JsonParserLax(boolean useValues, boolean chop) {
        this(useValues, chop, !chop);
    }

    /**
     * Creates a lax parser with explicit lazy chopping behavior.
     *
     * @param useValues whether to use eager {@link Value} containers
     * @param chop whether to eagerly copy overlay slices
     * @param lazyChop whether to defer chopping until values are accessed
     */
    public JsonParserLax(boolean useValues, boolean chop, boolean lazyChop) {
        this(useValues, chop, lazyChop, JsonDateHandling.UTIL_DATE);
    }

    /**
     * Creates a lax parser with full overlay configuration.
     *
     * @param useValues whether to use eager {@link Value} containers
     * @param chop whether to eagerly copy overlay slices
     * @param lazyChop whether to defer chopping until values are accessed
     * @param defaultDateHandling what a date-like string should become
     */
    public JsonParserLax(boolean useValues, boolean chop, boolean lazyChop, JsonDateHandling defaultDateHandling) {
        this.useValues = useValues;
        this.chop = chop;
        this.lazyChop = lazyChop;
        this.defaultDateHandling = defaultDateHandling;
    }

    @SuppressWarnings("unchecked")
    private Value decodeJsonObjectLax() {
        enterNesting();
        try {
        if (__currentChar == '{')
            this.nextChar();

        ValueMap map = useValues ? new ValueMapImpl() : new LazyValueMap(lazyChop);
        Value value = new ValueContainer(map);

        skipWhiteSpace();
        int startIndexOfKey = __index;
        Value key;
        MapItemValue miv;
        Value item;

        done:
        for (; __index < this.charArray.length; __index++) {
            skipWhiteSpace();

            switch (__currentChar) {
                case ':':
                    char startChar = charArray[startIndexOfKey];
                    if (startChar == ',') {
                        startIndexOfKey++;
                    }

                    key = extractLaxString(startIndexOfKey, __index - 1, false, JsonDateHandling.STRING);
                    __index++; //skip :

                    item = decodeValueInternal();
                    skipWhiteSpace();

                    miv = new MapItemValue(key, item);

                    map.add(miv);

                    startIndexOfKey = __index;
                    if (__currentChar == '}') {
                        __index++;
                        break done;
                    }

                    break;

                case '\'':
                    key = decodeStringSingle();

                    //puts ("key with quote", key);

                    skipWhiteSpace();

                    if (__currentChar != ':') {
                        complain("expecting current character to be ':' but got " + charDescription(__currentChar) + "\n");
                    }
                    __index++;
                    item = decodeValueInternal();

                    //puts ("key", "#" + key + "#", value);

                    skipWhiteSpace();

                    miv = new MapItemValue(key, item);

                    map.add(miv);
                    startIndexOfKey = __index;
                    if (__currentChar == '}') {
                        __index++;
                        break done;
                    }

                    break;

                case '"':
                    key = decodeStringDouble();

                    //puts ("key with quote", key);

                    skipWhiteSpace();

                    if (__currentChar != ':') {
                        complain("expecting current character to be ':' but got " + charDescription(__currentChar) + "\n");
                    }
                    __index++;
                    item = decodeValueInternal();

                    //puts ("key", "#" + key + "#", value);

                    skipWhiteSpace();

                    miv = new MapItemValue(key, item);

                    map.add(miv);
                    startIndexOfKey = __index;
                    if (__currentChar == '}') {
                        __index++;
                        break done;
                    }

                    break;
            }

            switch (__currentChar) {
                case '}':
                    __index++;
                    break done;

                case '/': /* */ //
                    handleComment();
                    startIndexOfKey = __index + 1;
                    break;

                case '#':
                    handleBashComment();
                    startIndexOfKey = __index + 1;
                    break;
            }
        }

        return value;
        } finally {
            exitNesting();
        }
    }

    private Value extractLaxString(int startIndexOfKey, int end, boolean encoded, JsonDateHandling dateHandling) {
        char startChar;
        startIndexLookup:
        for (; startIndexOfKey < __index && startIndexOfKey < charArray.length; startIndexOfKey++) {
            startChar = charArray[startIndexOfKey];
            switch (startChar) {
                case ' ':
                case '\n':
                case '\t':
                    continue;

                default:
                    break startIndexLookup;
            }
        }

        char endChar;
        int endIndex = end >= charArray.length ? charArray.length - 1 : end;
        endIndexLookup:
        for (; endIndex >= startIndexOfKey + 1 && endIndex >= 0; endIndex--) {
            endChar = charArray[endIndex];
            switch (endChar) {
                case ' ':
                case '\n':
                case '\t':
                case '}':
                    continue;
                case ',':
                case ';':
                    continue;

                case ']':
                    continue;
                default:
                    break endIndexLookup;
            }
        }
        return new CharSequenceValue(chop, Type.STRING, startIndexOfKey, endIndex + 1, this.charArray, encoded, dateHandling);
    }

    /**
     * Decodes the next value using the parser's lax JSON rules.
     *
     * @return parsed Groovy JSON value or overlay value
     */
    @Override
    protected final Object decodeValue() {
        return this.decodeValueInternal();
    }

    private Value decodeValueInternal() {
        Value value = null;

        for (; __index < charArray.length; __index++) {
            skipWhiteSpace();

            switch (__currentChar) {
                case '\n':
                    break;

                case '\r':
                    break;

                case ' ':
                    break;

                case '\t':
                    break;

                case '\b':
                    break;

                case '\f':
                    break;

                case '/': /* */ //
                    handleComment();
                    break;

                case '#':
                    handleBashComment();
                    break;

                case '"':
                    value = decodeStringDouble();
                    break;

                case '\'':
                    value = decodeStringSingle();
                    break;

                case 't':
                    if (isTrue()) {
                        return decodeTrue() ? ValueContainer.TRUE : ValueContainer.FALSE;
                    } else {
                        value = decodeStringLax();
                    }
                    break;

                case 'f':
                    if (isFalse()) {
                        return !decodeFalse() ? ValueContainer.FALSE : ValueContainer.TRUE;
                    } else {
                        value = decodeStringLax();
                    }
                    break;

                case 'n':
                    if (isNull()) {
                        decodeNull();
                        return ValueContainer.NULL;
                    } else {
                        value = decodeStringLax();
                    }

                    break;

                case '[':
                    value = decodeJsonArrayLax();
                    break;

                case '{':
                    value = decodeJsonObjectLax();
                    break;

                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '0':
                    return decodeNumberLax(false);

                case '-':
                    return decodeNumberLax(true);

                default:
                    value = decodeStringLax();
            }

            if (value != null) {
                return value;
            }
        }

        return null;
    }

    private void handleBashComment() {
        for (; __index < charArray.length; __index++) {
            __currentChar = charArray[__index];

            if (__currentChar == '\n') {
                return;
            }
        }
    }

    private void handleComment() {
        if (hasMore()) {
            __index++;
            __currentChar = charArray[__index];

            switch (__currentChar) {
                case '*':
                    for (; __index < charArray.length; __index++) {
                        __currentChar = charArray[__index];

                        if (__currentChar == '*') {
                            if (hasMore()) {
                                __index++;
                                __currentChar = charArray[__index];
                                if (__currentChar == '/') {
                                    return;
                                }
                            } else {
                                complain("missing close of comment");
                            }
                        }
                    }
                    // fall through
                case '/':
                    for (; __index < charArray.length; __index++) {
                        __currentChar = charArray[__index];

                        if (__currentChar == '\n') {
                            return;
                        }
                    }
            }
        }
    }

    /**
     * Decodes a number from a JSON value.  If at any point it is determined that
     * the value is not a valid number the value is treated as a {@code String}.
     *
     * @param minus indicate whether the number is negative
     * @return a number, or {@code String} if not a valid number
     */
    protected final Value decodeNumberLax(boolean minus) {
        char[] array = charArray;

        final int startIndex = __index;
        int index = __index;
        char currentChar;
        boolean doubleFloat = false;
        boolean foundDot = false;
        boolean foundSign = false;
        boolean foundExp = false;

        if (minus && index + 1 < array.length) {
            index++;
        }

        while (true) {
            currentChar = array[index];
            if (isNumberDigit(currentChar)) {
                //noop
            } else if (currentChar <= 32) { //white
                break;
            } else if (isDelimiter(currentChar)) {
                break;
            } else if (isDecimalChar(currentChar)) {
                switch (currentChar) {
                    case DECIMAL_POINT:
                        if (foundDot || foundExp) { return decodeStringLax(); }
                        foundDot = true;
                        break;
                    case LETTER_E:
                    case LETTER_BIG_E:
                        if (foundExp) { return decodeStringLax(); }
                        foundExp = true;
                        break;
                    case MINUS:
                    case PLUS:
                        if (foundSign || !foundExp) { return decodeStringLax(); }
                        if (foundExp && array[index - 1] != LETTER_E && array[index - 1] != LETTER_BIG_E) {
                            return decodeStringLax();
                        }
                        foundSign = true;
                        break;
                }
                doubleFloat = true;
            } else {
                return decodeStringLax();
            }
            index++;
            if (index >= array.length) break;
        }

        // Handle the case where the exponential number ends without the actual exponent
        if (foundExp) {
            char prevChar = array[index - 1];
            if (prevChar == LETTER_E || prevChar == LETTER_BIG_E || prevChar == MINUS || prevChar == PLUS) {
                return decodeStringLax();
            }
        }

        __index = index;
        __currentChar = currentChar;

        Type type = doubleFloat ? Type.DOUBLE : Type.INTEGER;

        // Checked here rather than in NumberValue: the overlay defers conversion to first access,
        // and a document over the limit should be rejected by the parse that read it.
        checkNumberLength(__index - startIndex);

        return new NumberValue(chop, type, startIndex, __index, this.charArray);
    }

    private boolean isNull() {
        if (__index + NULL.length <= charArray.length) {
            if (charArray[__index] == 'n' &&
                    charArray[__index + 1] == 'u' &&
                    charArray[__index + 2] == 'l' &&
                    charArray[__index + 3] == 'l') {
                return true;
            }
        }
        return false;
    }

    private boolean isTrue() {
        if (__index + TRUE.length <= charArray.length) {
            if (charArray[__index] == 't' &&
                    charArray[__index + 1] == 'r' &&
                    charArray[__index + 2] == 'u' &&
                    charArray[__index + 3] == 'e') {
                return true;

            }
        }
        return false;
    }

    private boolean isFalse() {
        if (__index + FALSE.length <= charArray.length) {
            if (charArray[__index] == 'f' &&
                    charArray[__index + 1] == 'a' &&
                    charArray[__index + 2] == 'l' &&
                    charArray[__index + 3] == 's' &&
                    charArray[__index + 4] == 'e') {
                return true;
            }
        }
        return false;
    }

    private Value decodeStringLax() {
        int index = __index;
        char currentChar = charArray[__index];
        final int startIndex = __index;
        boolean encoded = false;
        char[] charArray = this.charArray;

        for (; index < charArray.length; index++) {
            currentChar = charArray[index];

            if (isDelimiter(currentChar)) break;
            else if (currentChar == '\\') break;
        }

        Value value = this.extractLaxString(startIndex, index, encoded, defaultDateHandling);

        __index = index;
        return value;
    }

    private Value decodeStringDouble() {
        __currentChar = charArray[__index];

        if (__index < charArray.length && __currentChar == '"') {
            __index++;
        }

        final int startIndex = __index;

        boolean escape = false;
        boolean encoded = false;

        done:
        for (; __index < this.charArray.length; __index++) {
            __currentChar = charArray[__index];
            switch (__currentChar) {

                case '"':
                    if (!escape) {
                        break done;
                    } else {
                        escape = false;
                        continue;
                    }

                case '\\':
                    escape = !escape;
                    encoded = true;
                    continue;
            }
            escape = false;
        }

        Value value = new CharSequenceValue(chop, Type.STRING, startIndex, __index, this.charArray, encoded, defaultDateHandling);

        if (__index < charArray.length) {
            __index++;
        }

        return value;
    }

    private Value decodeStringSingle() {
        __currentChar = charArray[__index];

        if (__index < charArray.length && __currentChar == '\'') {
            __index++;
        }

        final int startIndex = __index;

        boolean escape = false;
        boolean encoded = false;
        int minusCount = 0;
        int colonCount = 0;

        done:
        for (; __index < this.charArray.length; __index++) {
            __currentChar = charArray[__index];
            switch (__currentChar) {
                case '\'':
                    if (!escape) {
                        break done;
                    } else {
                        escape = false;
                        continue;
                    }

                case '\\':
                    encoded = true;
                    escape = true;
                    continue;

                case '-':
                    minusCount++;
                    break;

                case ':':
                    colonCount++;
                    break;
            }
            escape = false;
        }

        JsonDateHandling dateHandling = (!encoded && minusCount >= 2 && colonCount >= 2)
                ? defaultDateHandling : JsonDateHandling.STRING;

        Value value = new CharSequenceValue(chop, Type.STRING, startIndex, __index, this.charArray, encoded, dateHandling);

        if (__index < charArray.length) {
            __index++;
        }

        return value;
    }

    private Value decodeJsonArrayLax() {
        enterNesting();
        try {
        if (__currentChar == '[') {
            __index++;
        }

        skipWhiteSpace();

        if (__currentChar == ']') {
            __index++;
            return new ValueContainer(new ArrayList<>());
        }

        List<Object> list;

        if (useValues) {
            list = new ArrayList<Object>();
        } else {
            list = new ValueList(lazyChop);
        }

        Value value = new ValueContainer(list);

        do {
            skipWhiteSpace();

            Object arrayItem = decodeValueInternal();

            list.add(arrayItem);

            boolean doStop = false;

            done:
            do { // Find either next array element or end of array while ignoring comments
                skipWhiteSpace();

                switch (__currentChar) {
                    case '/':
                        handleComment();
                        __index++;
                        continue;
                    case '#':
                        handleBashComment();
                        __index++;
                        continue;
                    case ',':
                        __index++;
                        break done;
                    case ']':
                        __index++;
                        doStop = true;
                        break done;
                    default:
                        String charString = charDescription(__currentChar);

                        complain(
                                String.format("expecting a ',' or a ']', " +
                                        " but got \nthe current character of  %s " +
                                        " on array index of %s \n", charString, list.size())
                        );
                }
            } while (this.hasMore());

            if (doStop) break;

        } while (this.hasMore());

        return value;
        } finally {
            exitNesting();
        }
    }

    /**
     * Parses a character buffer and unwraps top-level container values.
     *
     * @param cs JSON content to parse
     * @return parsed Groovy JSON value
     */
    @Override
    protected final Object decodeFromChars(char[] cs) {
        Value value = ((Value) super.decodeFromChars(cs));
        if (value.isContainer()) {
            return value.toValue();
        } else {
            return value;
        }
    }
}
