/*
 * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.test.store;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.UUID;
import org.h2.dev.store.btree.DataType;
import org.h2.dev.store.btree.DataUtils;
import org.h2.dev.store.btree.MVStore;
import org.h2.util.Utils;

/**
 * A data type implementation for the most common data types, including
 * serializable objects.
 */
public class ObjectType implements DataType {

    // TODO maybe support InputStream, Reader
    // TODO maybe support ResultSet, Date, Time, Timestamp
    // TODO maybe support boolean[], short[],...

    static final int TYPE_BOOLEAN = 1;
    static final int TYPE_BYTE = 2;
    static final int TYPE_SHORT = 3;
    static final int TYPE_INTEGER = 4;
    static final int TYPE_LONG = 5;
    static final int TYPE_BIG_INTEGER = 6;
    static final int TYPE_FLOAT = 7;
    static final int TYPE_DOUBLE = 8;
    static final int TYPE_BIG_DECIMAL = 9;
    static final int TYPE_CHARACTER = 10;
    static final int TYPE_STRING = 11;
    static final int TYPE_UUID = 12;
    static final int TYPE_BYTE_ARRAY = 13;
    static final int TYPE_INT_ARRAY = 14;
    static final int TYPE_LONG_ARRAY = 15;
    static final int TYPE_CHAR_ARRAY = 16;
    static final int TYPE_SERIALIZED_OBJECT = 17;

    static final int TAG_BOOLEAN_TRUE = 32;
    static final int TAG_INTEGER_NEGATIVE = 33;
    static final int TAG_INTEGER_FIXED = 34;
    static final int TAG_LONG_NEGATIVE = 35;
    static final int TAG_LONG_FIXED = 36;
    static final int TAG_BIG_INTEGER_0 = 37;
    static final int TAG_BIG_INTEGER_1 = 38;
    static final int TAG_BIG_INTEGER_SMALL = 39;
    static final int TAG_FLOAT_0 = 40;
    static final int TAG_FLOAT_1 = 41;
    static final int TAG_FLOAT_FIXED = 42;
    static final int TAG_DOUBLE_0 = 43;
    static final int TAG_DOUBLE_1 = 44;
    static final int TAG_DOUBLE_FIXED = 45;
    static final int TAG_BIG_DECIMAL_0 = 46;
    static final int TAG_BIG_DECIMAL_1 = 47;
    static final int TAG_BIG_DECIMAL_SMALL = 48;
    static final int TAG_BIG_DECIMAL_SMALL_SCALED = 49;

    static final int TAG_INTEGER_0_15 = 64;
    static final int TAG_LONG_0_7 = 80;
    static final int TAG_STRING_0_15 = 88;
    static final int TAG_BYTE_ARRAY_0_15 = 104;

    static final int FLOAT_ZERO_BITS = Float.floatToIntBits(0.0f);
    static final int FLOAT_ONE_BITS = Float.floatToIntBits(1.0f);
    static final long DOUBLE_ZERO_BITS = Double.doubleToLongBits(0.0d);
    static final long DOUBLE_ONE_BITS = Double.doubleToLongBits(1.0d);

    private AutoDetectDataType last = new StringType(this);

    @Override
    public int compare(Object a, Object b) {
        return last.compare(a, b);
    }

    @Override
    public int getMaxLength(Object obj) {
        return last.getMaxLength(obj);
    }

    @Override
    public int getMemory(Object obj) {
        return last.getMemory(obj);
    }

    @Override
    public void write(ByteBuffer buff, Object obj) {
        last.write(buff, obj);
    }

    private AutoDetectDataType newType(int typeId) {
        switch (typeId) {
        case TYPE_BOOLEAN:
            return new BooleanType(this);
        case TYPE_BYTE:
            return new ByteType(this);
        case TYPE_SHORT:
            return new ShortType(this);
        case TYPE_CHARACTER:
            return new CharacterType(this);
        case TYPE_INTEGER:
            return new IntegerType(this);
        case TYPE_LONG:
            return new LongType(this);
        case TYPE_FLOAT:
            return new FloatType(this);
        case TYPE_DOUBLE:
            return new DoubleType(this);
        case TYPE_BIG_INTEGER:
            return new BigIntegerType(this);
        case TYPE_BIG_DECIMAL:
            return new BigDecimalType(this);
        case TYPE_BYTE_ARRAY:
            return new ByteArrayType(this);
        case TYPE_CHAR_ARRAY:
            return new CharArrayType(this);
        case TYPE_INT_ARRAY:
            return new IntArrayType(this);
        case TYPE_LONG_ARRAY:
            return new LongArrayType(this);
        case TYPE_STRING:
            return new StringType(this);
        case TYPE_UUID:
            return new UUIDType(this);
        case TYPE_SERIALIZED_OBJECT:
            return new SerializedObjectType(this);
        }
        throw new RuntimeException("Unsupported type: " + typeId);
    }

