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

import static org.modeshape.schematic.document.Json.ReservedField.BASE_64;
import static org.modeshape.schematic.document.Json.ReservedField.BINARY_TYPE;
import static org.modeshape.schematic.document.Json.ReservedField.CODE;
import static org.modeshape.schematic.document.Json.ReservedField.DATE;
import static org.modeshape.schematic.document.Json.ReservedField.INCREMENT;
import static org.modeshape.schematic.document.Json.ReservedField.OBJECT_ID;
import static org.modeshape.schematic.document.Json.ReservedField.REGEX_OPTIONS;
import static org.modeshape.schematic.document.Json.ReservedField.REGEX_PATTERN;
import static org.modeshape.schematic.document.Json.ReservedField.SCOPE;
import static org.modeshape.schematic.document.Json.ReservedField.TIMESTAMP;
import static org.modeshape.schematic.document.Json.ReservedField.UUID;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import org.modeshape.schematic.document.Binary;
import org.modeshape.schematic.document.Bson;
import org.modeshape.schematic.document.Code;
import org.modeshape.schematic.document.CodeWithScope;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.Document.Field;
import org.modeshape.schematic.document.Json;
import org.modeshape.schematic.document.MaxKey;
import org.modeshape.schematic.document.MinKey;
import org.modeshape.schematic.document.ObjectId;
import org.modeshape.schematic.document.Symbol;
import org.modeshape.schematic.document.Timestamp;

public class CompactJsonWriter implements JsonWriter {

    @Override
    public void write( Object object,
                       OutputStream stream ) throws IOException {
        Writer writer = new BufferedWriter(new OutputStreamWriter(stream, Json.UTF8));
        write(object, writer);
        writer.flush();
    }

    @Override
    public void write( Object object,
                       Writer writer ) throws IOException {
        if (object == null) {
            writeNull(writer);
        } else if (object instanceof String) {
            write((String)object, writer);
        } else if (object instanceof Boolean) {
            write(((Boolean)object).booleanValue(), writer);
        } else if (object instanceof Integer) {
            write(((Integer)object).intValue(), writer);
        } else if (object instanceof Long) {
            write(((Long)object).longValue(), writer);
        } else if (object instanceof Float) {
            write(((Float)object).floatValue(), writer);
        } else if (object instanceof Double) {
            write(((Double)object).doubleValue(), writer);
        } else if (object.getClass().isArray()) {
            writeArray(object, writer);
        } else if (object instanceof ArrayEditor) {
            write((Iterable<?>)((ArrayEditor)object).unwrap(), writer);
        } else if (object instanceof DocumentEditor) {
            write(((DocumentEditor)object).unwrap(), writer);
        } else if (object instanceof Iterable) { // must check before 'BsonObject' because of inheritance
            write((Iterable<?>)object, writer);
        } else if (object instanceof Map) {
            write((Document)object, writer);
        } else if (object instanceof Binary) {
            write((Binary)object, writer);
        } else if (object instanceof Symbol) {
            write((Symbol)object, writer);
        } else if (object instanceof Pattern) {
            write((Pattern)object, writer);
        } else if (object instanceof Date) {
            write((Date)object, writer);
        } else if (object instanceof UUID) {
            write((UUID)object, writer);
        } else if (object instanceof CodeWithScope) { // must check before 'Code' because of inheritance
            write((CodeWithScope)object, writer);
        } else if (object instanceof Code) {
            write((Code)object, writer);
        } else if (object instanceof Timestamp) {
            write((Timestamp)object, writer);
        } else if (object instanceof Field) {
            write((Field)object, writer);
        } else if (object instanceof ObjectId) {
            write((ObjectId)object, writer);
        } else if (object instanceof MaxKey) {
            write((MaxKey)object, writer);
        } else if (object instanceof MinKey) {
            write((MinKey)object, writer);
        }
    }

    @Override
    public void write( Object object,
                       StringBuilder writer ) {
        try {
            write(object, new StringBuilderWriter(writer));
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }

    @Override
    public String write( Object object ) {
        StringBuilder sb = new StringBuilder();
        write(object, sb);
        return sb.toString();
    }

    protected void write( boolean value,
                          Writer writer ) throws IOException {
        writer.append(Boolean.toString(value));
    }

    protected void write( int value,
                          Writer writer ) throws IOException {
        writer.append(Integer.toString(value));
    }

    protected void write( long value,
                          Writer writer ) throws IOException {
        writer.append(Long.toString(value));
    }

    protected void write( float value,
                          Writer writer ) throws IOException {
        writer.append(Float.toString(value));
    }

    protected void write( double value,
                          Writer writer ) throws IOException {
        writer.append(Double.toString(value));
    }

    protected void writeNull( Writer writer ) throws IOException {
        writer.append("null");
    }

    protected void write( String value,
                          Writer writer ) throws IOException {
        // We need to escape certain characters found within the string ...
        writer.append('"');
        CharacterIterator iter = new StringCharacterIterator(value);
        for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
            switch (c) {
                case '"':
                case '\'':
                    // Escape the single- or double-quote characters ..
                    writer.append('\\').append(c);
                    break;
                case '/':
                    // JSON allows you to escape forward slashes (more for JavaScript), but we won't ...
                    // writer.append('\\');
                    writer.append(c);
                    break;
                case '\\':
                    // These characters need to be escaped.
                    writer.append('\\').append(c);
                    break;
                case '\b':
                    // These characters need to be escaped.
                    writer.append("\\b");
                    break;
                case '\f':
                    // These characters need to be escaped.
                    writer.append("\\f");
                    break;
                case '\n':
                    // These characters need to be escaped.
                    writer.append("\\n");
                    break;
                case '\r':
                    // These characters need to be escaped.
                    writer.append("\\r");
                    break;
                case '\t':
                    // These characters need to be escaped.
                    writer.append("\\t");
                    break;
                default:
                    if (c < ' ') {
                        // Characters less than the first printable character ' ' are non-printable
                        // and must be escaped as unicode ...
                        String t = "000" + Integer.toHexString(c);
                        writer.append("\\u").append(t.substring(t.length() - 4));
                    } else {
                        // Otherwise it's a normal character
                        writer.append(c);
                    }
                    break;
            }
        }
        writer.append('"');
    }

