/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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
 *
 *      https://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.springframework.boot.configurationprocessor.json;

// Note: this class was written without inspecting the non-free org.json source code.

/**
 * Parses a JSON (<a href="https://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) encoded
 * string into the corresponding object. Most clients of this class will use only need the
 * {@link #JSONTokener(String) constructor} and {@link #nextValue} method. Example usage:
 * <pre>
 * String json = "{"
 *         + "  \"query\": \"Pizza\", "
 *         + "  \"locations\": [ 94043, 90210 ] "
 *         + "}";
 *
 * JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
 * String query = object.getString("query");
 * JSONArray locations = object.getJSONArray("locations");</pre>
 * <p>
 * For best interoperability and performance use JSON that complies with RFC 4627, such as
 * that generated by {@link JSONStringer}. For legacy reasons this parser is lenient, so a
 * successful parse does not indicate that the input string was valid JSON. All the
 * following syntax errors will be ignored:
 * <ul>
 * <li>End of line comments starting with {@code //} or {@code #} and ending with a
 * newline character.
 * <li>C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such
 * comments may not be nested.
 * <li>Strings that are unquoted or {@code 'single quoted'}.
 * <li>Hexadecimal integers prefixed with {@code 0x} or {@code 0X}.
 * <li>Octal integers prefixed with {@code 0}.
 * <li>Array elements separated by {@code ;}.
 * <li>Unnecessary array separators. These are interpreted as if null was the omitted
 * value.
 * <li>Key-value pairs separated by {@code =} or {@code =>}.
 * <li>Key-value pairs separated by {@code ;}.
 * </ul>
 * <p>
 * Each tokener may be used to parse a single JSON string. Instances of this class are not
 * thread safe. Although this class is nonfinal, it was not designed for inheritance and
 * should not be subclassed. In particular, self-use by overrideable methods is not
 * specified. See <i>Effective Java</i> Item 17, "Design and Document or inheritance or
 * else prohibit it" for further information.
 */
public class JSONTokener {

	/**
	 * The input JSON.
	 */
	private final String in;

	/**
	 * The index of the next character to be returned by {@link #next}. When the input is
	 * exhausted, this equals the input's length.
	 */
	private int pos;

	/**
	 * @param in JSON encoded string. Null is not permitted and will yield a tokener that
	 * throws {@code NullPointerExceptions} when methods are called.
	 */
	public JSONTokener(String in) {
		// consume an optional byte order mark (BOM) if it exists
		if (in != null && in.startsWith("\ufeff")) {
			in = in.substring(1);
		}
		this.in = in;
	}

	/**
	 * Returns the next value from the input.
	 * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long,
	 * Double or {@link JSONObject#NULL}.
	 * @throws JSONException if the input is malformed.
	 */
	public Object nextValue() throws JSONException {
		int c = nextCleanInternal();
		switch (c) {
			case -1:
				throw syntaxError("End of input");

			case '{':
				return readObject();

			case '[':
				return readArray();

			case '\'', '"':
				return nextString((char) c);

			default:
				this.pos--;
				return readLiteral();
		}
	}

	private int nextCleanInternal() throws JSONException {
		while (this.pos < this.in.length()) {
			int c = this.in.charAt(this.pos++);
			switch (c) {
				case '\t', ' ', '\n', '\r':
					continue;

				case '/':
					if (this.pos == this.in.length()) {
						return c;
					}

					char peek = this.in.charAt(this.pos);
					switch (peek) {
						case '*':
							// skip a /* c-style comment */
							this.pos++;
							int commentEnd = this.in.indexOf("*/", this.pos);
							if (commentEnd == -1) {
								throw syntaxError("Unterminated comment");
							}
							this.pos = commentEnd + 2;
							continue;

						case '/':
							// skip a // end-of-line comment
							this.pos++;
							skipToEndOfLine();
							continue;

						default:
							return c;
					}

				case '#':
					/*
					 * Skip a # hash end-of-line comment. The JSON RFC doesn't specify
					 * this behavior, but it's required to parse existing documents. See
					 * https://b/2571423.
					 */
					skipToEndOfLine();
					continue;

				default:
					return c;
			}
		}

		return -1;
	}

	/**
	 * Advances the position until after the next newline character. If the line is
	 * terminated by "\r\n", the '\n' must be consumed as whitespace by the caller.
	 */
	private void skipToEndOfLine() {
		for (; this.pos < this.in.length(); this.pos++) {
			char c = this.in.charAt(this.pos);
			if (c == '\r' || c == '\n') {
				this.pos++;
				break;
			}
		}
	}

