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

import org.apache.groovy.json.internal.BaseJsonParser;
import org.apache.groovy.json.internal.CharBuf;
import org.apache.groovy.json.internal.Chr;
import groovy.lang.Closure;
import groovy.util.Expando;

import java.io.StringReader;
import java.net.URL;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Class responsible for the actual String serialization of the possible values of a JSON structure.
 * This class can also be used as a category to add <code>toJson()</code> methods to various types.
 * <p>
 * This class does not provide the ability to customize the resulting output.  A {@link JsonGenerator}
 * can be used if the ability to alter the resulting output is required.
 *
 * @see JsonGenerator
 * @since 1.8.0
 */
public class JsonOutput {

    /** Opening bracket used for JSON arrays. */
    static final char OPEN_BRACKET = '[';
    /** Closing bracket used for JSON arrays. */
    static final char CLOSE_BRACKET = ']';
    /** Opening brace used for JSON objects. */
    static final char OPEN_BRACE = '{';
    /** Closing brace used for JSON objects. */
    static final char CLOSE_BRACE = '}';
    /** Colon separator used between JSON names and values. */
    static final char COLON = ':';
    /** Comma separator used between JSON elements. */
    static final char COMMA = ',';
    /** Space character used when pretty-printing JSON. */
    static final char SPACE = ' ';
    /** New line character used when pretty-printing JSON. */
    static final char NEW_LINE = '\n';
    /** Quote character used for JSON strings. */
    static final char QUOTE = '"';

    /** Precomputed character array for an empty JSON string value. */
    static final char[] EMPTY_STRING_CHARS = Chr.array(QUOTE, QUOTE);
    /** Precomputed character array for an empty JSON object. */
    static final char[] EMPTY_MAP_CHARS = {OPEN_BRACE, CLOSE_BRACE};
    /** Precomputed character array for an empty JSON array. */
    static final char[] EMPTY_LIST_CHARS = {OPEN_BRACKET, CLOSE_BRACKET};

    /**
     * Default generator used by {@code JsonOutput} and the JSON builders.
     */
    static final JsonGenerator DEFAULT_GENERATOR = new DefaultJsonGenerator(new JsonGenerator.Options());

    /**
     * @return "true" or "false" for a boolean value
     */
    public static String toJson(Boolean bool) {
        return DEFAULT_GENERATOR.toJson(bool);
    }

    /**
     * @return a string representation for a number
     * @throws JsonException if the number is infinite or not a number.
     */
    public static String toJson(Number n) {
        return DEFAULT_GENERATOR.toJson(n);
    }

    /**
     * @return a JSON string representation of the character
     */
    public static String toJson(Character c) {
        return DEFAULT_GENERATOR.toJson(c);
    }

    /**
     * @return a properly encoded string with escape sequences
     */
    public static String toJson(String s) {
        return DEFAULT_GENERATOR.toJson(s);
    }

    /**
     * Format a date that is parseable from JavaScript, according to ISO-8601.
     *
     * @param date the date to format to a JSON string
     * @return a formatted date in the form of a string
     */
    public static String toJson(Date date) {
        return DEFAULT_GENERATOR.toJson(date);
    }

    /**
     * Format a calendar instance that is parseable from JavaScript, according to ISO-8601.
     *
     * @param cal the calendar to format to a JSON string
     * @return a formatted date in the form of a string
     */
    public static String toJson(Calendar cal) {
        return DEFAULT_GENERATOR.toJson(cal);
    }

    /**
     * @return the string representation of an uuid
     */
    public static String toJson(UUID uuid) {
        return DEFAULT_GENERATOR.toJson(uuid);
    }

    /**
     * @return the string representation of the URL
     */
    public static String toJson(URL url) {
        return DEFAULT_GENERATOR.toJson(url);
    }

    /**
     * @return an object representation of a closure
     */
    public static String toJson(Closure closure) {
        return DEFAULT_GENERATOR.toJson(closure);
    }

    /**
     * @return an object representation of an Expando
     */
    public static String toJson(Expando expando) {
        return DEFAULT_GENERATOR.toJson(expando);
    }

