/**
 * Copyright (C) 2012 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 org.bson.io.Bits;
import org.bson.types.ObjectId;
import static org.bson.BSON.*;

// Java
import java.io.IOException;
import java.io.InputStream;
import java.io.DataInputStream;
import java.io.UnsupportedEncodingException;

/**
 * A new implementation of the bson decoder.
 */
public class NewBSONDecoder implements BSONDecoder {

    @Override
    public BSONObject readObject(final byte [] pData) {
        _length = pData.length;
        final BasicBSONCallback c = new BasicBSONCallback();
        decode(pData, c);
        return (BSONObject)c.get();
   }

    @Override
    public BSONObject readObject(final InputStream pIn) throws IOException {
        // Slurp in the data and convert to a byte array.
        _length = Bits.readInt(pIn);

        if (_data == null || _data.length < _length) {
            _data = new byte[_length];
        }

        (new DataInputStream(pIn)).readFully(_data, 4, (_length - 4));

        return readObject(_data);
    }

    @Override
    public int decode(final byte [] pData, final BSONCallback pCallback) {
        _data = pData;
        _pos = 4;
        _callback = pCallback;
        _decode();
        return _length;
    }

    @Override
    public int decode(final InputStream pIn, final BSONCallback pCallback) throws IOException {
        _length = Bits.readInt(pIn);

        if (_data == null || _data.length < _length) {
            _data = new byte[_length];
        }

        (new DataInputStream(pIn)).readFully(_data, 4, (_length - 4));

        return decode(_data, pCallback);
    }

    private final void _decode() {
        _callback.objectStart();
        while (decodeElement());
        _callback.objectDone();
    }

    private final String readCstr() {
        int length = 0;
        final int offset = _pos;

        while (_data[_pos++] != 0) length++;

        try {
            return new String(_data, offset, length, DEFAULT_ENCODING);
        } catch (final UnsupportedEncodingException uee) {
            return new String(_data, offset, length);
        }
    }

    private final String readUtf8Str() {
        final int length = Bits.readInt(_data, _pos);
        _pos += 4;

        if (length <= 0 || length > MAX_STRING) throw new BSONException("String invalid - corruption");

        try {
            final String str = new String(_data, _pos, (length - 1), DEFAULT_ENCODING);
            _pos += length;
            return str;

        } catch (final UnsupportedEncodingException uee) {
            throw new BSONException("What is in the db", uee);
        }
    }

    private final Object _readBasicObject() {
        _pos += 4;

        final BSONCallback save = _callback;
        final BSONCallback _basic = _callback.createBSONCallback();
        _callback = _basic;
        _basic.reset();
        _basic.objectStart(false);

        while( decodeElement() );
        _callback = save;
        return _basic.get();
    }

    private final void _binary(final String pName) {

        final int totalLen = Bits.readInt(_data, _pos);
        _pos += 4;

        final byte bType = _data[_pos];
        _pos += 1;

        switch ( bType ){
            case B_GENERAL: {
                    final byte [] data = new byte[totalLen];

                    System.arraycopy(_data, _pos, data, 0, totalLen);
                    _pos += totalLen;

                    _callback.gotBinary(pName, bType, data);
                    return;
            }

            case B_BINARY: {
                final int len = Bits.readInt(_data, _pos);
                _pos += 4;

                if ( len + 4 != totalLen )
                    throw new IllegalArgumentException( "bad data size subtype 2 len: " + len + " totalLen: " + totalLen );

                final byte [] data = new byte[len];
                System.arraycopy(_data, _pos, data, 0, len);
                _pos += len;
                _callback.gotBinary(pName, bType, data);
                return;
            }

            case B_UUID: {
                if ( totalLen != 16 )
                    throw new IllegalArgumentException( "bad data size subtype 3 len: " + totalLen + " != 16");

                final long part1 = Bits.readLong(_data, _pos);
                _pos += 8;

                final long part2 = Bits.readLong(_data, _pos);
                _pos += 8;

                _callback.gotUUID(pName, part1, part2);
                return;
            }
        }

        final byte [] data = new byte[totalLen];
        System.arraycopy(_data, _pos, data, 0, totalLen);
        _pos += totalLen;

        _callback.gotBinary(pName, bType, data);
    }

    private final boolean decodeElement() {

        final byte type = _data[_pos];
        _pos += 1;

        if (type == EOO) return false;

        final String name = readCstr();

        switch (type) {
            case NULL: { _callback.gotNull(name); return true; }

            case UNDEFINED: { _callback.gotUndefined(name); return true; }

            case BOOLEAN: { _callback.gotBoolean(name, (_data[_pos] > 0)); _pos += 1; return true; }

            case NUMBER: { _callback.gotDouble(name, Double.longBitsToDouble(Bits.readLong(_data, _pos))); _pos += 8; return true; }

            case NUMBER_INT: { _callback.gotInt(name, Bits.readInt(_data, _pos)); _pos += 4; return true; }

            case NUMBER_LONG: {
                _callback.gotLong(name, Bits.readLong(_data, _pos));
                _pos += 8;
                return true;
            }

            case SYMBOL: { _callback.gotSymbol(name, readUtf8Str()); return true; }
            case STRING: {  _callback.gotString(name, readUtf8Str()); return true; }

            case OID: {
                // OID is stored as big endian

                final int p1 = Bits.readIntBE(_data, _pos);
                _pos += 4;

                final int p2 = Bits.readIntBE(_data, _pos);
                _pos += 4;

                final int p3 = Bits.readIntBE(_data, _pos);
                _pos += 4;

                _callback.gotObjectId(name , new ObjectId(p1, p2, p3));
                return true;
            }

            case REF: {
                _pos += 4;

                final String ns = readCstr();

                final int p1 = Bits.readInt(_data, _pos);
                _pos += 4;

                final int p2 = Bits.readInt(_data, _pos);
                _pos += 4;

                final int p3 = Bits.readInt(_data, _pos);
                _pos += 4;

                _callback.gotDBRef(name , ns, new ObjectId(p1, p2, p3));

                return true;
            }

            case DATE: { _callback.gotDate(name , Bits.readLong(_data, _pos)); _pos += 8; return true; }


            case REGEX: {
                _callback.gotRegex(name, readCstr(), readCstr());
                return true;
            }

            case BINARY: { _binary(name); return true; }

            case CODE: { _callback.gotCode(name, readUtf8Str()); return true; }

            case CODE_W_SCOPE: {
                _pos += 4;
                _callback.gotCodeWScope(name, readUtf8Str(), _readBasicObject());
                return true;
            }

            case ARRAY:
               _pos += 4;
                _callback.arrayStart(name);
                while (decodeElement());
                _callback.arrayDone();
                return true;

            case OBJECT:
                _pos += 4;
                _callback.objectStart(name);
                while (decodeElement());
                _callback.objectDone();
                return true;

            case TIMESTAMP:
                int i = Bits.readInt(_data, _pos);
                _pos += 4;

                int time = Bits.readInt(_data, _pos);
                _pos += 4;

                _callback.gotTimestamp(name, time, i);
                return true;

            case MINKEY: _callback.gotMinKey(name); return true;
            case MAXKEY: _callback.gotMaxKey(name); return true;

            default: throw new UnsupportedOperationException( "BSONDecoder doesn't understand type : " + type + " name: " + name  );
        }
    }

    private static final int MAX_STRING = ( 32 * 1024 * 1024 );
    private static final String DEFAULT_ENCODING = "UTF-8";

    private byte [] _data;
    private int _length;
    private int _pos = 0;
    private BSONCallback _callback;
}