    @Override
    public Object read(ByteBuffer buff) {
        int tag = buff.get();
        int typeId;
        if (tag <= TYPE_SERIALIZED_OBJECT) {
            typeId = tag;
        } else {
            switch(tag) {
            case TAG_BOOLEAN_TRUE:
                typeId = TYPE_BOOLEAN;
                break;
            case TAG_INTEGER_NEGATIVE:
            case TAG_INTEGER_FIXED:
                typeId = TYPE_INTEGER;
                break;
            case TAG_LONG_NEGATIVE:
            case TAG_LONG_FIXED:
                typeId = TYPE_LONG;
                break;
            case TAG_BIG_INTEGER_0:
            case TAG_BIG_INTEGER_1:
            case TAG_BIG_INTEGER_SMALL:
                typeId = TYPE_BIG_INTEGER;
                break;
            case TAG_FLOAT_0:
            case TAG_FLOAT_1:
            case TAG_FLOAT_FIXED:
                typeId = TYPE_FLOAT;
                break;
            case TAG_DOUBLE_0:
            case TAG_DOUBLE_1:
            case TAG_DOUBLE_FIXED:
                typeId = TYPE_DOUBLE;
                break;
            case TAG_BIG_DECIMAL_0:
            case TAG_BIG_DECIMAL_1:
            case TAG_BIG_DECIMAL_SMALL:
            case TAG_BIG_DECIMAL_SMALL_SCALED:
                typeId = TYPE_BIG_DECIMAL;
                break;
            default:
                if (tag >= TAG_INTEGER_0_15 && tag <= TAG_INTEGER_0_15 + 15) {
                    typeId = TYPE_INTEGER;
                } else if (tag >= TAG_STRING_0_15 && tag <= TAG_STRING_0_15 + 15) {
                    typeId = TYPE_STRING;
                } else if (tag >= TAG_LONG_0_7 && tag <= TAG_LONG_0_7 + 7) {
                    typeId = TYPE_LONG;
                } else if (tag >= TAG_BYTE_ARRAY_0_15 && tag <= TAG_BYTE_ARRAY_0_15 + 15) {
                    typeId = TYPE_BYTE_ARRAY;
                } else {
                    throw new RuntimeException("Unknown tag: " + tag);
                }
            }
        }
        if (typeId != last.typeId) {
            last = newType(typeId);
        }
        return last.read(buff, tag);
    }

    @Override
    public String asString() {
        return "o";
    }

    private static int getTypeId(Object obj) {
        if (obj instanceof Integer) {
            return TYPE_INTEGER;
        } else if (obj instanceof String) {
            return TYPE_STRING;
        } else if (obj instanceof Long) {
            return TYPE_LONG;
        } else if (obj instanceof BigDecimal) {
            if (obj.getClass() == BigDecimal.class) {
                return TYPE_BIG_DECIMAL;
            }
        } else if (obj instanceof byte[]) {
            return TYPE_BYTE_ARRAY;
        } else if (obj instanceof Double) {
            return TYPE_DOUBLE;
        } else if (obj instanceof Float) {
            return TYPE_FLOAT;
        } else if (obj instanceof Boolean) {
            return TYPE_BOOLEAN;
        } else if (obj instanceof UUID) {
            return TYPE_UUID;
        } else if (obj instanceof Byte) {
            return TYPE_BYTE;
        } else if (obj instanceof int[]) {
            return TYPE_INT_ARRAY;
        } else if (obj instanceof long[]) {
            return TYPE_LONG_ARRAY;
        } else if (obj instanceof char[]) {
            return TYPE_CHAR_ARRAY;
        } else if (obj instanceof Short) {
            return TYPE_SHORT;
        } else if (obj instanceof BigInteger) {
            if (obj.getClass() == BigInteger.class) {
                return TYPE_BIG_INTEGER;
            }
        } else if (obj instanceof Character) {
            return TYPE_CHARACTER;
        }
        if (obj == null) {
            throw new NullPointerException();
        }
        return TYPE_SERIALIZED_OBJECT;
    }