    /**
     * @return "null" for a null value, or a JSON array representation for a collection, array, iterator or enumeration,
     * or representation for other object.
     */
    public static String toJson(Object object) {
        return DEFAULT_GENERATOR.toJson(object);
    }

    /**
     * @return a JSON object representation for a map
     */
    public static String toJson(Map m) {
        return DEFAULT_GENERATOR.toJson(m);
    }

    /**
     * Pretty print a JSON payload.
     *
     * <p>
     * Nesting depth is bounded by the {@code groovy.json.maxNestingDepth} system property
     * (default {@link BaseJsonParser#DEFAULT_MAX_NESTING_DEPTH}), matching the bound the slurpers
     * enforce, so a payload this method will format is one {@link JsonSlurper} would parse. A
     * payload nested deeper &mdash; or left unbalanced, which the lexer cannot distinguish from
     * nesting &mdash; is rejected with a {@link JsonException} rather than expanded into an output
     * quadratically larger than itself. A property value of {@code 0} or less disables the check.
     *
     * <p>
     * Use {@link #prettyPrint(String, boolean, int)} to bound a single call instead.
     *
     * @param jsonPayload a JSON payload
     * @return a pretty representation of JSON payload.
     */
    public static String prettyPrint(String jsonPayload) {
        return prettyPrint(jsonPayload, false);
    }

    /**
     * Pretty print a JSON payload.
     *
     * <p>
     * Nesting depth is bounded by the {@code groovy.json.maxNestingDepth} system property
     * (default {@link BaseJsonParser#DEFAULT_MAX_NESTING_DEPTH}), matching the bound the slurpers
     * enforce, so a payload this method will format is one {@link JsonSlurper} would parse. A
     * payload nested deeper &mdash; or left unbalanced, which the lexer cannot distinguish from
     * nesting &mdash; is rejected with a {@link JsonException} rather than expanded into an output
     * quadratically larger than itself. A property value of {@code 0} or less disables the check.
     *
     * <p>
     * Use {@link #prettyPrint(String, boolean, int)} to bound a single call instead.
     *
     * @param jsonPayload a JSON payload
     * @param disableUnicodeEscaping whether to disable unicode escaping
     * @return a pretty representation of JSON payload.
     * @since 4.0.19
     */
    public static String prettyPrint(String jsonPayload, boolean disableUnicodeEscaping) {
        return prettyPrint(jsonPayload, disableUnicodeEscaping,
                Integer.getInteger("groovy.json.maxNestingDepth", BaseJsonParser.DEFAULT_MAX_NESTING_DEPTH));
    }

