package org.infinispan.marshall.core;

import java.io.EOFException;
import java.io.IOException;
import java.util.Map;

import org.infinispan.commons.util.HopscotchHashMap;

final class Primitives {

   // 0x03-0x0F reserved
   static final int ID_BYTE_ARRAY                  = 0x01; // byte[].class
   static final int ID_STRING                      = 0x02; // String.class

   static final int ID_BOOLEAN_OBJ                 = 0x10; // Boolean.class
   static final int ID_BYTE_OBJ                    = 0x11; // ..etc..
   static final int ID_CHAR_OBJ                    = 0x12;
   static final int ID_DOUBLE_OBJ                  = 0x13;
   static final int ID_FLOAT_OBJ                   = 0x14;
   static final int ID_INT_OBJ                     = 0x15;
   static final int ID_LONG_OBJ                    = 0x16;
   static final int ID_SHORT_OBJ                   = 0x17;

   static final int ID_BOOLEAN_ARRAY               = 0x18; // boolean[].class
   static final int ID_CHAR_ARRAY                  = 0x19; // ..etc..
   static final int ID_DOUBLE_ARRAY                = 0x1A;
   static final int ID_FLOAT_ARRAY                 = 0x1B;
   static final int ID_INT_ARRAY                   = 0x1C;
   static final int ID_LONG_ARRAY                  = 0x1D;
   static final int ID_SHORT_ARRAY                 = 0x1E;

   // 0x1F-0x27 unused

   static final int ID_ARRAY_EMPTY                 = 0x28; // zero elements
   static final int ID_ARRAY_SMALL                 = 0x29; // <=0x100 elements
   static final int ID_ARRAY_MEDIUM                = 0x2A; // <=0x10000 elements
   static final int ID_ARRAY_LARGE                 = 0x2B; // <0x80000000 elements

   static final int SMALL_ARRAY_MIN                = 0x1;
   static final int SMALL_ARRAY_MAX                = 0x100;
   static final int MEDIUM_ARRAY_MIN               = 0x101;
   static final int MEDIUM_ARRAY_MAX               = 0x10100;

   static final Map<Class<?>, Integer> PRIMITIVES = new HopscotchHashMap<>(18);

   static {
      PRIMITIVES.put(String.class, ID_STRING);
      PRIMITIVES.put(byte[].class, ID_BYTE_ARRAY);

      PRIMITIVES.put(Boolean.class, ID_BOOLEAN_OBJ);
      PRIMITIVES.put(Byte.class, ID_BYTE_OBJ);
      PRIMITIVES.put(Character.class, ID_CHAR_OBJ);
      PRIMITIVES.put(Double.class, ID_DOUBLE_OBJ);
      PRIMITIVES.put(Float.class, ID_FLOAT_OBJ);
      PRIMITIVES.put(Integer.class, ID_INT_OBJ);
      PRIMITIVES.put(Long.class, ID_LONG_OBJ);
      PRIMITIVES.put(Short.class, ID_SHORT_OBJ);

      PRIMITIVES.put(boolean[].class, ID_BOOLEAN_ARRAY);
      PRIMITIVES.put(char[].class, ID_CHAR_ARRAY);
      PRIMITIVES.put(double[].class, ID_DOUBLE_ARRAY);
      PRIMITIVES.put(float[].class, ID_FLOAT_ARRAY);
      PRIMITIVES.put(int[].class, ID_INT_ARRAY);
      PRIMITIVES.put(long[].class, ID_LONG_ARRAY);
      PRIMITIVES.put(short[].class, ID_SHORT_ARRAY);
   }

   private Primitives() {
   }

   static void writePrimitive(Object obj, BytesObjectOutput out, int id) throws IOException {
      out.writeByte(id);
      writeRawPrimitive(obj, out, id);
   }

