/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* 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.
*/

package com.linkedin.paldb.impl;

import com.linkedin.paldb.api.Configuration;
import com.linkedin.paldb.api.Serializer;
import com.linkedin.paldb.api.UnsupportedTypeException;
import com.linkedin.paldb.utils.DataInputOutput;
import com.linkedin.paldb.utils.LongPacker;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.IOException;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import org.xerial.snappy.Snappy;

/**
 * Internal serialization implementation.
 */
public final class StorageSerialization {

  //Buffer
  private final DataInputOutput dataInputOutput = new DataInputOutput();
  //Compression
  private final boolean compression;
  //Serializers
  private Serializers serializers;

  /**
   * Default constructor with configuration.
   * <p>
   * Uses <code>Configuration.COMPRESSION_ENABLED</code> and <code>Configuration.KEY_COMPARATOR</code> values
   * from the configuration.
   *
   * @param config configuration
   */
  public StorageSerialization(Configuration config) {
    this.compression = config.getBoolean(Configuration.COMPRESSION_ENABLED);
    this.serializers = config.getSerializers();
  }

  /**
   * Serializes the key object and returns it as a byte array.
   *
   * @param key key to serialize
   * @return key as byte array
   * @throws IOException if an io error occurs
   */
  public byte[] serializeKey(Object key)
      throws IOException {
    if (key == null) {
      throw new NullPointerException();
    }
    serializeObject(key, dataInputOutput.reset(), false);
    return dataInputOutput.toByteArray();
  }

  /**
   * Serializes the key and writes it into <code>dataOutput</code>.
   *
   * @param key key to serialize
   * @param dataOutput data output
   * @throws IOException if an io error occurs
   */
  public void serializeKey(Object key, DataOutput dataOutput)
      throws IOException {
    serializeObject(key, dataOutput, false);
  }

  /**
   * Serializes the value object and returns it as a byte array.
   *
   * @param value value to serialize
   * @return value as byte array
   * @throws IOException if an io error occurs
   */
  public byte[] serializeValue(Object value)
      throws IOException {

    serializeObject(value, dataInputOutput.reset(), compression);
    return dataInputOutput.toByteArray();
  }

  /**
   * Serializes the value and writes it into <code>dataOutput</code>.
   *
   * @param value value to serialize
   * @param dataOutput data output
   * @throws IOException if an io error occurs
   */
  public void serializeValue(Object value, DataOutput dataOutput)
      throws IOException {
    serializeObject(value, dataOutput, compression);
  }

