/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.schematic.document;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.modeshape.schematic.internal.document.CompactJsonWriter;
import org.modeshape.schematic.internal.document.JsonReader;
import org.modeshape.schematic.internal.document.JsonWriter;
import org.modeshape.schematic.internal.document.PrettyJsonWriter;

/**
 * A utility class for working with JSON documents. This class is able to read and write JSON documents that are in a special
 * modified format. <h3>Modified Format</h3> Any JSON document written in this modified format is still a valid JSON document.
 * However, certain value types supported by BSON are written as nested objects in particular patterns. In fact, it is nearly
 * identical to the JSON serialization used by MongoDB. All standard JSON values are written as expected, but the types unique to
 * BSON are written as follows:
 * <p>
 * <table border="1" cellspacing="0" cellpadding="3">
 * <tr>
 * <th>BSON Type</th>
 * <th>Class</th>
 * <th>Format</th>
 * <th>Example</th>
 * </tr>
 * <tr>
 * <td>Symbol</td>
 * <td>{@link Symbol}</td>
 * <td>"<i>value</i>"</td>
 * <td>"The quick brown fox"</td>
 * </tr>
 * <tr>
 * <td>Regular Expression</td>
 * <td>{@link Pattern}</td>
 * <td>{ "$regex" : "<i>pattern</i>", "$otions" : "<i>flags</i>" }</td>
 * <td>{ "$regex" : "[CH]at\sin", "$options" : "im" }</td>
 * </tr>
 * <tr>
 * <td>Date</td>
 * <td>{@link Date}</td>
 * <td>{ "$date" : "<i>yyyy-MM-dd</i>T<i>HH:mm:ss</i>Z" }</td>
 * <td>{ "$date" : "2011-06-11T08:44:25Z" }</td>
 * </tr>
 * <tr>
 * <td>Timestamp</td>
 * <td>{@link Timestamp}</td>
 * <td>{ "$ts" : <i>timeValue</i>, "$inc" : <i>incValue</i> }</td>
 * <td>"\/TS("2011-06-11T08:44:25Z")\/"</td>
 * </tr>
 * <tr>
 * <td>ObjectId</td>
 * <td>{@link ObjectId}</td>
 * <td>{ "$oid" : "<i>12bytesOfIdInBase16</i>" }</td>
 * <td>{ "$oid" : "0000012c0000c8000900000f" }</td>
 * </tr>
 * <tr>
 * <td>Binary</td>
 * <td>{@link Binary}</td>
 * <td>{ "$type" : <i>typeAsInt</i>, "$base64" : "<i>bytesInBase64</i>" }"</td>
 * <td>{ "$type" : 0, "$base64" : "TWFuIGlzIGRpc3R" }"</td>
 * </tr>
 * <tr>
 * <td>UUID</td>
 * <td>{@link UUID}</td>
 * <td>{ "$uuid" : "<i>string-form-of-uuid</i>" }</td>
 * <td>{ "$uuid" : "09e0e949-bba4-459c-bb1d-9352e5ee8958" }</td>
 * </tr>
 * <tr>
 * <td>Code</td>
 * <td>{@link Code}</td>
 * <td>{ "$code" : "<i>code</i>" }</td>
 * <td>{ "$code" : "244-I2" }</td>
 * </tr>
 * <tr>
 * <td>CodeWithScope</td>
 * <td>{@link CodeWithScope}</td>
 * <td>{ "$code" : "<i>code</i>", "$scope" : <i>scope document</i> }</td>
 * <td>{ "$code" : "244-I2", "$scope" : { "name" : "Joe" } }</td>
 * </tr>
 * <tr>
 * <td>MinKey</td>
 * <td>{@link MinKey}</td>
 * <td>"MinKey"</td>
 * <td>"MinKey"</td>
 * </tr>
 * <tr>
 * <td>MaxKey</td>
 * <td>{@link MaxKey}</td>
 * <td>"MaxKey"</td>
 * <td>"MaxKey"</td>
 * </tr>
 * <tr>
 * <td>Null value</td>
 * <td>n/a</td>
 * <td>null</td>
 * <td>null</td>
 * </tr>
 * </table>
 * </p>
 * 
 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
 */
public class Json {

    public static final Charset UTF8;
    static {
        Charset charset = Charset.defaultCharset();
        try {
            charset = Charset.forName("UTF-8");
        } catch (Throwable t) {
            Logger.getLogger(CompactJsonWriter.class.getName()).log(Level.SEVERE,
                                                                    "Unable to obtain 'UTF-8' character set for JSON writing; using default charset.");
        }
        UTF8 = charset;
    }

    private static final CompactJsonWriter SHARED_COMPACT_WRITER = new CompactJsonWriter();