   static void writeRawPrimitive(Object obj, BytesObjectOutput out, int id) throws IOException {
      switch (id) {
         case ID_BYTE_ARRAY:
            Primitives.writeByteArray((byte[]) obj, out);
            break;
         case ID_STRING:
            out.writeString((String) obj);
            break;
         case ID_BOOLEAN_OBJ:
            out.writeBoolean((boolean) obj);
            break;
         case ID_BYTE_OBJ:
            out.writeByte((byte) obj);
            break;
         case ID_CHAR_OBJ:
            out.writeChar((char) obj);
            break;
         case ID_DOUBLE_OBJ:
            out.writeDouble((double) obj);
            break;
         case ID_FLOAT_OBJ:
            out.writeFloat((float) obj);
            break;
         case ID_INT_OBJ:
            out.writeInt((int) obj);
            break;
         case ID_LONG_OBJ:
            out.writeLong((long) obj);
            break;
         case ID_SHORT_OBJ:
            out.writeShort((short) obj);
            break;
         case ID_BOOLEAN_ARRAY:
            Primitives.writeBooleanArray((boolean[]) obj, out);
            break;
         case ID_CHAR_ARRAY:
            Primitives.writeCharArray((char[]) obj, out);
            break;
         case ID_DOUBLE_ARRAY:
            Primitives.writeDoubleArray((double[]) obj, out);
            break;
         case ID_FLOAT_ARRAY:
            Primitives.writeFloatArray((float[]) obj, out);
            break;
         case ID_INT_ARRAY:
            Primitives.writeIntArray((int[]) obj, out);
            break;
         case ID_LONG_ARRAY:
            Primitives.writeLongArray((long[]) obj, out);
            break;
         case ID_SHORT_ARRAY:
            Primitives.writeShortArray((short[]) obj, out);
            break;
         default:
            throw new IOException("Unknown primitive type: " + obj);
      }
   }

   static Object readPrimitive(BytesObjectInput in) throws IOException, ClassNotFoundException {
      int subId = in.readUnsignedByte();
      return readRawPrimitive(in, subId);
   }

   static Object readRawPrimitive(BytesObjectInput in, int subId) throws IOException {
      switch (subId) {
         case ID_BYTE_ARRAY:
            return readByteArray(in);
         case ID_STRING:
            return in.readString();
         case ID_BOOLEAN_OBJ:
            return in.readBoolean();
         case ID_BYTE_OBJ:
            return in.readByte();
         case ID_CHAR_OBJ:
            return in.readChar();
         case ID_DOUBLE_OBJ:
            return in.readDouble();
         case ID_FLOAT_OBJ:
            return in.readFloat();
         case ID_INT_OBJ:
            return in.readInt();
         case ID_LONG_OBJ:
            return in.readLong();
         case ID_SHORT_OBJ:
            return in.readShort();
         case ID_BOOLEAN_ARRAY:
            return readBooleanArray(in);
         case ID_CHAR_ARRAY:
            return readCharArray(in);
         case ID_DOUBLE_ARRAY:
            return readDoubleArray(in);
         case ID_FLOAT_ARRAY:
            return readFloatArray(in);
         case ID_INT_ARRAY:
            return readIntArray(in);
         case ID_LONG_ARRAY:
            return readLongArray(in);
         case ID_SHORT_ARRAY:
            return readShortArray(in);
         default:
            throw new IOException("Unknown primitive sub id: " + Integer.toHexString(subId));
      }
   }

   private static void writeByteArray(byte[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         out.write(obj, 0, len);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         out.write(obj, 0, len);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         out.write(obj, 0, len);
      }
   }

   private static void writeBooleanArray(boolean[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         writeBooleans(obj, out);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         writeBooleans(obj, out);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         writeBooleans(obj, out);
      }
   }

