/*
 * Copyright (c) 2008-2014 MongoDB, 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.
 */

// BSON.java

package org.bson;

import org.bson.util.ClassMap;

import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import java.util.regex.Pattern;

public class BSON {

    static final Logger LOGGER = Logger.getLogger( "org.bson.BSON" );

    // ---- basics ----

    public static final byte EOO = 0;
    public static final byte NUMBER = 1;
    public static final byte STRING = 2;
    public static final byte OBJECT = 3;
    public static final byte ARRAY = 4;
    public static final byte BINARY = 5;
    public static final byte UNDEFINED = 6;
    public static final byte OID = 7;
    public static final byte BOOLEAN = 8;
    public static final byte DATE = 9;
    public static final byte NULL = 10;
    public static final byte REGEX = 11;
    public static final byte REF = 12;
    public static final byte CODE = 13;
    public static final byte SYMBOL = 14;
    public static final byte CODE_W_SCOPE = 15;
    public static final byte NUMBER_INT = 16;
    public static final byte TIMESTAMP = 17;
    public static final byte NUMBER_LONG = 18;

    public static final byte MINKEY = -1;
    public static final byte MAXKEY = 127;

    // --- binary types
    /*
       these are binary types
       so the format would look like
       <BINARY><name><BINARY_TYPE><...>
    */

    public static final byte B_GENERAL = 0;
    public static final byte B_FUNC = 1;
    public static final byte B_BINARY = 2;
    public static final byte B_UUID = 3;

    // ---- regular expression handling ----

    /** Converts a string of regular expression flags from the database in Java regular
     * expression flags.
     * @param flags flags from database
     * @return the Java flags
     */
    public static int regexFlags( String flags ){
        int fint = 0;
        if ( flags == null || flags.length() == 0 )
            return fint;

        flags = flags.toLowerCase();

        for( int i=0; i<flags.length(); i++ ) {
            RegexFlag flag = RegexFlag.getByCharacter( flags.charAt( i ) );
            if( flag != null ) {
                fint |= flag.javaFlag;
                if( flag.unsupported != null )
                    _warnUnsupportedRegex( flag.unsupported );
            }
            else {
                throw new IllegalArgumentException( "unrecognized flag ["+flags.charAt( i ) + "] " + (int)flags.charAt(i) );
            }
        }
        return fint;
    }

    public static int regexFlag( char c ){
        RegexFlag flag = RegexFlag.getByCharacter( c );
        if ( flag == null )
            throw new IllegalArgumentException( "unrecognized flag [" + c + "]" );

        if ( flag.unsupported != null ){
            _warnUnsupportedRegex( flag.unsupported );
            return 0;
        }

        return flag.javaFlag;
    }

    /** Converts Java regular expression flags into a string of flags for the database
     * @param flags Java flags
     * @return the flags for the database
     */
    public static String regexFlags( int flags ){
        StringBuilder buf = new StringBuilder();

        for( RegexFlag flag : RegexFlag.values() ) {
            if( ( flags & flag.javaFlag ) > 0 ) {
                buf.append( flag.flagChar );
                flags -= flag.javaFlag;
            }
        }

        if( flags > 0 )
            throw new IllegalArgumentException( "some flags could not be recognized." );

        return buf.toString();
    }

    private static enum RegexFlag {
        CANON_EQ( Pattern.CANON_EQ, 'c', "Pattern.CANON_EQ" ),
        UNIX_LINES(Pattern.UNIX_LINES, 'd', "Pattern.UNIX_LINES" ),
        GLOBAL( GLOBAL_FLAG, 'g', null ),
        CASE_INSENSITIVE( Pattern.CASE_INSENSITIVE, 'i', null ),
        MULTILINE(Pattern.MULTILINE, 'm', null ),
        DOTALL( Pattern.DOTALL, 's', "Pattern.DOTALL" ),
        LITERAL( Pattern.LITERAL, 't', "Pattern.LITERAL" ),
        UNICODE_CASE( Pattern.UNICODE_CASE, 'u', "Pattern.UNICODE_CASE" ),
        COMMENTS( Pattern.COMMENTS, 'x', null );

        private static final Map<Character, RegexFlag> byCharacter = new HashMap<Character, RegexFlag>();

        static {
            for (RegexFlag flag : values()) {
                byCharacter.put(flag.flagChar, flag);
            }
        }

        public static RegexFlag getByCharacter(char ch) {
            return byCharacter.get(ch);
        }
        public final int javaFlag;
        public final char flagChar;
        public final String unsupported;

        RegexFlag( int f, char ch, String u ) {
            javaFlag = f;
            flagChar = ch;
            unsupported = u;
        }
    }

    private static void _warnUnsupportedRegex( String flag ) {
        LOGGER.info( "flag " + flag + " not supported by db." );
    }