    /**
     * A set of field names that are reserved for special formatting of non-standard JSON value types as nested objects.
     * 
     * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
     * @since 5.1
     */
    public class ReservedField {
        /**
         * The "$oid" field name, used within an {@link ObjectId} value. A single ObjectId will be written
         */
        public static final String OBJECT_ID = "$oid";
        public static final String DATE = "$date";
        public static final String TIMESTAMP = "$ts";
        public static final String INCREMENT = "$inc";
        public static final String REGEX_PATTERN = "$regex";
        public static final String REGEX_OPTIONS = "$options";
        public static final String BINARY_TYPE = "$type";
        public static final String BASE_64 = "$base64";
        public static final String UUID = "$uuid";
        public static final String CODE = "$code";
        public static final String SCOPE = "$scope";
    }

    protected static JsonWriter getCompactJsonWriter() {
        return SHARED_COMPACT_WRITER;
    }

    protected static JsonWriter getPrettyWriter() {
        return new PrettyJsonWriter();
    }

    private static final JsonReader SHARED_READER = new JsonReader();

    protected static JsonReader getReader() {
        return SHARED_READER;
    }

    /**
     * Read the JSON representation from supplied URL and construct the {@link Document} representation, using the
     * {@link Charset#defaultCharset() default character set}.
     * <p>
     * This method will read standard JSON and modified JSON, and tolerates whitespace and use of several delimeters, including
     * the standard ':' as well as '=' and '=>'.
     * </p>
     * 
     * @param url the URL of the JSON document; may not be null and must be resolvable
     * @return the in-memory {@link Document} representation
     * @throws ParsingException if there was a problem reading from the stream
     */
    public static Document read( URL url ) throws ParsingException {
        return SHARED_READER.read(url);
    }

    /**
     * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the
     * {@link Charset#defaultCharset() default character set}.
     * <p>
     * This method will read standard JSON and modified JSON, and tolerates whitespace and use of several delimeters, including
     * the standard ':' as well as '=' and '=>'.
     * </p>
     * 
     * @param stream the input stream; may not be null
     * @return the in-memory {@link Document} representation
     * @throws ParsingException if there was a problem reading from the stream
     */
    public static Document read( InputStream stream ) throws ParsingException {
        return SHARED_READER.read(stream);
    }

    /**
     * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the
     * {@link Charset#defaultCharset() default character set}.
     * <p>
     * This method will read standard JSON and modified JSON, and tolerates whitespace and use of several delimeters, including
     * the standard ':' as well as '=' and '=>'.
     * </p>
     *
     * @param stream the input stream; may not be null
     * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise
     *
     * @return the in-memory {@link Document} representation
     * @throws ParsingException if there was a problem reading from the stream
     */
    public static Document read( InputStream stream, boolean introspectStringValues ) throws ParsingException {
        return SHARED_READER.read(stream, introspectStringValues);
    }

    /**
     * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the
     * supplied {@link Charset character set}.
     * <p>
     * This method will read standard JSON and modified JSON, and tolerates whitespace and use of several delimeters, including
     * the standard ':' as well as '=' and '=>'.
     * </p>
     * 
     * @param stream the input stream; may not be null
     * @param charset the character set that should be used; may not be null
     * @return the in-memory {@link Document} representation
     * @throws ParsingException if there was a problem reading from the stream
     */
    public static Document read( InputStream stream,
                                 Charset charset ) throws ParsingException {
        return SHARED_READER.read(stream, charset);
    }

    /**
     * Read the JSON representation from supplied input stream and construct the {@link Document} representation.
     * <p>
     * This method will read standard JSON and modified JSON, and tolerates whitespace and use of several delimeters, including
     * the standard ':' as well as '=' and '=>'.
     * </p>
     * 
     * @param reader the IO reader; may not be null
     * @return the in-memory {@link Document} representation
     * @throws ParsingException if there was a problem reading from the stream
     */
    public static Document read( Reader reader ) throws ParsingException {
        return SHARED_READER.read(reader);
    }

    /**
     * Read the supplied JSON representation and construct the {@link Document} representation.
     * <p>
     * This method will read standard JSON and modified JSON, and tolerates whitespace and use of several delimeters, including
     * the standard ':' as well as '=' and '=>'.
     * </p>
     * 
     * @param json the JSON document string; may not be null
     * @return the in-memory {@link Document} representation
     * @throws ParsingException if there was a problem reading from the stream
     */
    public static Document read( String json ) throws ParsingException {
        return SHARED_READER.read(json);
    }

