// BSONEncoder.java

/**
 *      Copyright (C) 2008 10gen Inc.
 *
 *   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.bson;

import com.mongodb.DBRefBase;
import org.bson.io.BasicOutputBuffer;
import org.bson.io.OutputBuffer;
import org.bson.types.BSONTimestamp;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.bson.types.Symbol;

import java.lang.reflect.Array;
import java.nio.Buffer;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;

import static org.bson.BSON.ARRAY;
import static org.bson.BSON.BINARY;
import static org.bson.BSON.BOOLEAN;
import static org.bson.BSON.B_BINARY;
import static org.bson.BSON.B_GENERAL;
import static org.bson.BSON.B_UUID;
import static org.bson.BSON.CODE;
import static org.bson.BSON.CODE_W_SCOPE;
import static org.bson.BSON.DATE;
import static org.bson.BSON.EOO;
import static org.bson.BSON.MAXKEY;
import static org.bson.BSON.MINKEY;
import static org.bson.BSON.NULL;
import static org.bson.BSON.NUMBER;
import static org.bson.BSON.NUMBER_INT;
import static org.bson.BSON.NUMBER_LONG;
import static org.bson.BSON.OBJECT;
import static org.bson.BSON.OID;
import static org.bson.BSON.REGEX;
import static org.bson.BSON.STRING;
import static org.bson.BSON.SYMBOL;
import static org.bson.BSON.TIMESTAMP;
import static org.bson.BSON.UNDEFINED;
import static org.bson.BSON.regexFlags;

/**
 * this is meant to be pooled or cached
 * there is some per instance memory for string conversion, etc...
 */
@SuppressWarnings("unchecked")
public class BasicBSONEncoder implements BSONEncoder {
    
    static final boolean DEBUG = false;

    public BasicBSONEncoder(){

    }

    public byte[] encode( BSONObject o ){
        BasicOutputBuffer buf = new BasicOutputBuffer();
        set( buf );
        putObject( o );
        done();
        return buf.toByteArray();
    }

    public void set( OutputBuffer out ){
        if ( _buf != null )
            throw new IllegalStateException( "in the middle of something" );
        
        _buf = out;
    }
 
    public void done(){
        _buf = null;
    }
   
    /**
     * @return true if object was handled
     */
    protected boolean handleSpecialObjects( String name , BSONObject o ){
        return false;
    }
    
    protected boolean putSpecial( String name , Object o ){
        return false;
    }

    /** Encodes a <code>BSONObject</code>.
     * This is for the higher level api calls
     * @param o the object to encode
     * @return the number of characters in the encoding
     */
    public int putObject( BSONObject o ){
        return putObject( null , o );
    }

    /**
     * this is really for embedded objects
     */
    protected int putObject( String name , BSONObject o ){

        if ( o == null )
            throw new NullPointerException( "can't save a null object" );

        if ( DEBUG ) System.out.println( "putObject : " + name + " [" + o.getClass() + "]" + " # keys " + o.keySet().size() );
        
        final int start = _buf.getPosition();
        
        byte myType = OBJECT;
        if ( o instanceof List )
            myType = ARRAY;

        if ( handleSpecialObjects( name , o ) )
            return _buf.getPosition() - start;
        
        if ( name != null ){
            _put( myType , name );
        }

        final int sizePos = _buf.getPosition();
        _buf.writeInt( 0 ); // leaving space for this.  set it at the end

        List transientFields = null;
        boolean rewriteID = myType == OBJECT && name == null;
        

        if ( myType == OBJECT ) {
            if ( rewriteID && o.containsField( "_id" ) )
                _putObjectField( "_id" , o.get( "_id" ) );
            
            {
                Object temp = o.get( "_transientFields" );
                if ( temp instanceof List )
                    transientFields = (List)temp;
            }
        }
        
        //TODO: reduce repeated code below.
        if ( o instanceof Map ){
	        for ( Entry<String, Object> e : ((Map<String, Object>)o).entrySet() ){
	        	
	            if ( rewriteID && e.getKey().equals( "_id" ) )
	                continue;
	            
	            if ( transientFields != null && transientFields.contains( e.getKey() ) )
	                continue;
	            
	            _putObjectField( e.getKey() , e.getValue() );
	
	        }        	
        } else {
	        for ( String s : o.keySet() ){
	
	            if ( rewriteID && s.equals( "_id" ) )
	                continue;
	            
	            if ( transientFields != null && transientFields.contains( s ) )
	                continue;
	            
	            Object val = o.get( s );
	
	            _putObjectField( s , val );
	
	        }
        }
        _buf.write( EOO );
        
        _buf.writeInt( sizePos , _buf.getPosition() - sizePos );
        return _buf.getPosition() - start;
    }