    protected void write( Symbol value,
                          Writer writer ) throws IOException {
        writer.append('"').append(value.getSymbol()).append('"');
    }

    protected void write( ObjectId value,
                          Writer writer ) throws IOException {
        write(new BasicDocument(OBJECT_ID, value.getBytesInBase16()), writer);
    }

    protected void write( Date value,
                          Writer writer ) throws IOException {
        String isoDate = Bson.getDateFormatter().format(value);
        write(new BasicDocument(DATE, isoDate), writer);
    }

    protected void write( Timestamp value,
                          Writer writer ) throws IOException {
        write(new BasicDocument(TIMESTAMP, value.getTime(), INCREMENT, value.getInc()), writer);
    }

    protected void write( MinKey value,
                          Writer writer ) throws IOException {
        write("MinKey", writer);
    }

    protected void write( MaxKey value,
                          Writer writer ) throws IOException {
        write("MaxKey", writer);
    }

    protected void write( Pattern value,
                          Writer writer ) throws IOException {
        BasicDocument obj = new BasicDocument(REGEX_PATTERN, value.pattern());
        String options = BsonUtils.regexFlagsFor(value);
        if (options.length() != 0) {
            obj.put(REGEX_OPTIONS, options);
        }
        write(obj, writer);
    }

    protected void write( Binary value,
                          Writer writer ) throws IOException {
        int type = value.getType() - 0;
        String base64 = value.getBytesInBase64();
        write(new BasicDocument(BINARY_TYPE, type, BASE_64, base64), writer);
    }

    protected void write( UUID value,
                          Writer writer ) throws IOException {
        write(new BasicDocument(UUID, value.toString()), writer);
    }

    protected void write( Code value,
                          Writer writer ) throws IOException {
        write(new BasicDocument(CODE, value.getCode()), writer);
    }

    protected void write( CodeWithScope value,
                          Writer writer ) throws IOException {
        write(new BasicDocument(CODE, value.getCode(), SCOPE, value.getScope()), writer);
    }

    protected void write( Field field,
                          Writer writer ) throws IOException {
        writer.append('"').append(field.getName()).append('"').append(' ').append(':').append(' ');
        write(field.getValue(), writer);
    }

    protected void write( Document bson,
                          Writer writer ) throws IOException {
        writer.append('{').append(' ');
        Iterator<Field> iter = bson.fields().iterator();
        if (iter.hasNext()) {
            write(iter.next(), writer);
            writer.append(' ');
            while (iter.hasNext()) {
                writer.append(',').append(' ');
                write(iter.next(), writer);
                writer.append(' ');
            }
        }
        writer.append('}');
    }

    protected void write( Iterable<?> arrayValue,
                          Writer writer ) throws IOException {
        writer.append('[');
        Iterator<?> iter = arrayValue.iterator();
        if (iter.hasNext()) {
            writer.append(' ');
            write(iter.next(), writer);
            while (iter.hasNext()) {
                writer.append(' ').append(',').append(' ');
                write(iter.next(), writer);
            }
        }
        writer.append(' ').append(']');
    }

    protected void writeArray( Object array,
                               Writer writer ) throws IOException {
        // Could transform this into a List, but this is more efficient ...
        writer.append('[');
        for (int i = 0, len = Array.getLength(array); i < len; i++) {
            if (i > 0) {
                writer.append(' ').append(',').append(' ');
            }
            write(Array.get(array, i), writer);
        }
        writer.append(' ').append(']');
    }

    protected static final class StringBuilderWriter extends Writer {

        private final StringBuilder builder;

        public StringBuilderWriter( final StringBuilder builder ) {
            this.builder = builder;
        }

        @Override
        public void write( final char[] cbuf,
                           final int off,
                           final int len ) {
            builder.append(cbuf, off, len);
        }

        @Override
        public Writer append( char c ) {
            builder.append(c);
            return this;
        }

        @Override
        public Writer append( CharSequence csq ) {
            builder.append(csq);
            return this;
        }

        @Override
        public Writer append( CharSequence csq,
                              int start,
                              int end ) {
            builder.append(csq, start, end);
            return this;
        }

        @Override
        public void write( final int c ) {
            builder.append(c);
        }

        @Override
        public void write( final char[] cbuf ) {
            builder.append(cbuf);
        }

        @Override
        public void write( final String str ) {
            builder.append(str);
        }

        @Override
        public void write( final String str,
                           final int off,
                           final int len ) {
            builder.append(str, off, len);
        }

        @Override
        public void flush() {
        }

        @Override
        public void close() {
        }
    }

}