  /**
   * Serialization implementation.
   *
   * @param obj object to serialize
   * @param useCompression use compression
   * @return serialized object in bytes
   * @throws IOException if an io error occurs
   */
  private void serializeObject(Object obj, DataOutput dataOutput, boolean useCompression)
      throws IOException {
    //Cast to primitive arrays if necessary
    if (obj != null && obj.getClass().isArray()) {
      if (obj instanceof Integer[]) {
        obj = (int[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Boolean[]) {
        obj = (boolean[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Byte[]) {
        obj = (byte[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Character[]) {
        obj = (char[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Double[]) {
        obj = (double[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Float[]) {
        obj = (float[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Long[]) {
        obj = (long[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Short[]) {
        obj = (short[]) getPrimitiveArray((Object[]) obj);
      } else if (obj instanceof Integer[][]) {
        obj = (int[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Boolean[][]) {
        obj = (boolean[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Byte[][]) {
        obj = (byte[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Character[][]) {
        obj = (char[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Double[][]) {
        obj = (double[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Float[][]) {
        obj = (float[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Long[][]) {
        obj = (long[][]) getPrimitiveArray((Object[][]) obj);
      } else if (obj instanceof Short[][]) {
        obj = (short[][]) getPrimitiveArray((Object[][]) obj);
      }
    }

    serialize(dataOutput, obj, useCompression);
  }

  /**
   * Returns true if compression is enabled.
   *
   * @return true if enabled, false otherwise
   */
  public boolean isCompressionEnabled() {
    return compression;
  }

  // UTILITIES

  private static Object getPrimitiveArray(Object[][] array) {
    Class arrayClass = array.getClass().getComponentType().getComponentType();
    if (!arrayClass.isPrimitive()) {
      Class primitiveClass = getPrimitiveType(arrayClass);

      int arrayLength = array.length;
      Object primitiveArray = Array.newInstance(primitiveClass, arrayLength, 0);

      for (int i = 0; i < arrayLength; i++) {
        Object[] obj = array[i];
        if (obj != null) {
          Object innerArray = Array.newInstance(primitiveClass, obj.length);
          for (int j = 0; j < obj.length; j++) {
            Object iobj = obj[j];
            if (iobj != null) {
              Array.set(innerArray, j, iobj);
            }
          }
          Array.set(primitiveArray, i, innerArray);
        }
      }

      return primitiveArray;
    }

    return null;
  }

  private static Object getPrimitiveArray(Object[] array) {
    Class arrayClass = array.getClass().getComponentType();
    if (!arrayClass.isPrimitive()) {
      Class primitiveClass = getPrimitiveType(arrayClass);

      int arrayLength = array.length;
      Object primitiveArray = Array.newInstance(primitiveClass, arrayLength);

      for (int i = 0; i < arrayLength; i++) {
        Object obj = array[i];
        if (obj != null) {
          Array.set(primitiveArray, i, obj);
        }
      }
      return primitiveArray;
    }
    return array;
  }

  private static Class getPrimitiveType(Class type) {
    if (!type.isPrimitive()) {
      if (type.equals(Boolean.class)) {
        return boolean.class;
      } else if (type.equals(Integer.class)) {
        return int.class;
      } else if (type.equals(Short.class)) {
        return short.class;
      } else if (type.equals(Long.class)) {
        return long.class;
      } else if (type.equals(Byte.class)) {
        return byte.class;
      } else if (type.equals(Float.class)) {
        return float.class;
      } else if (type.equals(Double.class)) {
        return double.class;
      } else if (type.equals(Character.class)) {
        return char.class;
      }
    }
    throw new IllegalArgumentException("The type should be a wrapped primitive");
  }

  // SERIALIZATION

  final static int NULL_ID = -1;
  final static int NULL = 0;
  final static int BOOLEAN_TRUE = 2;
  final static int BOOLEAN_FALSE = 3;
  final static int INTEGER_MINUS_1 = 4;
  final static int INTEGER_0 = 5;
  final static int INTEGER_1 = 6;
  final static int INTEGER_2 = 7;
  final static int INTEGER_3 = 8;
  final static int INTEGER_4 = 9;
  final static int INTEGER_5 = 10;
  final static int INTEGER_6 = 11;
  final static int INTEGER_7 = 12;
  final static int INTEGER_8 = 13;
  final static int INTEGER_255 = 14;
  final static int INTEGER_PACK_NEG = 15;
  final static int INTEGER_PACK = 16;
  final static int LONG_MINUS_1 = 17;
  final static int LONG_0 = 18;
  final static int LONG_1 = 19;
  final static int LONG_2 = 20;
  final static int LONG_3 = 21;
  final static int LONG_4 = 22;
  final static int LONG_5 = 23;
  final static int LONG_6 = 24;
  final static int LONG_7 = 25;
  final static int LONG_8 = 26;
  final static int LONG_PACK_NEG = 27;
  final static int LONG_PACK = 28;
  final static int LONG_255 = 29;
  final static int LONG_MINUS_MAX = 30;
  final static int SHORT_MINUS_1 = 31;
  final static int SHORT_0 = 32;
  final static int SHORT_1 = 33;
  final static int SHORT_255 = 34;
  final static int SHORT_FULL = 35;
  final static int BYTE_MINUS_1 = 36;
  final static int BYTE_0 = 37;
  final static int BYTE_1 = 38;
  final static int BYTE_FULL = 39;
  final static int CHAR = 40;
  final static int FLOAT_MINUS_1 = 41;
  final static int FLOAT_0 = 42;
  final static int FLOAT_1 = 43;
  final static int FLOAT_255 = 44;
  final static int FLOAT_SHORT = 45;
  final static int FLOAT_FULL = 46;
  final static int DOUBLE_MINUS_1 = 47;
  final static int DOUBLE_0 = 48;
  final static int DOUBLE_1 = 49;
  final static int DOUBLE_255 = 50;
  final static int DOUBLE_SHORT = 51;
  final static int DOUBLE_FULL = 52;
  final static int DOUBLE_ARRAY = 53;
  final static int BIGDECIMAL = 54;
  final static int BIGINTEGER = 55;
  final static int FLOAT_ARRAY = 56;
  final static int INTEGER_MINUS_MAX = 57;
  final static int SHORT_ARRAY = 58;
  final static int BOOLEAN_ARRAY = 59;
  final static int ARRAY_INT_B = 60;
  final static int ARRAY_INT_S = 61;
  final static int ARRAY_INT_I = 62;
  final static int ARRAY_INT_PACKED = 63;
  final static int ARRAY_LONG_B = 64;
  final static int ARRAY_LONG_S = 65;
  final static int ARRAY_LONG_I = 66;
  final static int ARRAY_LONG_L = 67;
  final static int ARRAY_LONG_PACKED = 68;
  final static int CHAR_ARRAY = 69;
  final static int BYTE_ARRAY = 70;
  final static int STRING_ARRAY = 71;
  final static int ARRAY_OBJECT = 72;
  final static int STRING_EMPTY = 101;
  final static int NOTUSED_STRING_C = 102;
  final static int STRING = 103;
  final static int ARRAY_INT_C = 104;
  final static int ARRAY_LONG_C = 105;
  final static int DOUBLE_ARRAY_C = 106;
  final static int FLOAT_ARRAY_C = 107;
  final static int CHAR_ARRAY_C = 108;
  final static int BYTE_ARRAY_C = 109;
  final static int SHORT_ARRAY_C = 110;
  final static int INT_INT_ARRAY = 111;
  final static int LONG_LONG_ARRAY = 112;
  final static int CLASS = 113;
  final static int CUSTOM = 114;
  final static String EMPTY_STRING = "";

  byte[] serialize(Object obj)
      throws IOException {
    return serialize(obj, false);
  }

  byte[] serialize(Object obj, boolean compress)
      throws IOException {
    DataInputOutput ba = new DataInputOutput();

    serialize(ba, obj, compress);

    return ba.toByteArray();
  }

  private void serialize(final DataOutput out, final Object obj)
      throws IOException {
    serialize(out, obj, false);
  }

  private void serialize(final DataOutput out, final Object obj, boolean compress)
      throws IOException {
    final Class clazz = obj != null ? obj.getClass() : null;

    if (obj == null) {
      out.write(NULL);
    } else if (clazz == Boolean.class) {
      if (((Boolean) obj).booleanValue()) {
        out.write(BOOLEAN_TRUE);
      } else {
        out.write(BOOLEAN_FALSE);
      }
    } else if (clazz == Integer.class) {
      serializeInt(out, (Integer) obj);
    } else if (clazz == Double.class) {
      serializeDouble(out, (Double) obj);
    } else if (clazz == Float.class) {
      serializeFloat(out, (Float) obj);
    } else if (clazz == Long.class) {
      serializeLong(out, (Long) obj);
    } else if (clazz == BigInteger.class) {
      serializeBigInteger(out, (BigInteger) obj);
    } else if (clazz == BigDecimal.class) {
      serializeBigDecimal(out, (BigDecimal) obj);
    } else if (clazz == Short.class) {
      serializeShort(out, (Short) obj);
    } else if (clazz == Byte.class) {
      serializeByte(out, (Byte) obj);
    } else if (clazz == Character.class) {
      serializeChar(out, (Character) obj);
    } else if (clazz == String.class) {
      serializeString(out, (String) obj);
    } else if (obj instanceof Class) {
      serializeClass(out, (Class) obj);
    } else if (obj instanceof int[]) {
      serializeIntArray(out, (int[]) obj, compress);
    } else if (obj instanceof long[]) {
      serializeLongArray(out, (long[]) obj, compress);
    } else if (obj instanceof short[]) {
      serializeShortArray(out, (short[]) obj, compress);
    } else if (obj instanceof boolean[]) {
      serializeBooleanArray(out, (boolean[]) obj);
    } else if (obj instanceof double[]) {
      serializeDoubleArray(out, (double[]) obj, compress);
    } else if (obj instanceof float[]) {
      serializeFloatArray(out, (float[]) obj, compress);
    } else if (obj instanceof char[]) {
      serializeCharArray(out, (char[]) obj, compress);
    } else if (obj instanceof byte[]) {
      serializeByteArray(out, (byte[]) obj, compress);
    } else if (obj instanceof String[]) {
      serializeStringArray(out, (String[]) obj);
    } else if (obj instanceof int[][]) {
      serializeIntIntArray(out, (int[][]) obj, compress);
    } else if (obj instanceof long[][]) {
      serializeLongLongArray(out, (long[][]) obj, compress);
    } else {
      // Custom
      Serializer serializer = serializers.getSerializer(obj.getClass());
      if (serializer != null) {
        int index = serializers.getIndex(obj.getClass());
        out.write(CUSTOM + index);
        serializer.write(out, obj);
      } else if (obj instanceof Object[]) {
        serializeObjectArray(out, (Object[]) obj);
      } else {
        throw new UnsupportedTypeException(obj);
      }
    }
  }

  private static void serializeInt(final DataOutput out, final int val)
      throws IOException {
    if (val == -1) {
      out.write(INTEGER_MINUS_1);
    } else if (val == 0) {
      out.write(INTEGER_0);
    } else if (val == 1) {
      out.write(INTEGER_1);
    } else if (val == 2) {
      out.write(INTEGER_2);
    } else if (val == 3) {
      out.write(INTEGER_3);
    } else if (val == 4) {
      out.write(INTEGER_4);
    } else if (val == 5) {
      out.write(INTEGER_5);
    } else if (val == 6) {
      out.write(INTEGER_6);
    } else if (val == 7) {
      out.write(INTEGER_7);
    } else if (val == 8) {
      out.write(INTEGER_8);
    } else if (val == Integer.MIN_VALUE) {
      out.write(INTEGER_MINUS_MAX);
    } else if (val > 0 && val < 255) {
      out.write(INTEGER_255);
      out.write(val);
    } else if (val < 0) {
      out.write(INTEGER_PACK_NEG);
      LongPacker.packInt(out, -val);
    } else {
      out.write(INTEGER_PACK);
      LongPacker.packInt(out, val);
    }
  }

  private static void serializeDouble(final DataOutput out, final double val)
      throws IOException {
    if (val == -1d) {
      out.write(DOUBLE_MINUS_1);
    } else if (val == 0d) {
      out.write(DOUBLE_0);
    } else if (val == 1d) {
      out.write(DOUBLE_1);
    } else if (val >= 0 && val <= 255 && (int) val == val) {
      out.write(DOUBLE_255);
      out.write((int) val);
    } else if (val >= Short.MIN_VALUE && val <= Short.MAX_VALUE && (short) val == val) {
      out.write(DOUBLE_SHORT);
      out.writeShort((int) val);
    } else {
      out.write(DOUBLE_FULL);
      out.writeDouble(val);
    }
  }

  private static void serializeFloat(final DataOutput out, final float val)
      throws IOException {
    if (val == -1f) {
      out.write(FLOAT_MINUS_1);
    } else if (val == 0f) {
      out.write(FLOAT_0);
    } else if (val == 1f) {
      out.write(FLOAT_1);
    } else if (val >= 0 && val <= 255 && (int) val == val) {
      out.write(FLOAT_255);
      out.write((int) val);
    } else if (val >= Short.MIN_VALUE && val <= Short.MAX_VALUE && (short) val == val) {
      out.write(FLOAT_SHORT);
      out.writeShort((int) val);
    } else {
      out.write(FLOAT_FULL);
      out.writeFloat(val);
    }
  }

  private static void serializeShort(final DataOutput out, final short val)
      throws IOException {
    if (val == -1) {
      out.write(SHORT_MINUS_1);
    } else if (val == 0) {
      out.write(SHORT_0);
    } else if (val == 1) {
      out.write(SHORT_1);
    } else if (val > 0 && val < 255) {
      out.write(SHORT_255);
      out.write(val);
    } else {
      out.write(SHORT_FULL);
      out.writeShort(val);
    }
  }

  private static void serializeByte(final DataOutput out, final byte val)
      throws IOException {
    if (val == -1) {
      out.write(BYTE_MINUS_1);
    } else if (val == 0) {
      out.write(BYTE_0);
    } else if (val == 1) {
      out.write(BYTE_1);
    } else {
      out.write(BYTE_FULL);
      out.writeByte(val);
    }
  }

  private static void serializeLong(final DataOutput out, final long val)
      throws IOException {
    if (val == -1) {
      out.write(LONG_MINUS_1);
    } else if (val == 0) {
      out.write(LONG_0);
    } else if (val == 1) {
      out.write(LONG_1);
    } else if (val == 2) {
      out.write(LONG_2);
    } else if (val == 3) {
      out.write(LONG_3);
    } else if (val == 4) {
      out.write(LONG_4);
    } else if (val == 5) {
      out.write(LONG_5);
    } else if (val == 6) {
      out.write(LONG_6);
    } else if (val == 7) {
      out.write(LONG_7);
    } else if (val == 8) {
      out.write(LONG_8);
    } else if (val == Long.MIN_VALUE) {
      out.write(LONG_MINUS_MAX);
    } else if (val > 0 && val < 255) {
      out.write(LONG_255);
      out.write((int) val);
    } else if (val < 0) {
      out.write(LONG_PACK_NEG);
      LongPacker.packLong(out, -val);
    } else {
      out.write(LONG_PACK);
      LongPacker.packLong(out, val);
    }
  }

  private static void serializeChar(final DataOutput out, final char val)
      throws IOException {
    out.write(CHAR);
    out.writeChar(val);
  }

  private static void serializeString(final DataOutput out, final String val)
      throws IOException {
    if (val.length() == 0) {
      out.write(STRING_EMPTY);
    } else {
      out.write(STRING);
      final int len = val.length();
      LongPacker.packInt(out, len);
      for (int i = 0; i < len; i++) {
        int c = (int) val.charAt(i); //TODO investigate if c could be negative here
        LongPacker.packInt(out, c);
      }
    }
  }

  private static void serializeBigInteger(final DataOutput out, final BigInteger val)
      throws IOException {
    out.write(BIGINTEGER);
    byte[] buf = val.toByteArray();
    serializeByteArray(out, buf, false);
  }

  private static void serializeBigDecimal(final DataOutput out, final BigDecimal val)
      throws IOException {
    out.write(BIGDECIMAL);
    serializeByteArray(out, val.unscaledValue().toByteArray(), false);
    LongPacker.packInt(out, val.scale());
  }

  private static void serializeClass(final DataOutput out, final Class val)
      throws IOException {
    out.write(CLASS);
    serializeString(out, val.getName());
  }

  private static void serializeBooleanArray(final DataOutput out, final boolean[] val)
      throws IOException {
    out.write(BOOLEAN_ARRAY);
    LongPacker.packInt(out, val.length);
    for (boolean s : val) {
      out.writeBoolean(s);
    }
  }

  private static void serializeShortArray(final DataOutput out, final short[] val, boolean compress)
      throws IOException {
    if (compress && val.length > 250) {
      out.write(SHORT_ARRAY_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else {
      out.write(SHORT_ARRAY);
      LongPacker.packInt(out, val.length);
      for (short s : val) {
        out.writeShort(s);
      }
    }
  }

  private static void serializeDoubleArray(final DataOutput out, final double[] val, boolean compress)
      throws IOException {
    if (compress && val.length > 250) {
      out.write(DOUBLE_ARRAY_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else {
      out.write(DOUBLE_ARRAY);
      LongPacker.packInt(out, val.length);
      for (double s : val) {
        out.writeDouble(s);
      }
    }
  }

  private static void serializeFloatArray(final DataOutput out, final float[] val, boolean compress)
      throws IOException {
    if (compress && val.length > 250) {
      out.write(FLOAT_ARRAY_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else {
      out.write(FLOAT_ARRAY);
      LongPacker.packInt(out, val.length);
      for (float s : val) {
        out.writeFloat(s);
      }
    }
  }

  private static void serializeCharArray(final DataOutput out, final char[] val, boolean compress)
      throws IOException {
    if (compress && val.length > 250) {
      out.write(CHAR_ARRAY_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else {
      out.write(CHAR_ARRAY);
      LongPacker.packInt(out, val.length);
      for (char s : val) {
        out.writeChar(s);
      }
    }
  }

  private static void serializeIntArray(final DataOutput out, final int[] val, boolean compress)
      throws IOException {
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int i : val) {
      max = Math.max(max, i);
      min = Math.min(min, i);
    }

    if (0 <= min && max <= 255) {
      out.write(ARRAY_INT_B);
      LongPacker.packInt(out, val.length);
      for (int i : val) {
        out.write(i);
      }
    } else if (min >= Short.MIN_VALUE && max <= Short.MAX_VALUE) {
      out.write(ARRAY_INT_S);
      LongPacker.packInt(out, val.length);
      for (int i : val) {
        out.writeShort(i);
      }
    } else if (compress && val.length > 250) {
      out.write(ARRAY_INT_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else if (min >= 0) {
      out.write(ARRAY_INT_PACKED);
      LongPacker.packInt(out, val.length);
      for (int l : val) {
        LongPacker.packInt(out, l);
      }
    } else {
      out.write(ARRAY_INT_I);
      LongPacker.packInt(out, val.length);
      for (int i : val) {
        out.writeInt(i);
      }
    }
  }

  private static void serializeIntIntArray(final DataOutput out, final int[][] val, boolean compress)
      throws IOException {
    out.write(INT_INT_ARRAY);
    LongPacker.packInt(out, val.length);

    for (int[] v : val) {
      serializeIntArray(out, v, compress);
    }
  }

  private static void serializeLongArray(final DataOutput out, final long[] val, boolean compress)
      throws IOException {
    long max = Long.MIN_VALUE;
    long min = Long.MAX_VALUE;
    for (long i : val) {
      max = Math.max(max, i);
      min = Math.min(min, i);
    }

    if (0 <= min && max <= 255) {
      out.write(ARRAY_LONG_B);
      LongPacker.packInt(out, val.length);
      for (long l : val) {
        out.write((int) l);
      }
    } else if (min >= Short.MIN_VALUE && max <= Short.MAX_VALUE) {
      out.write(ARRAY_LONG_S);
      LongPacker.packInt(out, val.length);
      for (long l : val) {
        out.writeShort((short) l);
      }
    } else if (compress && val.length > 250) {
      out.write(ARRAY_LONG_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else if (0 <= min && max <= Long.MAX_VALUE) {
      out.write(ARRAY_LONG_PACKED);
      LongPacker.packInt(out, val.length);
      for (long l : val) {
        LongPacker.packLong(out, l);
      }
    } else if (Integer.MIN_VALUE <= min && max <= Integer.MAX_VALUE) {
      out.write(ARRAY_LONG_I);
      LongPacker.packInt(out, val.length);
      for (long l : val) {
        out.writeInt((int) l);
      }
    } else {
      out.write(ARRAY_LONG_L);
      LongPacker.packInt(out, val.length);
      for (long l : val) {
        out.writeLong(l);
      }
    }
  }

  private static void serializeLongLongArray(final DataOutput out, final long[][] val, boolean compress)
      throws IOException {
    out.write(LONG_LONG_ARRAY);
    LongPacker.packInt(out, val.length);

    for (long[] v : val) {
      serializeLongArray(out, v, compress);
    }
  }

  private static void serializeByteArray(final DataOutput out, final byte[] val, boolean compress)
      throws IOException {
    if (compress && val.length > 250) {
      out.write(BYTE_ARRAY_C);
      byte[] b = Snappy.compress(val);
      LongPacker.packInt(out, b.length);
      out.write(b);
    } else {
      out.write(BYTE_ARRAY);
      LongPacker.packInt(out, val.length);
      out.write(val);
    }
  }

  private static void serializeStringArray(final DataOutput out, final String[] val)
      throws IOException {
    out.write(STRING_ARRAY);
    LongPacker.packInt(out, val.length);
    for (String s : val) {
      serializeString(out, s);
    }
  }

  private void serializeObjectArray(final DataOutput out, final Object[] val)
      throws IOException {
    out.write(ARRAY_OBJECT);
    LongPacker.packInt(out, val.length);
    for (Object o : val) {
      serialize(out, o);
    }
  }

  public Object deserialize(byte[] buf)
      throws ClassNotFoundException, IOException {
    DataInputOutput bs = new DataInputOutput(buf);
    Object ret = deserialize(bs);
    if (bs.available() != 0) {
      throw new RuntimeException("bytes left: " + bs.available());
    }

    return ret;
  }

  public Object deserialize(DataInput is)
      throws IOException, ClassNotFoundException {
    Object ret = null;

    final int head = is.readUnsignedByte();

    if (head >= CUSTOM) {
      Serializer serializer = serializers.getSerializer(head - CUSTOM);
      ret = serializer.read(is);
    } else {
      switch (head) {
        case NULL:
          break;
        case BOOLEAN_TRUE:
          ret = Boolean.TRUE;
          break;
        case BOOLEAN_FALSE:
          ret = Boolean.FALSE;
          break;
        case INTEGER_MINUS_1:
          ret = Integer.valueOf(-1);
          break;
        case INTEGER_0:
          ret = Integer.valueOf(0);
          break;
        case INTEGER_1:
          ret = Integer.valueOf(1);
          break;
        case INTEGER_2:
          ret = Integer.valueOf(2);
          break;
        case INTEGER_3:
          ret = Integer.valueOf(3);
          break;
        case INTEGER_4:
          ret = Integer.valueOf(4);
          break;
        case INTEGER_5:
          ret = Integer.valueOf(5);
          break;
        case INTEGER_6:
          ret = Integer.valueOf(6);
          break;
        case INTEGER_7:
          ret = Integer.valueOf(7);
          break;
        case INTEGER_8:
          ret = Integer.valueOf(8);
          break;
        case INTEGER_MINUS_MAX:
          ret = Integer.valueOf(Integer.MIN_VALUE);
          break;
        case INTEGER_255:
          ret = Integer.valueOf(is.readUnsignedByte());
          break;
        case INTEGER_PACK_NEG:
          ret = Integer.valueOf(-LongPacker.unpackInt(is));
          break;
        case INTEGER_PACK:
          ret = Integer.valueOf(LongPacker.unpackInt(is));
          break;
        case LONG_MINUS_1:
          ret = Long.valueOf(-1);
          break;
        case LONG_0:
          ret = Long.valueOf(0);
          break;
        case LONG_1:
          ret = Long.valueOf(1);
          break;
        case LONG_2:
          ret = Long.valueOf(2);
          break;
        case LONG_3:
          ret = Long.valueOf(3);
          break;
        case LONG_4:
          ret = Long.valueOf(4);
          break;
        case LONG_5:
          ret = Long.valueOf(5);
          break;
        case LONG_6:
          ret = Long.valueOf(6);
          break;
        case LONG_7:
          ret = Long.valueOf(7);
          break;
        case LONG_8:
          ret = Long.valueOf(8);
          break;
        case LONG_255:
          ret = Long.valueOf(is.readUnsignedByte());
          break;
        case LONG_PACK_NEG:
          ret = Long.valueOf(-LongPacker.unpackLong(is));
          break;
        case LONG_PACK:
          ret = Long.valueOf(LongPacker.unpackLong(is));
          break;
        case LONG_MINUS_MAX:
          ret = Long.valueOf(Long.MIN_VALUE);
          break;
        case SHORT_MINUS_1:
          ret = Short.valueOf((short) -1);
          break;
        case SHORT_0:
          ret = Short.valueOf((short) 0);
          break;
        case SHORT_1:
          ret = Short.valueOf((short) 1);
          break;
        case SHORT_255:
          ret = Short.valueOf((short) is.readUnsignedByte());
          break;
        case SHORT_FULL:
          ret = Short.valueOf(is.readShort());
          break;
        case BYTE_MINUS_1:
          ret = Byte.valueOf((byte) -1);
          break;
        case BYTE_0:
          ret = Byte.valueOf((byte) 0);
          break;
        case BYTE_1:
          ret = Byte.valueOf((byte) 1);
          break;
        case BYTE_FULL:
          ret = Byte.valueOf(is.readByte());
          break;
        case SHORT_ARRAY:
          ret = deserializeShortArray(is);
          break;
        case BOOLEAN_ARRAY:
          ret = deserializeBooleanArray(is);
          break;
        case DOUBLE_ARRAY:
          ret = deserializeDoubleArray(is);
          break;
        case FLOAT_ARRAY:
          ret = deserializeFloatArray(is);
          break;
        case CHAR_ARRAY:
          ret = deserializeCharArray(is);
          break;
        case SHORT_ARRAY_C:
          ret = deserializeShortCompressedArray(is);
          break;
        case DOUBLE_ARRAY_C:
          ret = deserializeDoubleCompressedArray(is);
          break;
        case FLOAT_ARRAY_C:
          ret = deserializeFloatCompressedArray(is);
          break;
        case CHAR_ARRAY_C:
          ret = deserializeCharCompressedArray(is);
          break;
        case CHAR:
          ret = Character.valueOf(is.readChar());
          break;
        case FLOAT_MINUS_1:
          ret = Float.valueOf(-1);
          break;
        case FLOAT_0:
          ret = Float.valueOf(0);
          break;
        case FLOAT_1:
          ret = Float.valueOf(1);
          break;
        case FLOAT_255:
          ret = Float.valueOf(is.readUnsignedByte());
          break;
        case FLOAT_SHORT:
          ret = Float.valueOf(is.readShort());
          break;
        case FLOAT_FULL:
          ret = Float.valueOf(is.readFloat());
          break;
        case DOUBLE_MINUS_1:
          ret = Double.valueOf(-1);
          break;
        case DOUBLE_0:
          ret = Double.valueOf(0);
          break;
        case DOUBLE_1:
          ret = Double.valueOf(1);
          break;
        case DOUBLE_255:
          ret = Double.valueOf(is.readUnsignedByte());
          break;
        case DOUBLE_SHORT:
          ret = Double.valueOf(is.readShort());
          break;
        case DOUBLE_FULL:
          ret = Double.valueOf(is.readDouble());
          break;
        case BIGINTEGER:
          ret = new BigInteger((byte[]) deserialize(is));
          break;
        case BIGDECIMAL:
          ret = new BigDecimal(new BigInteger((byte[]) deserialize(is)), LongPacker.unpackInt(is));
          break;
        case STRING:
          ret = deserializeString(is);
          break;
        case STRING_EMPTY:
          ret = EMPTY_STRING;
          break;
        case CLASS:
          ret = deserializeClass(is);
          break;
        case ARRAY_INT_B:
          ret = deserializeArrayIntB(is);
          break;
        case ARRAY_INT_S:
          ret = deserializeArrayIntS(is);
          break;
        case ARRAY_INT_I:
          ret = deserializeArrayIntI(is);
          break;
        case ARRAY_INT_C:
          ret = deserializeArrayIntCompressed(is);
          break;
        case ARRAY_INT_PACKED:
          ret = deserializeArrayIntPack(is);
          break;
        case ARRAY_LONG_B:
          ret = deserializeArrayLongB(is);
          break;
        case ARRAY_LONG_S:
          ret = deserializeArrayLongS(is);
          break;
        case ARRAY_LONG_I:
          ret = deserializeArrayLongI(is);
          break;
        case ARRAY_LONG_L:
          ret = deserializeArrayLongL(is);
          break;
        case ARRAY_LONG_C:
          ret = deserializeArrayLongCompressed(is);
          break;
        case ARRAY_LONG_PACKED:
          ret = deserializeArrayLongPack(is);
          break;
        case BYTE_ARRAY:
          ret = deserializeByteArray(is);
          break;
        case BYTE_ARRAY_C:
          ret = deserializeByteCompressedArray(is);
          break;
        case STRING_ARRAY:
          ret = deserializeStringArray(is);
          break;
        case INT_INT_ARRAY:
          ret = deserializeIntIntArray(is);
          break;
        case LONG_LONG_ARRAY:
          ret = deserializeLongLongArray(is);
          break;
        case ARRAY_OBJECT:
          ret = deserializeArrayObject(is);
          break;
        case -1:
          throw new EOFException();
      }
    }
    return ret;
  }

  private static String deserializeString(DataInput buf)
      throws IOException {
    int len = LongPacker.unpackInt(buf);
    char[] b = new char[len];
    for (int i = 0; i < len; i++) {
      b[i] = (char) LongPacker.unpackInt(buf);
    }

    return new String(b);
  }

  private static Class deserializeClass(DataInput is)
      throws IOException, ClassNotFoundException {
    is.readByte();
    String className = (String) deserializeString(is);
    Class cls = Class.forName(className);
    return cls;
  }

  private static short[] deserializeShortArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    short[] ret = new short[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readShort();
    }
    return ret;
  }

  private static short[] deserializeShortCompressedArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompressShortArray(b);
  }

  private static float[] deserializeFloatArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    float[] ret = new float[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readFloat();
    }
    return ret;
  }

  private static float[] deserializeFloatCompressedArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompressFloatArray(b);
  }

  private static double[] deserializeDoubleArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    double[] ret = new double[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readDouble();
    }
    return ret;
  }

  private static double[] deserializeDoubleCompressedArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompressDoubleArray(b);
  }

  private static char[] deserializeCharArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    char[] ret = new char[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readChar();
    }
    return ret;
  }

  private static char[] deserializeCharCompressedArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompressCharArray(b);
  }

  private static boolean[] deserializeBooleanArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    boolean[] ret = new boolean[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readBoolean();
    }
    return ret;
  }

  private static String[] deserializeStringArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    String[] ret = new String[size];
    for (int i = 0; i < size; i++) {
      final int head = is.readUnsignedByte();
      switch (head) {
        case STRING:
          ret[i] = deserializeString(is);
          break;
        case STRING_EMPTY:
          ret[i] = EMPTY_STRING;
          break;
        default:
          throw new EOFException();
      }
    }
    return ret;
  }

  private static byte[] deserializeByteArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return b;
  }

  private static byte[] deserializeByteCompressedArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompress(b);
  }

  private Object[] deserializeArrayObject(DataInput is)
      throws IOException, ClassNotFoundException {
    int size = LongPacker.unpackInt(is);

    Object[] s = (Object[]) Array.newInstance(Object.class, size);
    for (int i = 0; i < size; i++) {
      s[i] = deserialize(is);
    }
    return s;
  }

  private static long[] deserializeArrayLongL(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    long[] ret = new long[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readLong();
    }
    return ret;
  }

  private static long[] deserializeArrayLongI(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    long[] ret = new long[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readInt();
    }
    return ret;
  }

  private static long[] deserializeArrayLongS(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    long[] ret = new long[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readShort();
    }
    return ret;
  }

  private static long[] deserializeArrayLongB(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    long[] ret = new long[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readUnsignedByte();
      if (ret[i] < 0) {
        throw new EOFException();
      }
    }
    return ret;
  }

  private static long[] deserializeArrayLongCompressed(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompressLongArray(b);
  }

  private static long[][] deserializeLongLongArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    long[][] res = new long[size][];
    for (int i = 0; i < size; i++) {
      final int head = is.readUnsignedByte();
      switch (head) {
        case ARRAY_LONG_B:
          res[i] = deserializeArrayLongB(is);
          break;
        case ARRAY_LONG_S:
          res[i] = deserializeArrayLongS(is);
          break;
        case ARRAY_LONG_I:
          res[i] = deserializeArrayLongI(is);
          break;
        case ARRAY_LONG_L:
          res[i] = deserializeArrayLongL(is);
          break;
        case ARRAY_LONG_C:
          res[i] = deserializeArrayLongCompressed(is);
          break;
        case ARRAY_LONG_PACKED:
          res[i] = deserializeArrayLongPack(is);
          break;
        default:
          throw new IOException("Not recognized");
      }
    }
    return res;
  }

  private static int[][] deserializeIntIntArray(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    int[][] res = new int[size][];
    for (int i = 0; i < size; i++) {
      final int head = is.readUnsignedByte();
      switch (head) {
        case ARRAY_INT_B:
          res[i] = deserializeArrayIntB(is);
          break;
        case ARRAY_INT_S:
          res[i] = deserializeArrayIntS(is);
          break;
        case ARRAY_INT_I:
          res[i] = deserializeArrayIntI(is);
          break;
        case ARRAY_INT_C:
          res[i] = deserializeArrayIntCompressed(is);
          break;
        case ARRAY_INT_PACKED:
          res[i] = deserializeArrayIntPack(is);
          break;
        default:
          throw new IOException("Not recognized");
      }
    }
    return res;
  }

  private static int[] deserializeArrayIntI(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    int[] ret = new int[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readInt();
    }
    return ret;
  }

  private static int[] deserializeArrayIntS(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    int[] ret = new int[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readShort();
    }
    return ret;
  }

  private static int[] deserializeArrayIntB(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    int[] ret = new int[size];
    for (int i = 0; i < size; i++) {
      ret[i] = is.readUnsignedByte();
      if (ret[i] < 0) {
        throw new EOFException();
      }
    }
    return ret;
  }

  private static int[] deserializeArrayIntPack(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    if (size < 0) {
      throw new EOFException();
    }

    int[] ret = new int[size];
    for (int i = 0; i < size; i++) {
      ret[i] = LongPacker.unpackInt(is);
    }
    return ret;
  }

  private static int[] deserializeArrayIntCompressed(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    byte[] b = new byte[size];
    is.readFully(b);
    return Snappy.uncompressIntArray(b);
  }

  private static long[] deserializeArrayLongPack(DataInput is)
      throws IOException {
    int size = LongPacker.unpackInt(is);
    if (size < 0) {
      throw new EOFException();
    }

    long[] ret = new long[size];
    for (int i = 0; i < size; i++) {
      ret[i] = LongPacker.unpackLong(is);
    }
    return ret;
  }
}