/*
 * 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.Bson.END_OF_DOCUMENT;
import static org.modeshape.schematic.document.Bson.END_OF_STRING;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.util.Date;
import java.util.Iterator;
import java.util.UUID;
import java.util.regex.Pattern;
import org.modeshape.schematic.annotation.ThreadSafe;
import org.modeshape.schematic.document.Binary;
import org.modeshape.schematic.document.Bson.BinaryType;
import org.modeshape.schematic.document.Bson.Type;
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.MaxKey;
import org.modeshape.schematic.document.MinKey;
import org.modeshape.schematic.document.Null;
import org.modeshape.schematic.document.ObjectId;
import org.modeshape.schematic.document.Symbol;
import org.modeshape.schematic.document.Timestamp;
import org.modeshape.schematic.internal.io.BsonDataOutput;

/**
 * A component that writes BSON representations from the in-memory {@link Document} representation.
 * 
 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
 */
@ThreadSafe
public class BsonWriter {

    /**
     * Write the supplied in-memory {@link Document} in standard BSON binary format to the supplied stream.
     * 
     * @param object 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 void write( Object object,
                       OutputStream stream ) throws IOException {
        BsonDataOutput buffer = new BsonDataOutput();
        write(null, object, buffer);
        buffer.writeTo(stream);
    }

    /**
     * Return the array of bytes containing the standard BSON binary form of the supplied in-memory {@link Document}.
     * 
     * @param object the BSON object or BSON value; may not be null
     * @return the bytes
     * @throws IOException if there was a problem reading from the stream
     */
    public byte[] write( Object object ) throws IOException {
        BsonDataOutput buffer = new BsonDataOutput();
        write(null, object, buffer);
        ByteArrayOutputStream stream = new ByteArrayOutputStream(buffer.size());
        buffer.writeTo(stream);
        return stream.toByteArray();
    }

    /**
     * Write to the supplied output the binary BSON representation of the supplied in-memory {@link Document}.
     * 
     * @param object the BSON object or BSON value; may not be null
     * @param output the output; may not be null
     * @throws IOException if there was a problem writing to the ObjectOutput
     */
    public void write( Object object,
                       DataOutput output ) throws IOException {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        write(object, stream);
        stream.flush();
        byte[] bytes = stream.toByteArray();
        output.write(bytes);
    }

    protected void write( String name,
                          Object object,
                          BsonDataOutput output ) {
        if (object == null) {
            writeNull(name, output);
        } else if (object instanceof String) {
            write(name, (String)object, output);
        } else if (object instanceof Boolean) {
            write(name, ((Boolean)object).booleanValue(), output);
        } else if (object instanceof Integer) {
            write(name, ((Integer)object).intValue(), output);
        } else if (object instanceof Long) {
            write(name, ((Long)object).longValue(), output);
        } else if (object instanceof Float) {
            write(name, ((Float)object).floatValue(), output);
        } else if (object instanceof Double) {
            write(name, ((Double)object).doubleValue(), output);
        } else if (object.getClass().isArray()) {
            writeArray(name, object, output);
        } else if (object instanceof ArrayEditor) {
            write(name, (Iterable<?>)((ArrayEditor)object).unwrap(), output);
        } else if (object instanceof DocumentEditor) {
            write(name, ((DocumentEditor)object).unwrap(), output);
        } else if (object instanceof Iterable) { // must check before 'BsonObject' because of inheritance
            write(name, (Iterable<?>)object, output);
        } else if (object instanceof Document) {
            write(name, (Document)object, output);
        } else if (object instanceof Binary) {
            write(name, (Binary)object, output);
        } else if (object instanceof Symbol) {
            write(name, (Symbol)object, output);
        } else if (object instanceof Pattern) {
            write(name, (Pattern)object, output);
        } else if (object instanceof Date) {
            write(name, (Date)object, output);
        } else if (object instanceof UUID) {
            write(name, (UUID)object, output);
        } else if (object instanceof CodeWithScope) { // must check before 'Code' because of inheritance
            write(name, (CodeWithScope)object, output);
        } else if (object instanceof Code) {
            write(name, (Code)object, output);
        } else if (object instanceof Timestamp) {
            write(name, (Timestamp)object, output);
        } else if (object instanceof ObjectId) {
            write(name, (ObjectId)object, output);
        } else if (object instanceof MaxKey) {
            write(name, (MaxKey)object, output);
        } else if (object instanceof MinKey) {
            write(name, (MinKey)object, output);
        } else if (object instanceof Null) { // do this last because it is rare ...
            writeNull(name, output);
        } else {
            throw new RuntimeException("Unable to serialize type \'" + object.getClass() + "\" to BSON");
        }
    }