	/**
	 * Returns the string up to but not including {@code quote}, unescaping any character
	 * escape sequences encountered along the way. The opening quote should have already
	 * been read. This consumes the closing quote, but does not include it in the returned
	 * string.
	 * @param quote either ' or ".
	 * @return the string up to but not including {@code quote}
	 * @throws NumberFormatException if any unicode escape sequences are malformed.
	 * @throws JSONException if processing of json failed
	 */
	public String nextString(char quote) throws JSONException {
		/*
		 * For strings that are free of escape sequences, we can just extract the result
		 * as a substring of the input. But if we encounter an escape sequence, we need to
		 * use a StringBuilder to compose the result.
		 */
		StringBuilder builder = null;

		/* the index of the first character not yet appended to the builder. */
		int start = this.pos;

		while (this.pos < this.in.length()) {
			int c = this.in.charAt(this.pos++);
			if (c == quote) {
				if (builder == null) {
					// a new string avoids leaking memory
					return new String(this.in.substring(start, this.pos - 1));
				}
				else {
					builder.append(this.in, start, this.pos - 1);
					return builder.toString();
				}
			}

			if (c == '\\') {
				if (this.pos == this.in.length()) {
					throw syntaxError("Unterminated escape sequence");
				}
				if (builder == null) {
					builder = new StringBuilder();
				}
				builder.append(this.in, start, this.pos - 1);
				builder.append(readEscapeCharacter());
				start = this.pos;
			}
		}

		throw syntaxError("Unterminated string");
	}

	/**
	 * Unescapes the character identified by the character or characters that immediately
	 * follow a backslash. The backslash '\' should have already been read. This supports
	 * both unicode escapes "u000A" and two-character escapes "\n".
	 * @return the unescaped char
	 * @throws NumberFormatException if any unicode escape sequences are malformed.
	 * @throws JSONException if processing of json failed
	 */
	private char readEscapeCharacter() throws JSONException {
		char escaped = this.in.charAt(this.pos++);
		switch (escaped) {
			case 'u':
				if (this.pos + 4 > this.in.length()) {
					throw syntaxError("Unterminated escape sequence");
				}
				String hex = this.in.substring(this.pos, this.pos + 4);
				this.pos += 4;
				return (char) Integer.parseInt(hex, 16);

			case 't':
				return '\t';

			case 'b':
				return '\b';

			case 'n':
				return '\n';

			case 'r':
				return '\r';

			case 'f':
				return '\f';

			case '\'', '"', '\\':
			default:
				return escaped;
		}
	}

	/**
	 * Reads a null, boolean, numeric or unquoted string literal value. Numeric values
	 * will be returned as an Integer, Long, or Double, in that order of preference.
	 * @return a literal value
	 * @throws JSONException if processing of json failed
	 */
	private Object readLiteral() throws JSONException {
		String literal = nextToInternal("{}[]/\\:,=;# \t\f");

		if (literal.isEmpty()) {
			throw syntaxError("Expected literal value");
		}
		else if ("null".equalsIgnoreCase(literal)) {
			return JSONObject.NULL;
		}
		else if ("true".equalsIgnoreCase(literal)) {
			return Boolean.TRUE;
		}
		else if ("false".equalsIgnoreCase(literal)) {
			return Boolean.FALSE;
		}

		/* try to parse as an integral type... */
		if (literal.indexOf('.') == -1) {
			int base = 10;
			String number = literal;
			if (number.startsWith("0x") || number.startsWith("0X")) {
				number = number.substring(2);
				base = 16;
			}
			else if (number.startsWith("0") && number.length() > 1) {
				number = number.substring(1);
				base = 8;
			}
			try {
				long longValue = Long.parseLong(number, base);
				if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) {
					return (int) longValue;
				}
				else {
					return longValue;
				}
			}
			catch (NumberFormatException e) {
				/*
				 * This only happens for integral numbers greater than Long.MAX_VALUE,
				 * numbers in exponential form (5e-10) and unquoted strings. Fall through
				 * to try floating point.
				 */
			}
		}

		/* ...next try to parse as a floating point... */
		try {
			return Double.valueOf(literal);
		}
		catch (NumberFormatException ignored) {
		}