    private static final int GLOBAL_FLAG = 256;

    // --- (en|de)coding hooks -----

    public static boolean hasDecodeHooks() { return _decodeHooks; }

    public static void addEncodingHook( Class c , Transformer t ){
        _encodeHooks = true;
        List<Transformer> l = _encodingHooks.get( c );
        if ( l == null ){
            l = new CopyOnWriteArrayList<Transformer>();
            _encodingHooks.put( c , l );
        }
        l.add( t );
    }

    public static void addDecodingHook( Class c , Transformer t ){
        _decodeHooks = true;
        List<Transformer> l = _decodingHooks.get( c );
        if ( l == null ){
            l = new CopyOnWriteArrayList<Transformer>();
            _decodingHooks.put( c , l );
        }
        l.add( t );
    }

    public static Object applyEncodingHooks( Object o ){
        if ( ! _anyHooks() )
            return o;

        if ( _encodingHooks.size() == 0 || o == null )
            return o;
        List<Transformer> l = _encodingHooks.get( o.getClass() );
        if ( l != null )
            for ( Transformer t : l )
                o = t.transform( o );
        return o;
    }

    public static Object applyDecodingHooks( Object o ){
        if ( ! _anyHooks() || o == null )
            return o;

        List<Transformer> l = _decodingHooks.get( o.getClass() );
        if ( l != null )
            for ( Transformer t : l )
                o = t.transform( o );
        return o;
    }

   /**
     * Returns the encoding hook(s) associated with the specified class
     *
     */
    public static List<Transformer> getEncodingHooks( Class c ){
        return _encodingHooks.get( c );
    }

    /**
     * Clears *all* encoding hooks.
     */
    public static void clearEncodingHooks(){
        _encodeHooks = false;
        _encodingHooks.clear();
    }

    /**
     * Remove all encoding hooks for a specific class.
     */
    public static void removeEncodingHooks( Class c ){
        _encodingHooks.remove( c );
    }

    /**
     * Remove a specific encoding hook for a specific class.
     */
    public static void removeEncodingHook( Class c , Transformer t ){
        getEncodingHooks( c ).remove( t );
    }

   /**
     * Returns the decoding hook(s) associated with the specific class
     */
    public static List<Transformer> getDecodingHooks( Class c ){
        return _decodingHooks.get( c );
    }

    /**
     * Clears *all* decoding hooks.
     */
    public static void clearDecodingHooks(){
        _decodeHooks = false;
        _decodingHooks.clear();
    }

    /**
     * Remove all decoding hooks for a specific class.
     */
    public static void removeDecodingHooks( Class c ){
        _decodingHooks.remove( c );
    }

    /**
     * Remove a specific encoding hook for a specific class.
     */
    public static void removeDecodingHook( Class c , Transformer t ){
        getDecodingHooks( c ).remove( t );
    }


    public static void clearAllHooks(){
        clearEncodingHooks();
        clearDecodingHooks();
    }

    /**
     * Returns true if any encoding or decoding hooks are loaded.
     */
    private static boolean _anyHooks(){
        return _encodeHooks || _decodeHooks;
    }

    private static boolean _encodeHooks = false;
    private static boolean _decodeHooks = false;
    static ClassMap<List<Transformer>> _encodingHooks =
	new ClassMap<List<Transformer>>();

    static ClassMap<List<Transformer>> _decodingHooks =
        new ClassMap<List<Transformer>>();

    /**
     * @deprecated Use {@link Charset#forName(String)} to create UTF-8 charset.
     */
    @Deprecated
    static protected Charset _utf8 = Charset.forName( "UTF-8" );

    // ----- static encode/decode -----

    public static byte[] encode( BSONObject o ){
        BSONEncoder e = _staticEncoder.get();
        try {
            return e.encode( o );
        }
        finally {
            e.done();
        }
    }

    public static BSONObject decode( byte[] b ){
        BSONDecoder d = _staticDecoder.get();
        return d.readObject( b );
    }

    static ThreadLocal<BSONEncoder> _staticEncoder = new ThreadLocal<BSONEncoder>(){
        protected BSONEncoder initialValue(){
            return new BasicBSONEncoder();
        }
    };

    static ThreadLocal<BSONDecoder> _staticDecoder = new ThreadLocal<BSONDecoder>(){
        protected BSONDecoder initialValue(){
            return new BasicBSONDecoder();
        }
    };

    // --- coercing ---

    public static int toInt( Object o ){
        if ( o == null )
            throw new NullPointerException( "can't be null" );

                if ( o instanceof Number )
            return ((Number)o).intValue();

        if ( o instanceof Boolean )
            return ((Boolean)o) ? 1 : 0;

        throw new IllegalArgumentException( "can't convert: " + o.getClass().getName() + " to int" );
    }
}