   private static void writeBooleans(boolean[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      final int bc = len & ~7;
      for (int i = 0; i < bc;) {
         out.write(
               (obj[i++] ? 1 : 0)
                     | (obj[i++] ? 2 : 0)
                     | (obj[i++] ? 4 : 0)
                     | (obj[i++] ? 8 : 0)
                     | (obj[i++] ? 16 : 0)
                     | (obj[i++] ? 32 : 0)
                     | (obj[i++] ? 64 : 0)
                     | (obj[i++] ? 128 : 0)
         );
      }
      if (bc < len) {
         int o = 0;
         int bit = 1;
         for (int i = bc; i < len; i++) {
            if (obj[i]) o |= bit;
            bit <<= 1;
         }
         out.writeByte(o);
      }
   }

   private static void writeCharArray(char[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         for (char v : obj) out.writeChar(v);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         for (char v : obj) out.writeChar(v);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         for (char v : obj) out.writeChar(v);
      }
   }

   private static void writeDoubleArray(double[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         for (double v : obj) out.writeDouble(v);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         for (double v : obj) out.writeDouble(v);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         for (double v : obj) out.writeDouble(v);
      }
   }

   private static void writeFloatArray(float[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         for (float v : obj) out.writeFloat(v);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         for (float v : obj) out.writeFloat(v);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         for (float v : obj) out.writeFloat(v);
      }
   }

   private static void writeIntArray(int[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         for (int v : obj) out.writeInt(v);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         for (int v : obj) out.writeInt(v);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         for (int v : obj) out.writeInt(v);
      }
   }

   private static void writeLongArray(long[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         for (long v : obj) out.writeLong(v);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         for (long v : obj) out.writeLong(v);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         for (long v : obj) out.writeLong(v);
      }
   }

   private static void writeShortArray(short[] obj, BytesObjectOutput out) {
      final int len = obj.length;
      if (len == 0) {
         out.writeByte(ID_ARRAY_EMPTY);
      } else if (len <= SMALL_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_SMALL);
         out.writeByte(len - SMALL_ARRAY_MIN);
         for (short v : obj) out.writeShort(v);
      } else if (len <= MEDIUM_ARRAY_MAX) {
         out.writeByte(ID_ARRAY_MEDIUM);
         out.writeShort(len - MEDIUM_ARRAY_MIN);
         for (short v : obj) out.writeShort(v);
      } else {
         out.writeByte(ID_ARRAY_LARGE);
         out.writeInt(len);
         for (short v : obj) out.writeShort(v);
      }
   }