	protected void _putObjectField( String name , Object val ){

        if ( name.equals( "_transientFields" ) )
            return;
        
        if ( DEBUG ) System.out.println( "\t put thing : " + name );

        if ( name.contains( "\0" ) )
            throw new IllegalArgumentException( "Document field names can't have a NULL character. (Bad Key: '" + name + "')" );
        
        if ( name.equals( "$where") && val instanceof String ){
            _put( CODE , name );
            _putValueString( val.toString() );
            return;
        }
        
        val = BSON.applyEncodingHooks( val );

        if ( val == null )
            putNull(name);
        else if ( val instanceof Date )
            putDate( name , (Date)val );
        else if ( val instanceof Number )
            putNumber(name, (Number)val );
        else if ( val instanceof Character )
            putString(name, val.toString() );
        else if ( val instanceof String )
            putString(name, val.toString() );
        else if ( val instanceof ObjectId )
            putObjectId(name, (ObjectId)val );
        else if ( val instanceof BSONObject )
            putObject(name, (BSONObject)val );
        else if ( val instanceof Boolean )
            putBoolean(name, (Boolean)val );
        else if ( val instanceof Pattern )
            putPattern(name, (Pattern)val );
        else if ( val instanceof Map )
            putMap( name , (Map)val );
        else if ( val instanceof Iterable)
            putIterable( name , (Iterable)val );
        else if ( val instanceof byte[] )
            putBinary( name , (byte[])val );
        else if ( val instanceof Binary )
            putBinary( name , (Binary)val );
        else if ( val instanceof UUID )
            putUUID( name , (UUID)val );
        else if ( val.getClass().isArray() )
        	putArray( name , val );

        else if (val instanceof Symbol) {
            putSymbol(name, (Symbol) val);
        }
        else if (val instanceof BSONTimestamp) {
            putTimestamp( name , (BSONTimestamp)val );
        }
        else if (val instanceof CodeWScope) {
            putCodeWScope( name , (CodeWScope)val );
        }
        else if (val instanceof Code) {
            putCode( name , (Code)val );
        }
        else if (val instanceof DBRefBase) {
            BSONObject temp = new BasicBSONObject();
            temp.put("$ref", ((DBRefBase)val).getRef());
            temp.put("$id", ((DBRefBase)val).getId());
            putObject( name, temp );
        }
        else if ( val instanceof MinKey )
            putMinKey( name );
        else if ( val instanceof MaxKey )
            putMaxKey( name );
        else if ( putSpecial( name , val ) ){
            // no-op
        }
        else {
            throw new IllegalArgumentException( "can't serialize " + val.getClass() );
        }
        
    }
	
    private void putArray( String name , Object array ) {
        _put( ARRAY , name );
        final int sizePos = _buf.getPosition();
        _buf.writeInt( 0 );
                	        
        int size = Array.getLength(array);
        for ( int i = 0; i < size; i++ )
            _putObjectField( String.valueOf( i ) , Array.get( array, i ) );

        _buf.write( EOO );
        _buf.writeInt( sizePos , _buf.getPosition() - sizePos ); 
    }
	
    private void putIterable( String name , Iterable l ){
        _put( ARRAY , name );
        final int sizePos = _buf.getPosition();
        _buf.writeInt( 0 );
        
        int i=0;
        for ( Object obj: l ) {
            _putObjectField( String.valueOf( i ) , obj );
            i++;
        }
        	

        _buf.write( EOO );
        _buf.writeInt( sizePos , _buf.getPosition() - sizePos );        
    }
    
    private void putMap( String name , Map m ){
        _put( OBJECT , name );
        final int sizePos = _buf.getPosition();
        _buf.writeInt( 0 );
        
        for ( Map.Entry entry : (Set<Map.Entry>)m.entrySet() )
            _putObjectField( entry.getKey().toString() , entry.getValue() );

        _buf.write( EOO );
        _buf.writeInt( sizePos , _buf.getPosition() - sizePos );
    }
    

    protected void putNull( String name ){
        _put( NULL , name );
    }

    protected void putUndefined(String name){
        _put(UNDEFINED, name);
    }

    protected void putTimestamp(String name, BSONTimestamp ts ){
        _put( TIMESTAMP , name );
        _buf.writeInt( ts.getInc() );
        _buf.writeInt( ts.getTime() );
    }
    
    protected void putCodeWScope( String name , CodeWScope code ){
        _put( CODE_W_SCOPE , name );
        int temp = _buf.getPosition();
        _buf.writeInt( 0 );
        _putValueString( code.getCode() );
        putObject( code.getScope() );
        _buf.writeInt( temp , _buf.getPosition() - temp );
    }

    protected void putCode( String name , Code code ){
        _put( CODE , name );
        int temp = _buf.getPosition();
        _putValueString( code.getCode() );
    }

    protected void putBoolean( String name , Boolean b ){
        _put( BOOLEAN , name );
        _buf.write( b ? (byte)0x1 : (byte)0x0 );
    }

    protected void putDate( String name , Date d ){
        _put( DATE , name );
        _buf.writeLong( d.getTime() );
    }