		/* ... finally give up. We have an unquoted string */
		return new String(literal); // a new string avoids leaking memory
	}

	/**
	 * Returns the string up to but not including any of the given characters or a newline
	 * character. This does not consume the excluded character.
	 * @return the string up to but not including any of the given characters or a newline
	 * character
	 */
	private String nextToInternal(String excluded) {
		int start = this.pos;
		for (; this.pos < this.in.length(); this.pos++) {
			char c = this.in.charAt(this.pos);
			if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) {
				return this.in.substring(start, this.pos);
			}
		}
		return this.in.substring(start);
	}

	/**
	 * Reads a sequence of key/value pairs and the trailing closing brace '}' of an
	 * object. The opening brace '{' should have already been read.
	 * @return an object
	 * @throws JSONException if processing of json failed
	 */
	private JSONObject readObject() throws JSONException {
		JSONObject result = new JSONObject();

		/* Peek to see if this is the empty object. */
		int first = nextCleanInternal();
		if (first == '}') {
			return result;
		}
		else if (first != -1) {
			this.pos--;
		}

		while (true) {
			Object name = nextValue();
			if (!(name instanceof String)) {
				if (name == null) {
					throw syntaxError("Names cannot be null");
				}
				else {
					throw syntaxError(
							"Names must be strings, but " + name + " is of type " + name.getClass().getName());
				}
			}

			/*
			 * Expect the name/value separator to be either a colon ':', an equals sign
			 * '=', or an arrow "=>". The last two are bogus but we include them because
			 * that's what the original implementation did.
			 */
			int separator = nextCleanInternal();
			if (separator != ':' && separator != '=') {
				throw syntaxError("Expected ':' after " + name);
			}
			if (this.pos < this.in.length() && this.in.charAt(this.pos) == '>') {
				this.pos++;
			}

			result.put((String) name, nextValue());

			switch (nextCleanInternal()) {
				case '}':
					return result;
				case ';', ',':
					continue;
				default:
					throw syntaxError("Unterminated object");
			}
		}
	}

	/**
	 * Reads a sequence of values and the trailing closing brace ']' of an array. The
	 * opening brace '[' should have already been read. Note that "[]" yields an empty
	 * array, but "[,]" returns a two-element array equivalent to "[null,null]".
	 * @return an array
	 * @throws JSONException if processing of json failed
	 */
	private JSONArray readArray() throws JSONException {
		JSONArray result = new JSONArray();

		/* to cover input that ends with ",]". */
		boolean hasTrailingSeparator = false;

		while (true) {
			switch (nextCleanInternal()) {
				case -1:
					throw syntaxError("Unterminated array");
				case ']':
					if (hasTrailingSeparator) {
						result.put(null);
					}
					return result;
				case ',', ';':
					/* A separator without a value first means "null". */
					result.put(null);
					hasTrailingSeparator = true;
					continue;
				default:
					this.pos--;
			}

			result.put(nextValue());

			switch (nextCleanInternal()) {
				case ']':
					return result;
				case ',', ';':
					hasTrailingSeparator = true;
					continue;
				default:
					throw syntaxError("Unterminated array");
			}
		}
	}

	/**
	 * Returns an exception containing the given message plus the current position and the
	 * entire input string.
	 * @param message the message
	 * @return an exception
	 */
	public JSONException syntaxError(String message) {
		return new JSONException(message + this);
	}

	/**
	 * Returns the current position and the entire input string.
	 * @return the current position and the entire input string.
	 */
	@Override
	public String toString() {
		// consistent with the original implementation
		return " at character " + this.pos + " of " + this.in;
	}

	/*
	 * Legacy APIs.
	 *
	 * None of the methods below are on the critical path of parsing JSON documents. They
	 * exist only because they were exposed by the original implementation and may be used
	 * by some clients.
	 */

	public boolean more() {
		return this.pos < this.in.length();
	}

	public char next() {
		return this.pos < this.in.length() ? this.in.charAt(this.pos++) : '\0';
	}

	public char next(char c) throws JSONException {
		char result = next();
		if (result != c) {
			throw syntaxError("Expected " + c + " but was " + result);
		}
		return result;
	}

	public char nextClean() throws JSONException {
		int nextCleanInt = nextCleanInternal();
		return nextCleanInt == -1 ? '\0' : (char) nextCleanInt;
	}

	public String next(int length) throws JSONException {
		if (this.pos + length > this.in.length()) {
			throw syntaxError(length + " is out of bounds");
		}
		String result = this.in.substring(this.pos, this.pos + length);
		this.pos += length;
		return result;
	}

	public String nextTo(String excluded) {
		if (excluded == null) {
			throw new NullPointerException("excluded == null");
		}
		return nextToInternal(excluded).trim();
	}

	public String nextTo(char excluded) {
		return nextToInternal(String.valueOf(excluded)).trim();
	}

	public void skipPast(String thru) {
		int thruStart = this.in.indexOf(thru, this.pos);
		this.pos = thruStart == -1 ? this.in.length() : (thruStart + thru.length());
	}

	public char skipTo(char to) {
		int index = this.in.indexOf(to, this.pos);
		if (index != -1) {
			this.pos = index;
			return to;
		}
		else {
			return '\0';
		}
	}

	public void back() {
		if (--this.pos == -1) {
			this.pos = 0;
		}
	}

	public static int dehexchar(char hex) {
		if (hex >= '0' && hex <= '9') {
			return hex - '0';
		}
		else if (hex >= 'A' && hex <= 'F') {
			return hex - 'A' + 10;
		}
		else if (hex >= 'a' && hex <= 'f') {
			return hex - 'a' + 10;
		}
		else {
			return -1;
		}
	}

}