    protected void writeCString( String value,
                                 BsonDataOutput output ) {
        output.writeUTFString(value);
        output.writeByte(END_OF_STRING);
    }

    protected void writeString( String value,
                                BsonDataOutput output ) {
        // Write out the size of the string; use '0' now and rewrite it ...
        int position = output.size();
        output.writeInt(0);
        int len = output.writeUTFString(value);
        output.writeByte(END_OF_STRING);
        // Now write the length ...
        assert (len + 1) >= 0;
        output.writeInt(position, len + 1);// +1 for the zero-byte
    }

    protected void writeNull( String name,
                              BsonDataOutput output ) {
        output.writeByte(Type.NULL);
        writeCString(name, output);
    }

    protected void write( String name,
                          String value,
                          BsonDataOutput output ) {
        output.writeByte(Type.STRING);
        writeCString(name, output);
        writeString(value, output);
    }

    protected void write( String name,
                          boolean value,
                          BsonDataOutput output ) {
        output.writeByte(Type.BOOLEAN);
        writeCString(name, output);
        output.writeByte(value ? (byte)0x01 : (byte)0x00);
    }

    protected void write( String name,
                          int value,
                          BsonDataOutput output ) {
        output.writeByte(Type.INT32);
        writeCString(name, output);
        output.writeInt(value);
    }

    protected void write( String name,
                          long value,
                          BsonDataOutput output ) {
        output.writeByte(Type.INT64);
        writeCString(name, output);
        output.writeLong(value);
    }

    protected void write( String name,
                          float value,
                          BsonDataOutput output ) {
        output.writeByte(Type.DOUBLE);
        writeCString(name, output);
        output.writeDouble(value);
    }

    protected void write( String name,
                          double value,
                          BsonDataOutput output ) {
        output.writeByte(Type.DOUBLE);
        writeCString(name, output);
        output.writeDouble(value);
    }

    protected void writeArray( String name,
                               Object arrayValue,
                               BsonDataOutput output ) {
        if (name != null) {
            output.writeByte(Type.ARRAY);
            writeCString(name, output);
        }
        // Write the size for the array; we'll come back to this after we write the array ...
        int arraySizePosition = output.size();
        output.writeInt(0);

        // Now write out the array as a document ...
        int length = Array.getLength(arrayValue);
        Iterator<String> indexIter = IndexSequence.infiniteSequence();
        for (int i = 0; i != length; ++i) {
            String elementName = indexIter.next(); // the strings are shared/reused
            Object value = Array.get(arrayValue, i);
            write(elementName, value, output);
        }
        output.writeByte(END_OF_DOCUMENT);

        // Determine the number of bytes written in the array, and overwrite the value we wrote earlier ..
        int arraySize = output.size() - arraySizePosition;
        output.writeInt(arraySizePosition, arraySize);
    }

    protected void write( String name,
                          Iterable<?> arrayValue,
                          BsonDataOutput output ) {
        if (name != null) {
            output.writeByte(Type.ARRAY);
            writeCString(name, output);
        }
        // Write the size for the array; we'll come back to this after we write the array ...
        int arraySizePosition = output.size();
        output.writeInt(0);
        // Now write out the array as a document ...
        Iterator<String> indexIter = IndexSequence.infiniteSequence();
        Iterator<?> valueIter = arrayValue.iterator();
        while (valueIter.hasNext()) {
            String elementName = indexIter.next(); // the strings are shared/reused
            Object value = valueIter.next();
            write(elementName, value, output);
        }
        output.writeByte(END_OF_DOCUMENT);

        // Determine the number of bytes written in the array, and overwrite the value we wrote earlier ..
        int arraySize = output.size() - arraySizePosition;
        output.writeInt(arraySizePosition, arraySize);
    }