   private static byte[] readByteArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new byte[]{};
         case ID_ARRAY_SMALL:
            return readFully(mkByteArray(in.readUnsignedByte() + SMALL_ARRAY_MIN), in);
         case ID_ARRAY_MEDIUM:
            return readFully(mkByteArray(in.readUnsignedShort() + MEDIUM_ARRAY_MIN), in);
         case ID_ARRAY_LARGE:
            return readFully(new byte[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static byte[] mkByteArray(int len) {
      return new byte[len];
   }

   private static byte[] readFully(byte[] arr, BytesObjectInput in) throws EOFException {
      in.readFully(arr);
      return arr;
   }

   private static boolean[] readBooleanArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new boolean[]{};
         case ID_ARRAY_SMALL:
            return readBooleans(mkBooleanArray(in.readUnsignedByte() + SMALL_ARRAY_MIN), in);
         case ID_ARRAY_MEDIUM:
            return readBooleans(mkBooleanArray(in.readUnsignedShort() + MEDIUM_ARRAY_MIN), in);
         case ID_ARRAY_LARGE:
            return readBooleans(new boolean[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static boolean[] mkBooleanArray(int len) {
      return new boolean[len];
   }

   private static boolean[] readBooleans(boolean[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      int v;
      int bc = len & ~7;
      for (int i = 0; i < bc; ) {
         v = in.readByte();
         arr[i++] = (v & 1) != 0;
         arr[i++] = (v & 2) != 0;
         arr[i++] = (v & 4) != 0;
         arr[i++] = (v & 8) != 0;
         arr[i++] = (v & 16) != 0;
         arr[i++] = (v & 32) != 0;
         arr[i++] = (v & 64) != 0;
         arr[i++] = (v & 128) != 0;
      }
      if (bc < len) {
         v = in.readByte();
         switch (len & 7) {
            case 7:
               arr[bc + 6] = (v & 64) != 0;
            case 6:
               arr[bc + 5] = (v & 32) != 0;
            case 5:
               arr[bc + 4] = (v & 16) != 0;
            case 4:
               arr[bc + 3] = (v & 8) != 0;
            case 3:
               arr[bc + 2] = (v & 4) != 0;
            case 2:
               arr[bc + 1] = (v & 2) != 0;
            case 1:
               arr[bc] = (v & 1) != 0;
         }
      }
      return arr;
   }

   private static char[] readCharArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new char[]{};
         case ID_ARRAY_SMALL:
            return readChars(mkCharArray(in.readUnsignedByte() + SMALL_ARRAY_MIN), in);
         case ID_ARRAY_MEDIUM:
            return readChars(mkCharArray(in.readUnsignedShort() + MEDIUM_ARRAY_MIN), in);
         case ID_ARRAY_LARGE:
            return readChars(new char[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static char[] mkCharArray(int len) {
      return new char[len];
   }

   private static char[] readChars(char[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      for (int i = 0; i < len; i ++) arr[i] = in.readChar();
      return arr;
   }

   private static double[] readDoubleArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new double[]{};
         case ID_ARRAY_SMALL:
            return readDoubles(new double[in.readUnsignedByte() + SMALL_ARRAY_MIN], in);
         case ID_ARRAY_MEDIUM:
            return readDoubles(new double[in.readUnsignedShort() + MEDIUM_ARRAY_MIN], in);
         case ID_ARRAY_LARGE:
            return readDoubles(new double[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static double[] readDoubles(double[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      for (int i = 0; i < len; i ++) arr[i] = in.readDouble();
      return arr;
   }

   private static float[] readFloatArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new float[]{};
         case ID_ARRAY_SMALL:
            return readFloats(new float[in.readUnsignedByte() + SMALL_ARRAY_MIN], in);
         case ID_ARRAY_MEDIUM:
            return readFloats(new float[in.readUnsignedShort() + MEDIUM_ARRAY_MIN], in);
         case ID_ARRAY_LARGE:
            return readFloats(new float[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static float[] readFloats(float[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      for (int i = 0; i < len; i ++) arr[i] = in.readFloat();
      return arr;
   }

   private static int[] readIntArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new int[]{};
         case ID_ARRAY_SMALL:
            return readInts(new int[in.readUnsignedByte() + SMALL_ARRAY_MIN], in);
         case ID_ARRAY_MEDIUM:
            return readInts(new int[in.readUnsignedShort() + MEDIUM_ARRAY_MIN], in);
         case ID_ARRAY_LARGE:
            return readInts(new int[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static int[] readInts(int[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      for (int i = 0; i < len; i ++) arr[i] = in.readInt();
      return arr;
   }

   private static long[] readLongArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new long[]{};
         case ID_ARRAY_SMALL:
            return readLongs(new long[in.readUnsignedByte() + SMALL_ARRAY_MIN], in);
         case ID_ARRAY_MEDIUM:
            return readLongs(new long[in.readUnsignedShort() + MEDIUM_ARRAY_MIN], in);
         case ID_ARRAY_LARGE:
            return readLongs(new long[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static long[] readLongs(long[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      for (int i = 0; i < len; i ++) arr[i] = in.readLong();
      return arr;
   }

   private static short[] readShortArray(BytesObjectInput in) throws IOException {
      byte type = in.readByte();
      switch (type) {
         case ID_ARRAY_EMPTY:
            return new short[]{};
         case ID_ARRAY_SMALL:
            return readShorts(new short[in.readUnsignedByte() + SMALL_ARRAY_MIN], in);
         case ID_ARRAY_MEDIUM:
            return readShorts(new short[in.readUnsignedShort() + MEDIUM_ARRAY_MIN], in);
         case ID_ARRAY_LARGE:
            return readShorts(new short[in.readInt()], in);
         default:
            throw new IOException("Unknown array type: " + Integer.toHexString(type));
      }
   }

   private static short[] readShorts(short[] arr, BytesObjectInput in) throws EOFException {
      final int len = arr.length;
      for (int i = 0; i < len; i ++) arr[i] = in.readShort();
      return arr;
   }
}