    /**
     * Return a {@link DocumentSequence} that can be used to pull multiple documents from the stream.
     *
     * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise
     * @param stream the input stream; may not be null
     * @return the sequence that can be used to get one or more Document instances from a single input
     */
    public static DocumentSequence readMultiple( InputStream stream, boolean introspectStringValues ) {
        return SHARED_READER.readMultiple(stream, introspectStringValues);
    }

    /**
     * Return a {@link DocumentSequence} that can be used to pull multiple documents from the stream.
     * 
     * @param reader the IO reader; may not be null
     * @return the sequence that can be used to get one or more Document instances from a single input
     */
    public static DocumentSequence readMultiple( Reader reader ) {
        return SHARED_READER.readMultiple(reader);
    }

    /**
     * Return the modified JSON representation for the supplied in-memory {@link Document}. The resulting JSON will have no
     * embedded line feeds or extra spaces.
     * <p>
     * This format is compact and easy for software to read, but usually very difficult for people to read anything but very small
     * documents.
     * </p>
     * 
     * @param bson the BSON object or BSON value; may not be null
     * @return the string; may not be null
     */
    public static String write( Document bson ) {
        return getCompactJsonWriter().write(bson);
    }

    /**
     * Return the modified JSON representation for the supplied object value. The resulting JSON will have no embedded line feeds
     * or extra spaces.
     * <p>
     * This format is compact and easy for software to read, but usually very difficult for people to read anything but very small
     * documents.
     * </p>
     * 
     * @param value the BSON object or BSON value; may not be null
     * @return the string; may not be null
     */
    public static String write( Object value ) {
        return getCompactJsonWriter().write(value);
    }

    /**
     * Write to the supplied writer the modified JSON representation of the supplied in-memory {@link Document}. The resulting
     * JSON will have no embedded line feeds or extra spaces.
     * <p>
     * This format is compact and easy for software to read, but usually very difficult for people to read anything but very small
     * documents.
     * </p>
     * 
     * @param bson the BSON object or BSON value; may not be null
     * @param writer the writer; may not be null
     * @throws IOException if there was a problem reading from the stream
     */
    public static void write( Document bson,
                              Writer writer ) throws IOException {
        getCompactJsonWriter().write(bson, writer);
    }

    /**
     * Write to the supplied writer the modified JSON representation of the supplied in-memory {@link Document}. The resulting
     * JSON will have no embedded line feeds or extra spaces.
     * <p>
     * This format is compact and easy for software to read, but usually very difficult for people to read anything but very small
     * documents.
     * </p>
     * 
     * @param bson the BSON object or BSON value; may not be null
     * @param stream the output stream; may not be null
     * @throws IOException if there was a problem reading from the stream
     */
    public static void write( Document bson,
                              OutputStream stream ) throws IOException {
        getCompactJsonWriter().write(bson, stream);
    }

    /**
     * Return the modified JSON representation for the supplied in-memory {@link Document}. The resulting JSON will be indented
     * for each name/value pair and each array value.
     * <p>
     * This format is very readable by people and software, but is less compact due to the extra whitespace.
     * </p>
     * 
     * @param bson the BSON object or BSON value; may not be null
     * @return the JSON representation; never null
     */
    public static String writePretty( Document bson ) {
        return getPrettyWriter().write(bson);
    }

    /**
     * Return the modified JSON representation for the supplied object value. The resulting JSON will be indented for each
     * name/value pair and each array value.
     * <p>
     * This format is very readable by people and software, but is less compact due to the extra whitespace.
     * </p>
     * 
     * @param value the BSON object or BSON value; may not be null
     * @return the JSON representation; never null
     */
    public static String writePretty( Object value ) {
        return getPrettyWriter().write(value);
    }

    /**
     * Write to the supplied writer the modified JSON representation of the supplied in-memory {@link Document}. The resulting
     * JSON will be indented for each name/value pair and each array value.
     * <p>
     * This format is very readable by people and software, but is less compact due to the extra whitespace.
     * </p>
     * 
     * @param bson the BSON object or BSON value; may not be null
     * @param writer the writer; may not be null
     * @throws IOException if there was a problem reading from the stream
     */
    public static void writePretty( Document bson,
                                    Writer writer ) throws IOException {
        getPrettyWriter().write(bson, writer);
    }

    /**
     * Write to the supplied writer the modified JSON representation of the supplied in-memory {@link Document}. The resulting
     * JSON will be indented for each name/value pair and each array value.
     * <p>
     * This format is very readable by people and software, but is less compact due to the extra whitespace.
     * </p>
     * 
     * @param bson the BSON object or BSON value; may not be null
     * @param stream the output stream; may not be null
     * @throws IOException if there was a problem reading from the stream
     */
    public static void writePretty( Document bson,
                                    OutputStream stream ) throws IOException {
        getPrettyWriter().write(bson, stream);
    }

}