    /**
     * Pretty print a JSON payload, bounding its nesting depth explicitly.
     *
     * <p>
     * Every nesting level adds to the indent of each line it encloses, so output size grows with
     * the square of the payload's nesting and an unbounded payload can exhaust memory. A payload
     * nested deeper than {@code maxNestingDepth} &mdash; or left unbalanced, which the lexer cannot
     * distinguish from nesting &mdash; is rejected with a {@link JsonException}. Pass {@code 0} or
     * less to disable the check.
     *
     * <p>
     * The other overloads take this bound from the {@code groovy.json.maxNestingDepth} system
     * property, which applies process-wide; prefer this method to format a deeply nested payload
     * without changing how the rest of the process parses JSON.
     *
     * @param jsonPayload a JSON payload
     * @param disableUnicodeEscaping whether to disable unicode escaping
     * @param maxNestingDepth the maximum nesting depth to accept; {@code 0} or less disables the check
     * @return a pretty representation of JSON payload.
     * @throws JsonException if the payload nests deeper than {@code maxNestingDepth}
     * @since 6.0.0
     */
    public static String prettyPrint(String jsonPayload, boolean disableUnicodeEscaping, int maxNestingDepth) {
        int indentSize = 0;
        int nestingDepth = 0;
        // Just a guess that the pretty view will take 20 percent more than original.
        final CharBuf output = CharBuf.create((int) (jsonPayload.length() * 1.2));

        JsonLexer lexer = new JsonLexer(new StringReader(jsonPayload));
        // Will store already created indents.
        Map<Integer, char[]> indentCache = new HashMap<>();
        while (lexer.hasNext()) {
            JsonToken token = lexer.next();
            switch (token.getType()) {
                case OPEN_CURLY:
                    nestingDepth = enterNesting(nestingDepth, maxNestingDepth);
                    indentSize += 4;
                    output.addChars(Chr.array(OPEN_BRACE, NEW_LINE)).addChars(getIndent(indentSize, indentCache));

                    break;
                case CLOSE_CURLY:
                    if (nestingDepth > 0) {
                        nestingDepth--;
                    }
                    indentSize -= 4;
                    output.addChar(NEW_LINE);
                    if (indentSize > 0) {
                        output.addChars(getIndent(indentSize, indentCache));
                    }
                    output.addChar(CLOSE_BRACE);

                    break;
                case OPEN_BRACKET:
                    nestingDepth = enterNesting(nestingDepth, maxNestingDepth);
                    indentSize += 4;
                    output.addChars(Chr.array(OPEN_BRACKET, NEW_LINE)).addChars(getIndent(indentSize, indentCache));

                    break;
                case CLOSE_BRACKET:
                    if (nestingDepth > 0) {
                        nestingDepth--;
                    }
                    indentSize -= 4;
                    output.addChar(NEW_LINE);
                    if (indentSize > 0) {
                        output.addChars(getIndent(indentSize, indentCache));
                    }
                    output.addChar(CLOSE_BRACKET);

                    break;
                case COMMA:
                    output.addChars(Chr.array(COMMA, NEW_LINE)).addChars(getIndent(indentSize, indentCache));

                    break;
                case COLON:
                    output.addChars(Chr.array(COLON, SPACE));

                    break;
                case STRING:
                    String textStr = token.getText();
                    String textWithoutQuotes = textStr.substring(1, textStr.length() - 1);
                    if (!textWithoutQuotes.isEmpty()) {
                        output.addJsonEscapedString(textWithoutQuotes, disableUnicodeEscaping);
                    } else {
                        output.addQuoted(Chr.array());
                    }

                    break;
                default:
                    output.addString(token.getText());
            }
        }

        return output.toString();
    }

    /**
     * Records entry into a nested array or object, enforcing the configured maximum nesting depth.
     * Bounding the depth also bounds the indent cache and the size of the output: each level adds
     * four characters to every indent it encloses, so an unbounded document costs memory quadratic
     * in its own nesting.
     *
     * @param nestingDepth the depth before this token
     * @param maxNestingDepth the maximum permitted depth; {@code 0} or less disables the check
     * @return the depth after this token
     * @throws JsonException if the configured maximum nesting depth would be exceeded
     */
    private static int enterNesting(int nestingDepth, int maxNestingDepth) {
        if (maxNestingDepth > 0 && nestingDepth + 1 > maxNestingDepth) {
            throw new JsonException("Maximum JSON nesting depth of " + maxNestingDepth + " exceeded");
        }
        return nestingDepth + 1;
    }

    /**
     * Creates a new indent if it doesn't exist in the indent cache.
     *
     * @return indent with the specified size.
     */
    private static char[] getIndent(int indentSize, Map<Integer, char[]> indentCache) {
        char[] indent = indentCache.get(indentSize);
        if (indent == null) {
            indent = new char[indentSize];
            Arrays.fill(indent, SPACE);
            indentCache.put(indentSize, indent);
        }

        return indent;
    }

    /**
     * Obtains JSON unescaped text for the given text
     *
     * @param text The text
     * @return The unescaped text
     */
    public static JsonUnescaped unescaped(CharSequence text) {
        return new JsonUnescaped(text);
    }

    /**
     * Represents unescaped JSON
     */
    public static class JsonUnescaped {
        private final CharSequence text;

        /**
         * Creates a wrapper for raw JSON text that should not be escaped.
         *
         * @param text the raw JSON text
         */
        public JsonUnescaped(CharSequence text) {
            this.text = text;
        }

        /**
         * Returns the raw JSON text.
         *
         * @return the unescaped JSON text
         */
        public CharSequence getText() {
            return text;
        }

        /**
         * Returns the wrapped raw JSON text.
         *
         * @return the unescaped JSON text
         */
        @Override
        public String toString() {
            return text.toString();
        }
    }

}
