/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.dashbuilder.json;

/**
 * Implementation of parsing a JSON string into instances of {@link
 * org.dashbuilder.json.JsonValue}.
 */
class JsonTokenizer {

    private static final int INVALID_CHAR = -1;

    private static final String STOPCHARS = ",:]}/\\\"[{;=#";

    private JsonFactory jsonFactory;

    private boolean lenient = true;

    private int pushBackBuffer = INVALID_CHAR;

    private final String json;
    private int position = 0;

    JsonTokenizer(JsonFactory serverJsonFactory, String json) {
        this.jsonFactory = serverJsonFactory;
        this.json = json;
    }

    void back(char c) {
        assert pushBackBuffer == INVALID_CHAR;
        pushBackBuffer = c;
    }

    void back(int c) {
        back((char) c);
    }

    int next() {
        if (pushBackBuffer != INVALID_CHAR) {
            final int c = pushBackBuffer;
            pushBackBuffer = INVALID_CHAR;
            return c;
        }

        return position < json.length() ? json.charAt(position++) : INVALID_CHAR;
    }

    String next(int n) throws JsonException {
        if (n == 0) {
            return "";
        }

        char[] buffer = new char[n];
        int pos = 0;

        if (pushBackBuffer != INVALID_CHAR) {
            buffer[0] = (char) pushBackBuffer;
            pos = 1;
            pushBackBuffer = INVALID_CHAR;
        }

        int len;
        while ((pos < n) && ((len = read(buffer, pos, n - pos)) != -1)) {
            pos += len;
        }

        if (pos < n) {
            throw new JsonException("Position " + pos + " less than " + n);
        }

        return String.valueOf(buffer);
    }

    int nextNonWhitespace() {
        while (true) {
            final int c = next();
            if (!Character.isSpace((char) c)) {
                return c;
            }
        }
    }

    String nextString(int startChar) throws JsonException {
        final StringBuilder buffer = new StringBuilder();
        int c = next();
        assert c == '"' || (lenient && c == '\'');
        while (true) {
            c = next();
            switch (c) {
                case '\r':
                case '\n':
                    throw new JsonException("");
                case '\\':
                    c = next();
                    switch (c) {
                        case 'b':
                            buffer.append('\b');
                            break;
                        case 't':
                            buffer.append('\t');
                            break;
                        case 'n':
                            buffer.append('\n');
                            break;
                        case 'f':
                            buffer.append('\f');
                            break;
                        case 'r':
                            buffer.append('\r');
                            break;
                        // TODO: Not sure should even support this escaping
                        // mode since JSON is always UTF-8.
                        case 'u':
                            buffer.append((char) Integer.parseInt(next(4), 16));
                            break;
                        default:
                            buffer.append((char) c);
                    }
                    break;
                default:
                    if (c == startChar) {
                        return buffer.toString();
                    }
                    buffer.append((char) c);
            }
        }
    }

    String nextUntilOneOf(String chars) {
        final StringBuilder buffer = new StringBuilder();
        int c = next();
        while (c != INVALID_CHAR) {
            if (Character.isSpace((char) c) || chars.indexOf((char) c) >= 0) {
                back(c);
                break;
            }
            buffer.append((char) c);
            c = next();
        }
        return buffer.toString();
    }

    <T extends JsonValue> T nextValue() throws JsonException {
        final int c = nextNonWhitespace();
        back(c);
        switch (c) {
            case '"':
            case '\'':
                return (T) jsonFactory.create(nextString(c));
            case '{':
                return (T) parseObject();
            case '[':
                return (T) parseArray();
            default:
                return (T) getValueForLiteral(nextUntilOneOf(STOPCHARS));
        }
    }

    JsonArray parseArray() throws JsonException {
        final JsonArray array = jsonFactory.createArray();
        int c = nextNonWhitespace();
        assert c == '[';
        while (true) {
            c = nextNonWhitespace();
            switch (c) {
                case ']':
                    return array;
                default:
                    back(c);
                    array.set(array.length(), (JsonValue) nextValue());
                    final int d = nextNonWhitespace();
                    switch (d) {
                        case ']':
                            return array;
                        case ',':
                            break;
                        default:
                            throw new JsonException("Invalid array: expected , or ]");
                    }
            }
        }
    }

    JsonObject parseObject() throws JsonException {
        final JsonObject object = jsonFactory.createObject();
        int c = nextNonWhitespace();
        if (c != '{') {
            throw new JsonException(
                    "Payload does not begin with '{'.  Got " + c + "("
                            + Character.valueOf((char) c) + ")");
        }

        while (true) {
            c = nextNonWhitespace();
            switch (c) {
                case '}':
                    // We're done.
                    return object;
                case '"':
                case '\'':
                    back(c);
                    // Ready to start a key.
                    final String key = nextString(c);
                    if (nextNonWhitespace() != ':') {
                        throw new JsonException(
                                "Invalid object: expecting \":\"");
                    }
                    // TODO: Make sure this key is not already set.
                    object.put(key, (JsonValue) nextValue());
                    switch (nextNonWhitespace()) {
                        case ',':
                            break;
                        case '}':
                            return object;
                        default:
                            throw new JsonException(
                                    "Invalid object: expecting } or ,");
                    }
                    break;
                case ',':
                    break;
                default:
                    if (lenient && (Character.isDigit((char) c) || Character.isLetterOrDigit((char) c))) {
                        StringBuilder keyBuffer = new StringBuilder();
                        keyBuffer.append(c);
                        while (true) {
                            c = next();
                            if (Character.isDigit((char) c) || Character.isLetterOrDigit((char) c)) {
                                keyBuffer.append(c);
                            } else {
                                back(c);
                                break;
                            }
                        }
                        if (nextNonWhitespace() != ':') {
                            throw new JsonException(
                                    "Invalid object: expecting \":\"");
                        }
                        // TODO: Make sure this key is not already set.
                        object.put(keyBuffer.toString(), (JsonValue) nextValue());
                        switch (nextNonWhitespace()) {
                            case ',':
                                break;
                            case '}':
                                return object;
                            default:
                                throw new JsonException(
                                        "Invalid object: expecting } or ,");
                        }

                    } else {
                        throw new JsonException("Invalid object: ");
                    }
            }
        }
    }

    private JsonNumber getNumberForLiteral(String literal)
            throws JsonException {
        try {
            return jsonFactory.create(Double.parseDouble(literal));
        } catch (NumberFormatException e) {
            throw new JsonException("Invalid number literal: " + literal);
        }
    }

    private JsonValue getValueForLiteral(String literal) throws JsonException {
        if ("".equals(literal)) {
            throw new JsonException("Missing value");
        }

        if ("null".equals(literal) || "undefined".equals(literal)) {
            return jsonFactory.createNull();
        }

        if ("true".equals(literal)) {
            return jsonFactory.create(true);
        }

        if ("false".equals(literal)) {
            return jsonFactory.create(false);
        }

        final char c = literal.charAt(0);
        if (c == '-' || Character.isDigit(c)) {
            return getNumberForLiteral(literal);
        }

        throw new JsonException("Invalid literal: \"" + literal + "\"");
    }

    private int read(char[] buffer, int pos, int len) {
        int maxLen = Math.min(json.length() - position, len);
        String src = json.substring(position, position + maxLen);
        char result[] = src.toCharArray();
        System.arraycopy(result, 0, buffer, pos, maxLen);
        position += maxLen;
        return maxLen;
    }
}