    protected void putNumber( String name , Number n ){
		if ( n instanceof Integer ||
	             n instanceof Short ||
	             n instanceof Byte ||
	             n instanceof AtomicInteger ){
		    _put( NUMBER_INT , name );
		    _buf.writeInt( n.intValue() );
		}
	    else if ( n instanceof Long || n instanceof AtomicLong ) {
	        _put( NUMBER_LONG , name );
	        _buf.writeLong( n.longValue() );
	    }
	    else if ( n instanceof Float || n instanceof Double ) {
	      _put( NUMBER , name );
	      _buf.writeDouble( n.doubleValue() );
	    }
		else {
	        throw new IllegalArgumentException( "can't serialize " + n.getClass() );
		}
    }
    
    protected void putBinary( String name , byte[] data ){
        putBinary( name, B_GENERAL, data );
    }
    
    protected void putBinary( String name , Binary val ){
        putBinary( name, val.getType(), val.getData() );        
    }
    
    private void putBinary( String name , int type , byte[] data ){
        _put( BINARY , name );
        int totalLen = data.length;
        
        if (type == B_BINARY)
            totalLen += 4;
        
        _buf.writeInt( totalLen );
        _buf.write( type );
        if (type == B_BINARY)
            _buf.writeInt( totalLen -4 );
        int before = _buf.getPosition();
        _buf.write( data );
        int after = _buf.getPosition();
        com.mongodb.util.MyAsserts.assertEquals( after - before , data.length );
    }
    
    protected void putUUID( String name , UUID val ){
        _put( BINARY , name );
        _buf.writeInt( 16 );
        _buf.write( B_UUID );
        _buf.writeLong( val.getMostSignificantBits());
        _buf.writeLong( val.getLeastSignificantBits());
    }

    protected void putSymbol( String name , Symbol s ){
        _putString(name, s.getSymbol(), SYMBOL);
    }

    protected void putString(String name, String s) {
        _putString(name, s, STRING);
    }

    private void _putString( String name , String s, byte type ){
        _put( type , name );
        _putValueString( s );
    }

    protected void putObjectId( String name , ObjectId oid ){
        _put( OID , name );
        // according to spec, values should be stored big endian
        _buf.writeIntBE( oid._time() );
        _buf.writeIntBE( oid._machine() );
        _buf.writeIntBE( oid._inc() );
    }
    
    private void putPattern( String name, Pattern p ) {
        _put( REGEX , name );
        _put( p.pattern(), true );
        _put( regexFlags( p.flags() ), true );
    }

    private void putMinKey( String name ) {
        _put( MINKEY , name );
    }

    private void putMaxKey( String name ) {
        _put( MAXKEY , name );
    }


    // ----------------------------------------------
    
    /**
     * Encodes the type and key.
     * 
     */
    protected void _put( byte type , String name ){
        _buf.write( type );
        _put( name, true );
    }

    protected void _putValueString( String s ){
        int lenPos = _buf.getPosition();
        _buf.writeInt( 0 ); // making space for size
        int strLen = _put( s, false );
        _buf.writeInt( lenPos , strLen );
    }
    
    void _reset( Buffer b ){
        b.position(0);
        b.limit( b.capacity() );
    }

    /**
     * puts as utf-8 string
     */
    protected int _put( String str ){
        return _put(str, false);
    }

    /**
     * puts as utf-8 string
     */
    private int _put(String str, boolean checkForNullCharacters) {

        final int len = str.length();
        int total = 0;

        for ( int i=0; i<len; ){
            int c = Character.codePointAt( str , i );

            if (checkForNullCharacters && c == 0x0) {
                throw new BSONException(
                        String.format("BSON cstring '%s' is not valid because it contains a null character at index %d", str, i));
            }

            if ( c < 0x80 ){
                _buf.write( (byte)c );
                total += 1;
            }
            else if ( c < 0x800 ){
                _buf.write( (byte)(0xc0 + (c >> 6) ) );
                _buf.write( (byte)(0x80 + (c & 0x3f) ) );
                total += 2;
            }
            else if (c < 0x10000){
                _buf.write( (byte)(0xe0 + (c >> 12) ) );
                _buf.write( (byte)(0x80 + ((c >> 6) & 0x3f) ) );
                _buf.write( (byte)(0x80 + (c & 0x3f) ) );
                total += 3;
            }
            else {
                _buf.write( (byte)(0xf0 + (c >> 18)) );
                _buf.write( (byte)(0x80 + ((c >> 12) & 0x3f)) );
                _buf.write( (byte)(0x80 + ((c >> 6) & 0x3f)) );
                _buf.write( (byte)(0x80 + (c & 0x3f)) );
                total += 4;
            }
            
            i += Character.charCount(c);
        }  
        
        _buf.write( (byte)0 );
        total++;
        return total;
    }

    public void writeInt( int x ){
        _buf.writeInt( x );
    }

    public void writeLong( long x ){
        _buf.writeLong( x );
    }
    
    public void writeCString( String s ){
        _put(s, true);
    }

    protected OutputBuffer _buf;

}