    /**
     * Switch the last remembered type to match the type of the given object.
     *
     * @param obj the object
     * @return the auto-detected type used
     */
    AutoDetectDataType switchType(Object obj) {
        int typeId = getTypeId(obj);
        AutoDetectDataType l = last;
        if (typeId != l.typeId) {
            l = last = newType(typeId);
        }
        return l;
    }

    /**
     * Compare the contents of two arrays.
     *
     * @param data1 the first array (must not be null)
     * @param data2 the second array (must not be null)
     * @return the result of the comparison (-1, 1 or 0)
     */
    public static int compareNotNull(char[] data1, char[] data2) {
        if (data1 == data2) {
            return 0;
        }
        int len = Math.min(data1.length, data2.length);
        for (int i = 0; i < len; i++) {
            char x = data1[i];
            char x2 = data2[i];
            if (x != x2) {
                return x > x2 ? 1 : -1;
            }
        }
        return Integer.signum(data1.length - data2.length);
    }

    /**
     * Compare the contents of two arrays.
     *
     * @param data1 the first array (must not be null)
     * @param data2 the second array (must not be null)
     * @return the result of the comparison (-1, 1 or 0)
     */
    public static int compareNotNull(int[] data1, int[] data2) {
        if (data1 == data2) {
            return 0;
        }
        int len = Math.min(data1.length, data2.length);
        for (int i = 0; i < len; i++) {
            int x = data1[i];
            int x2 = data2[i];
            if (x != x2) {
                return x > x2 ? 1 : -1;
            }
        }
        return Integer.signum(data1.length - data2.length);
    }

    /**
     * Compare the contents of two arrays.
     *
     * @param data1 the first array (must not be null)
     * @param data2 the second array (must not be null)
     * @return the result of the comparison (-1, 1 or 0)
     */
    public static int compareNotNull(long[] data1, long[] data2) {
        if (data1 == data2) {
            return 0;
        }
        int len = Math.min(data1.length, data2.length);
        for (int i = 0; i < len; i++) {
            long x = data1[i];
            long x2 = data2[i];
            if (x != x2) {
                return x > x2 ? 1 : -1;
            }
        }
        return Integer.signum(data1.length - data2.length);
    }

    /**
     * The base class for auto-detect data types.
     */
    abstract class AutoDetectDataType implements DataType {

        protected final ObjectType base;
        protected final int typeId;

        AutoDetectDataType(ObjectType base, int typeId) {
            this.base = base;
            this.typeId = typeId;
        }

        @Override
        public int getMemory(Object o) {
            return getType(o).getMemory(o);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            AutoDetectDataType aType = getType(aObj);
            AutoDetectDataType bType = getType(bObj);
            if (aType == bType) {
                return aType.compare(aObj, bObj);
            }
            int typeDiff = aType.typeId - bType.typeId;
            return Integer.signum(typeDiff);
        }

        @Override
        public int getMaxLength(Object o) {
            return getType(o).getMaxLength(o);
        }

        @Override
        public void write(ByteBuffer buff, Object o) {
            getType(o).write(buff, o);
        }

        @Override
        public final Object read(ByteBuffer buff) {
            throw new RuntimeException();
        }

        /**
         * Get the type for the given object.
         *
         * @param o the object
         * @return the type
         */
        AutoDetectDataType getType(Object o) {
            return base.switchType(o);
        }

        /**
         * Read an object from the buffer.
         *
         * @param buff the buffer
         * @param tag the first byte of the object (usually the type)
         * @return the read object
         */
        abstract Object read(ByteBuffer buff, int tag);

        @Override
        public String asString() {
            return "o" + typeId;
        }

    }

    /**
     * The type for boolean true and false.
     */
    class BooleanType extends AutoDetectDataType {

        BooleanType(ObjectType base) {
            super(base, TYPE_BOOLEAN);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Boolean && bObj instanceof Boolean) {
                Boolean a = (Boolean) aObj;
                Boolean b = (Boolean) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Boolean ? 0 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Boolean ? 1 : super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Boolean) {
                int tag = ((Boolean) obj) ? TAG_BOOLEAN_TRUE : TYPE_BOOLEAN;
                buff.put((byte) tag);
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            return tag == TYPE_BOOLEAN ? Boolean.FALSE : Boolean.TRUE;
        }

    }

    /**
     * The type for byte objects.
     */
    class ByteType extends AutoDetectDataType {