    protected void write( String name,
                          Document document,
                          BsonDataOutput output ) {
        if (name != null) {
            output.writeByte(Type.DOCUMENT);
            writeCString(name, output);
        }
        // Write the size for the document; we'll come back to this after we write the array ...
        int arraySizePosition = output.size();
        output.writeInt(-1);

        for (Field field : document.fields()) {
            write(field.getName(), field.getValue(), output);
        }
        output.writeByte(END_OF_DOCUMENT);

        // Determine the number of bytes written in the array, and overwrite the value we wrote earlier ..
        int arraySize = output.size() - arraySizePosition;
        output.writeInt(arraySizePosition, arraySize);
    }

    protected void write( String name,
                          Binary value,
                          BsonDataOutput output ) {
        output.writeByte(Type.BINARY);
        writeCString(name, output);
        byte[] bytes = value.getBytes();
        output.writeInt(bytes.length);
        output.writeByte(value.getType());
        output.write(bytes);
    }

    protected void write( String name,
                          Symbol value,
                          BsonDataOutput output ) {
        output.writeByte(Type.SYMBOL);
        writeCString(name, output);
        writeString(value.getSymbol(), output);
    }

    protected void write( String name,
                          Pattern value,
                          BsonDataOutput output ) {
        output.writeByte(Type.REGEX);
        writeCString(name, output);
        writeCString(value.pattern(), output);
        writeCString(BsonUtils.regexFlagsFor(value), output);
    }

    protected void write( String name,
                          Date value,
                          BsonDataOutput output ) {
        output.writeByte(Type.DATETIME);
        writeCString(name, output);
        output.writeLong(value.getTime());
    }

    protected void write( String name,
                          UUID value,
                          BsonDataOutput output ) {
        output.writeByte(Type.BINARY);
        writeCString(name, output);
        output.writeInt(16);
        output.writeByte(BinaryType.UUID);
        output.writeLong(value.getMostSignificantBits());
        output.writeLong(value.getLeastSignificantBits());
    }

    protected void write( String name,
                          CodeWithScope value,
                          BsonDataOutput output ) {
        output.writeByte(Type.JAVASCRIPT_WITH_SCOPE);
        writeCString(name, output);
        // Write the size for the CodeWithScope; we'll come back to this after we write the object ...
        int arraySizePosition = output.size();
        output.writeInt(0);

        // Write the code & scope ...
        writeString(value.getCode(), output);
        write(null, value.getScope(), output);

        // Determine the number of bytes written in the array, and overwrite the value we wrote earlier ..
        int arraySize = output.size() - arraySizePosition;
        output.writeInt(arraySizePosition, arraySize);
    }

    protected void write( String name,
                          Code value,
                          BsonDataOutput output ) {
        output.writeByte(Type.JAVASCRIPT);
        writeCString(name, output);
        writeString(value.getCode(), output);
    }

    protected void write( String name,
                          Timestamp value,
                          BsonDataOutput output ) {
        output.writeByte(Type.TIMESTAMP);
        writeCString(name, output);
        output.writeInt(value.getInc());
        output.writeInt(value.getTime());
    }

    protected void write( String name,
                          ObjectId value,
                          BsonDataOutput output ) {
        output.writeByte(Type.OBJECTID);
        writeCString(name, output);
        output.write(value.getBytes());
    }

    protected void write( String name,
                          MaxKey value,
                          BsonDataOutput output ) {
        output.writeByte(Type.MAXKEY);
        writeCString(name, output);
    }

    protected void write( String name,
                          MinKey value,
                          BsonDataOutput output ) {
        output.writeByte(Type.MINKEY);
        writeCString(name, output);
    }
}