        ByteType(ObjectType base) {
            super(base, TYPE_BYTE);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Byte && bObj instanceof Byte) {
                Byte a = (Byte) aObj;
                Byte b = (Byte) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Byte ? 0 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Byte ? 2 : super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Byte) {
                buff.put((byte) TYPE_BYTE);
                buff.put(((Byte) obj).byteValue());
            } else {
                super.write(buff, obj);
            }
        }

        public Object read(ByteBuffer buff, int tag) {
            return Byte.valueOf(buff.get());
        }

    }

    /**
     * The type for character objects.
     */
    class CharacterType extends AutoDetectDataType {

        CharacterType(ObjectType base) {
            super(base, TYPE_CHARACTER);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Character && bObj instanceof Character) {
                Character a = (Character) aObj;
                Character b = (Character) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Character ? 24 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Character ? 3 : super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Character) {
                buff.put((byte) TYPE_CHARACTER);
                buff.putChar(((Character) obj).charValue());
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            return Character.valueOf(buff.getChar());
        }

    }

    /**
     * The type for short objects.
     */
    class ShortType extends AutoDetectDataType {

        ShortType(ObjectType base) {
            super(base, TYPE_SHORT);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Short && bObj instanceof Short) {
                Short a = (Short) aObj;
                Short b = (Short) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Short ? 24 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Short ? 3 : super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Short) {
                buff.put((byte) TYPE_SHORT);
                buff.putShort(((Short) obj).shortValue());
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            return Short.valueOf(buff.getShort());
        }

    }

    /**
     * The type for integer objects.
     */
    class IntegerType extends AutoDetectDataType {

        IntegerType(ObjectType base) {
            super(base, TYPE_INTEGER);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Integer && bObj instanceof Integer) {
                Integer a = (Integer) aObj;
                Integer b = (Integer) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Integer ? 24 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Integer ?
                    1 + DataUtils.MAX_VAR_INT_LEN :
                    super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Integer) {
                int x = (Integer) obj;
                if (x < 0) {
                    // -Integer.MIN_VALUE is smaller than 0
                    if (-x < 0 || -x > DataUtils.COMPRESSED_VAR_INT_MAX) {
                        buff.put((byte) TAG_INTEGER_FIXED);
                        buff.putInt(x);
                    } else {
                        buff.put((byte) TAG_INTEGER_NEGATIVE);
                        DataUtils.writeVarInt(buff, -x);
                    }
                } else if (x <= 15) {
                    buff.put((byte) (TAG_INTEGER_0_15 + x));
                } else if (x <= DataUtils.COMPRESSED_VAR_INT_MAX) {
                    buff.put((byte) TYPE_INTEGER);
                    DataUtils.writeVarInt(buff, x);
                } else {
                    buff.put((byte) TAG_INTEGER_FIXED);
                    buff.putInt(x);
                }
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            switch (tag) {
            case TYPE_INTEGER:
                return DataUtils.readVarInt(buff);
            case TAG_INTEGER_NEGATIVE:
                return -DataUtils.readVarInt(buff);
            case TAG_INTEGER_FIXED:
                return buff.getInt();
            }
            return tag - TAG_INTEGER_0_15;
        }

    }

    /**
     * The type for long objects.
     */
    class LongType extends AutoDetectDataType {

        LongType(ObjectType base) {
            super(base, TYPE_LONG);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Long && bObj instanceof Long) {
                Long a = (Long) aObj;
                Long b = (Long) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Long ? 30 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Long ?
                    1 + DataUtils.MAX_VAR_LONG_LEN :
                    super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Long) {
                long x = (Long) obj;
                if (x < 0) {
                    // -Long.MIN_VALUE is smaller than 0
                    if (-x < 0 || -x > DataUtils.COMPRESSED_VAR_LONG_MAX) {
                        buff.put((byte) TAG_LONG_FIXED);
                        buff.putLong(x);
                    } else {
                        buff.put((byte) TAG_LONG_NEGATIVE);
                        DataUtils.writeVarLong(buff, -x);
                    }
                } else if (x <= 7) {
                    buff.put((byte) (TAG_LONG_0_7 + x));
                } else if (x <= DataUtils.COMPRESSED_VAR_LONG_MAX) {
                    buff.put((byte) TYPE_LONG);
                    DataUtils.writeVarLong(buff, x);
                } else {
                    buff.put((byte) TAG_LONG_FIXED);
                    buff.putLong(x);
                }
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            switch (tag) {
            case TYPE_LONG:
                return DataUtils.readVarLong(buff);
            case TAG_LONG_NEGATIVE:
                return -DataUtils.readVarLong(buff);
            case TAG_LONG_FIXED:
                return buff.getLong();
            }
            return Long.valueOf(tag - TAG_LONG_0_7);
        }

    }

    /**
     * The type for float objects.
     */
    class FloatType extends AutoDetectDataType {

        FloatType(ObjectType base) {
            super(base, TYPE_FLOAT);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Float && bObj instanceof Float) {
                Float a = (Float) aObj;
                Float b = (Float) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Float ? 24 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Float ?
                1 + DataUtils.MAX_VAR_INT_LEN :
                super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Float) {
                float x = (Float) obj;
                int f = Float.floatToIntBits(x);
                if (f == ObjectType.FLOAT_ZERO_BITS) {
                    buff.put((byte) TAG_FLOAT_0);
                } else if (f == ObjectType.FLOAT_ONE_BITS) {
                        buff.put((byte) TAG_FLOAT_1);
                } else {
                    int value = Integer.reverse(f);
                    if (value >= 0 && value <= DataUtils.COMPRESSED_VAR_INT_MAX) {
                        buff.put((byte) TYPE_FLOAT);
                        DataUtils.writeVarInt(buff, value);
                    } else {
                        buff.put((byte) TAG_FLOAT_FIXED);
                        buff.putFloat(x);
                    }
                }
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            switch (tag) {
            case TAG_FLOAT_0:
                return 0f;
            case TAG_FLOAT_1:
                return 1f;
            case TAG_FLOAT_FIXED:
                return buff.getFloat();
            }
            return Float.intBitsToFloat(Integer.reverse(DataUtils.readVarInt(buff)));
        }

    }

    /**
     * The type for double objects.
     */
    class DoubleType extends AutoDetectDataType {

        DoubleType(ObjectType base) {
            super(base, TYPE_DOUBLE);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof Double && bObj instanceof Double) {
                Double a = (Double) aObj;
                Double b = (Double) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof Double ? 30 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            return obj instanceof Double ?
                1 + DataUtils.MAX_VAR_LONG_LEN :
                super.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof Double) {
                double x = (Double) obj;
                long d = Double.doubleToLongBits(x);
                if (d == ObjectType.DOUBLE_ZERO_BITS) {
                    buff.put((byte) TAG_DOUBLE_0);
                } else if (d == ObjectType.DOUBLE_ONE_BITS) {
                        buff.put((byte) TAG_DOUBLE_1);
                } else {
                    long value = Long.reverse(d);
                    if (value >= 0 && value <= DataUtils.COMPRESSED_VAR_LONG_MAX) {
                        buff.put((byte) TYPE_DOUBLE);
                        DataUtils.writeVarLong(buff, value);
                    } else {
                        buff.put((byte) TAG_DOUBLE_FIXED);
                        buff.putDouble(x);
                    }
                }
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            switch (tag) {
            case TAG_DOUBLE_0:
                return 0d;
            case TAG_DOUBLE_1:
                return 1d;
            case TAG_DOUBLE_FIXED:
                return buff.getDouble();
            }
            return Double.longBitsToDouble(Long.reverse(DataUtils.readVarLong(buff)));
        }

    }

    /**
     * The type for BigInteger objects.
     */
    class BigIntegerType extends AutoDetectDataType {

        BigIntegerType(ObjectType base) {
            super(base, TYPE_BIG_INTEGER);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof BigInteger && bObj instanceof BigInteger) {
                BigInteger a = (BigInteger) aObj;
                BigInteger b = (BigInteger) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof BigInteger ? 100 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof BigInteger)) {
                return super.getMaxLength(obj);
            }
            BigInteger x = (BigInteger) obj;
            if (BigInteger.ZERO.equals(x) || BigInteger.ONE.equals(x)) {
                return 1;
            }
            int bits = x.bitLength();
            if (bits <= 63) {
                return 1 + DataUtils.MAX_VAR_LONG_LEN;
            }
            byte[] bytes = x.toByteArray();
            return 1 + DataUtils.MAX_VAR_INT_LEN + bytes.length;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof BigInteger) {
                BigInteger x = (BigInteger) obj;
                if (BigInteger.ZERO.equals(x)) {
                    buff.put((byte) TAG_BIG_INTEGER_0);
                } else if (BigInteger.ONE.equals(x)) {
                    buff.put((byte) TAG_BIG_INTEGER_1);
                } else {
                    int bits = x.bitLength();
                    if (bits <= 63) {
                        buff.put((byte) TAG_BIG_INTEGER_SMALL);
                        DataUtils.writeVarLong(buff, x.longValue());
                    } else {
                        buff.put((byte) TYPE_BIG_INTEGER);
                        byte[] bytes = x.toByteArray();
                        DataUtils.writeVarInt(buff, bytes.length);
                        buff.put(bytes);
                    }
                }
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            switch (tag) {
            case TAG_BIG_INTEGER_0:
                return BigInteger.ZERO;
            case TAG_BIG_INTEGER_1:
                return BigInteger.ONE;
            case TAG_BIG_INTEGER_SMALL:
                return BigInteger.valueOf(DataUtils.readVarLong(buff));
            }
            int len = DataUtils.readVarInt(buff);
            byte[] bytes = Utils.newBytes(len);
            buff.get(bytes);
            return new BigInteger(bytes);
        }

    }

    /**
     * The type for BigDecimal objects.
     */
    class BigDecimalType extends AutoDetectDataType {

        BigDecimalType(ObjectType base) {
            super(base, TYPE_BIG_DECIMAL);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof BigDecimal && bObj instanceof BigDecimal) {
                BigDecimal a = (BigDecimal) aObj;
                BigDecimal b = (BigDecimal) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof BigDecimal ? 150 : super.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof BigDecimal)) {
                return super.getMaxLength(obj);
            }
            BigDecimal x = (BigDecimal) obj;
            if (BigDecimal.ZERO.equals(x) || BigDecimal.ONE.equals(x)) {
                return 1;
            }
            int scale = x.scale();
            BigInteger b = x.unscaledValue();
            int bits = b.bitLength();
            if (bits <= 63) {
                if (scale == 0) {
                    return 1 + DataUtils.MAX_VAR_LONG_LEN;
                }
                return 1 + DataUtils.MAX_VAR_INT_LEN +
                        DataUtils.MAX_VAR_LONG_LEN;
            }
            byte[] bytes = b.toByteArray();
            return 1 + DataUtils.MAX_VAR_INT_LEN +
                    DataUtils.MAX_VAR_INT_LEN + bytes.length;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof BigDecimal) {
                BigDecimal x = (BigDecimal) obj;
                if (BigDecimal.ZERO.equals(x)) {
                    buff.put((byte) TAG_BIG_DECIMAL_0);
                } else if (BigDecimal.ONE.equals(x)) {
                    buff.put((byte) TAG_BIG_DECIMAL_1);
                } else {
                    int scale = x.scale();
                    BigInteger b = x.unscaledValue();
                    int bits = b.bitLength();
                    if (bits < 64) {
                        if (scale == 0) {
                            buff.put((byte) TAG_BIG_DECIMAL_SMALL);
                        } else {
                            buff.put((byte) TAG_BIG_DECIMAL_SMALL_SCALED);
                            DataUtils.writeVarInt(buff, scale);
                        }
                        DataUtils.writeVarLong(buff, b.longValue());
                    } else {
                        buff.put((byte) TYPE_BIG_DECIMAL);
                        DataUtils.writeVarInt(buff, scale);
                        byte[] bytes = b.toByteArray();
                        DataUtils.writeVarInt(buff, bytes.length);
                        buff.put(bytes);
                    }
                }
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            switch (tag) {
            case TAG_BIG_DECIMAL_0:
                return BigDecimal.ZERO;
            case TAG_BIG_DECIMAL_1:
                return BigDecimal.ONE;
            case TAG_BIG_DECIMAL_SMALL:
                return BigDecimal.valueOf(DataUtils.readVarLong(buff));
            case TAG_BIG_DECIMAL_SMALL_SCALED:
                int scale = DataUtils.readVarInt(buff);
                return BigDecimal.valueOf(DataUtils.readVarLong(buff), scale);
            }
            int scale = DataUtils.readVarInt(buff);
            int len = DataUtils.readVarInt(buff);
            byte[] bytes = Utils.newBytes(len);
            buff.get(bytes);
            BigInteger b = new BigInteger(bytes);
            return new BigDecimal(b, scale);
        }

    }

    /**
     * The type for string objects.
     */
    class StringType extends AutoDetectDataType {

        StringType(ObjectType base) {
            super(base, TYPE_STRING);
        }

        @Override
        public int getMemory(Object obj) {
            if (!(obj instanceof String)) {
                return super.getMemory(obj);
            }
            return MVStore.STRING_TYPE.getMemory(obj);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof String && bObj instanceof String) {
                return MVStore.STRING_TYPE.compare(aObj,  bObj);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof String)) {
                return super.getMaxLength(obj);
            }
            return 1 + MVStore.STRING_TYPE.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (!(obj instanceof String)) {
                super.write(buff, obj);
                return;
            }
            String s = (String) obj;
            int len = s.length();
            if (len <= 15) {
                buff.put((byte) (TAG_STRING_0_15 + len));
            } else {
                buff.put((byte) TYPE_STRING);
                DataUtils.writeVarInt(buff, len);
            }
            DataUtils.writeStringData(buff, s, len);
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            int len;
            if (tag == TYPE_STRING) {
                len = DataUtils.readVarInt(buff);
            } else {
                len = tag - TAG_STRING_0_15;
            }
            return DataUtils.readString(buff, len);
        }

    }

    /**
     * The type for UUID objects.
     */
    class UUIDType extends AutoDetectDataType {

        UUIDType(ObjectType base) {
            super(base, TYPE_UUID);
        }

        @Override
        public int getMemory(Object obj) {
            return obj instanceof UUID ? 40 : super.getMemory(obj);
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof UUID && bObj instanceof UUID) {
                UUID a = (UUID) aObj;
                UUID b = (UUID) bObj;
                return a.compareTo(b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof UUID)) {
                return super.getMaxLength(obj);
            }
            return 17;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (!(obj instanceof UUID)) {
                super.write(buff, obj);
                return;
            }
            buff.put((byte) TYPE_UUID);
            UUID a = (UUID) obj;
            buff.putLong(a.getMostSignificantBits());
            buff.putLong(a.getLeastSignificantBits());
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            long a = buff.getLong(), b = buff.getLong();
            return new UUID(a, b);
        }

    }

    /**
     * The type for byte arrays.
     */
    class ByteArrayType extends AutoDetectDataType {

        ByteArrayType(ObjectType base) {
            super(base, TYPE_BYTE_ARRAY);
        }

        @Override
        public int getMemory(Object obj) {
            if (!(obj instanceof byte[])) {
                return super.getMemory(obj);
            }
            return 24 + ((byte[]) obj).length;
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof byte[] && bObj instanceof byte[]) {
                byte[] a = (byte[]) aObj;
                byte[] b = (byte[]) bObj;
                return Utils.compareNotNull(a, b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof byte[])) {
                return super.getMaxLength(obj);
            }
            return 1 + DataUtils.MAX_VAR_INT_LEN + ((byte[]) obj).length;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof byte[]) {
                byte[] data = (byte[]) obj;
                int len = data.length;
                if (len <= 15) {
                    buff.put((byte) (TAG_BYTE_ARRAY_0_15 + len));
                } else {
                    buff.put((byte) TYPE_BYTE_ARRAY);
                    DataUtils.writeVarInt(buff,  data.length);
                }
                buff.put(data);
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            int len;
            if (tag == TYPE_BYTE_ARRAY) {
                len = DataUtils.readVarInt(buff);
            } else {
                len = tag - TAG_BYTE_ARRAY_0_15;
            }
            byte[] data = new byte[len];
            buff.get(data);
            return data;
        }

    }

    /**
     * The type for char arrays.
     */
    class CharArrayType extends AutoDetectDataType {

        CharArrayType(ObjectType base) {
            super(base, TYPE_CHAR_ARRAY);
        }

        @Override
        public int getMemory(Object obj) {
            if (!(obj instanceof char[])) {
                return super.getMemory(obj);
            }
            return 24 + 2 * ((char[]) obj).length;
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof char[] && bObj instanceof char[]) {
                char[] a = (char[]) aObj;
                char[] b = (char[]) bObj;
                return compareNotNull(a, b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof char[])) {
                return super.getMaxLength(obj);
            }
            return 1 + DataUtils.MAX_VAR_INT_LEN + 2 * ((char[]) obj).length;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof char[]) {
                buff.put((byte) TYPE_CHAR_ARRAY);
                char[] data = (char[]) obj;
                int len = data.length;
                DataUtils.writeVarInt(buff,  len);
                buff.asCharBuffer().put(data);
                buff.position(buff.position() + len + len);
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            int len = DataUtils.readVarInt(buff);
            char[] data = new char[len];
            buff.asCharBuffer().get(data);
            buff.position(buff.position() + len + len);
            return data;
        }

    }

    /**
     * The type for char arrays.
     */
    class IntArrayType extends AutoDetectDataType {

        IntArrayType(ObjectType base) {
            super(base, TYPE_INT_ARRAY);
        }

        @Override
        public int getMemory(Object obj) {
            if (!(obj instanceof int[])) {
                return super.getMemory(obj);
            }
            return 24 + 4 * ((int[]) obj).length;
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof int[] && bObj instanceof int[]) {
                int[] a = (int[]) aObj;
                int[] b = (int[]) bObj;
                return compareNotNull(a, b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof int[])) {
                return super.getMaxLength(obj);
            }
            return 1 + DataUtils.MAX_VAR_INT_LEN + 4 * ((int[]) obj).length;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof int[]) {
                buff.put((byte) TYPE_INT_ARRAY);
                int[] data = (int[]) obj;
                int len = data.length;
                DataUtils.writeVarInt(buff,  len);
                buff.asIntBuffer().put(data);
                buff.position(buff.position() + 4 * len);
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            int len = DataUtils.readVarInt(buff);
            int[] data = new int[len];
            buff.asIntBuffer().get(data);
            buff.position(buff.position() + 4 * len);
            return data;
        }

    }

    /**
     * The type for char arrays.
     */
    class LongArrayType extends AutoDetectDataType {

        LongArrayType(ObjectType base) {
            super(base, TYPE_LONG_ARRAY);
        }

        @Override
        public int getMemory(Object obj) {
            if (!(obj instanceof long[])) {
                return super.getMemory(obj);
            }
            return 24 + 8 * ((long[]) obj).length;
        }

        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj instanceof long[] && bObj instanceof long[]) {
                long[] a = (long[]) aObj;
                long[] b = (long[]) bObj;
                return compareNotNull(a, b);
            }
            return super.compare(aObj, bObj);
        }

        @Override
        public int getMaxLength(Object obj) {
            if (!(obj instanceof long[])) {
                return super.getMaxLength(obj);
            }
            return 1 + DataUtils.MAX_VAR_INT_LEN + 8 * ((long[]) obj).length;
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            if (obj instanceof long[]) {
                buff.put((byte) TYPE_LONG_ARRAY);
                long[] data = (long[]) obj;
                int len = data.length;
                DataUtils.writeVarInt(buff,  len);
                buff.asLongBuffer().put(data);
                buff.position(buff.position() + 8 * len);
            } else {
                super.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            int len = DataUtils.readVarInt(buff);
            long[] data = new long[len];
            buff.asLongBuffer().get(data);
            buff.position(buff.position() + 8 * len);
            return data;
        }

    }

    /**
     * The type for serialized objects.
     */
    class SerializedObjectType extends AutoDetectDataType {

        SerializedObjectType(ObjectType base) {
            super(base, TYPE_SERIALIZED_OBJECT);
        }

        @SuppressWarnings("unchecked")
        @Override
        public int compare(Object aObj, Object bObj) {
            if (aObj == bObj) {
                return 0;
            }
            DataType ta = getType(aObj);
            DataType tb = getType(bObj);
            if (ta != this && ta == tb) {
                return ta.compare(aObj, bObj);
            }
            // TODO ensure comparable type (both may be comparable but not
            // with each other)
            if (aObj instanceof Comparable) {
                if (aObj.getClass().isAssignableFrom(bObj.getClass())) {
                    return ((Comparable<Object>) aObj).compareTo(bObj);
                }
            }
            if (bObj instanceof Comparable) {
                if (bObj.getClass().isAssignableFrom(aObj.getClass())) {
                    return -((Comparable<Object>) bObj).compareTo(aObj);
                }
            }
            byte[] a = Utils.serialize(aObj);
            byte[] b = Utils.serialize(bObj);
            return Utils.compareNotNull(a, b);
        }

        @Override
        public int getMemory(Object obj) {
            DataType t = getType(obj);
            if (t == this) {
                return 1000;
            }
            return t.getMemory(obj);
        }

        @Override
        public int getMaxLength(Object obj) {
            DataType t = getType(obj);
            if (t == this) {
                byte[] data = Utils.serialize(obj);
                return 1 + DataUtils.MAX_VAR_INT_LEN + data.length;
            }
            return t.getMaxLength(obj);
        }

        @Override
        public void write(ByteBuffer buff, Object obj) {
            DataType t = getType(obj);
            if (t == this) {
                buff.put((byte) TYPE_SERIALIZED_OBJECT);
                byte[] data = Utils.serialize(obj);
                DataUtils.writeVarInt(buff, data.length);
                buff.put(data);
            } else {
                t.write(buff, obj);
            }
        }

        @Override
        public Object read(ByteBuffer buff, int tag) {
            int len = DataUtils.readVarInt(buff);
            byte[] data = new byte[len];
            buff.get(data);
            return Utils.deserialize(data);
        }

    }

}
