package javajs.util;

import java.io.BufferedInputStream;
import java.util.Hashtable;
import java.util.Map;

import javajs.api.GenericBinaryDocumentReader;

/**
 * A simple MessagePack reader. See https://github.com/msgpack/msgpack/blob/master/spec.md
 * with very few dependencies.
 * 
 * Nuances: 
 * 
 *  Does not implement unsigned int32 or int64 (delivers simple integers in all cases).
 *  Does not use doubles; just floats
 *  
 * Note: 
 * 
 *  homogeneousArrays == true will deliver null for empty array.
 * 
 * 
 * Use in MMTF:
 * 
 * 
 *     BufferedInputStream bs = [whatever]
 *     
 *      GenericBinaryDocument binaryDoc =  new javajs.util.BinaryDocument();
 *   
 *      binaryDoc.setStream(bs, true);
 * 
 * 
 *     map = (new MessagePackReader(binaryDoc, true)).readMap();
 * 
 *     entities = (Object[]) map.get("entityList");
 *
 *     float[] x = (float[]) decode((byte[]) map.get("xCoordList"))
 *     
 * 
 * @author Bob Hanson hansonr@stolaf.edu
 */

public class MessagePackReader {

  private GenericBinaryDocumentReader doc;

  private boolean isHomo;// homogeneous arrays -- use int[] not Integer

  // these maps must be checked for the specific number of bits, in the following order:
  private final static int POSITIVEFIXINT_x80 = 0x80; //0xxxxxxx
  private final static int FIXMAP_xF0         = 0x80; //1000xxxx
//  private final static int FIXARRAY_xF0       = 0x90; //1001xxxx
  private final static int FIXSTR_xE0         = 0xa0; //101xxxxx
  private final static int NEGATIVEFIXINT_xE0 = 0xe0; //111xxxxx
  private final static int DEFINITE_xE0       = 0xc0; //110xxxxx
  
  private final static int NIL          = 0xc0;
//  private final static int (NEVERUSED)        = 0xc1;
  private final static int FALSE        = 0xc2;
  private final static int TRUE         = 0xc3;
  private final static int BIN8         = 0xc4;
  private final static int BIN16        = 0xc5;
  private final static int BIN32        = 0xc6;
  private final static int EXT8         = 0xc7;
  private final static int EXT16        = 0xc8;
  private final static int EXT32        = 0xc9;
  private final static int FLOAT32      = 0xca;
  private final static int FLOAT64      = 0xcb;
  private final static int UINT8        = 0xcc;
  private final static int UINT16       = 0xcd;
  private final static int UINT32       = 0xce;
  private final static int UINT64       = 0xcf;
  private final static int INT8         = 0xd0;
  private final static int INT16        = 0xd1;
  private final static int INT32        = 0xd2;
  private final static int INT64        = 0xd3;
  private final static int FIXEXT1      = 0xd4;
  private final static int FIXEXT2      = 0xd5;
  private final static int FIXEXT4      = 0xd6;
  private final static int FIXEXT8      = 0xd7;
  private final static int FIXEXT16     = 0xd8;
  private final static int STR8         = 0xd9;
  private final static int STR16        = 0xda;
  private final static int STR32        = 0xdb;
  private final static int ARRAY16      = 0xdc;
  private final static int ARRAY32      = 0xdd;
  private final static int MAP16        = 0xde;
  private final static int MAP32        = 0xdf;

  public MessagePackReader(GenericBinaryDocumentReader binaryDoc, boolean isHomogeneousArrays) {
    isHomo = isHomogeneousArrays;
    doc = binaryDoc;
  }

  public MessagePackReader() {
    // for reflection
  }
  
  public Map<String, Object> getMapForStream(BufferedInputStream is) throws Exception {
    doc = new BinaryDocument().setStream(is, true);
    Map<String, Object> map = readMap();
    is.close();
    return map;
  }
  
  @SuppressWarnings("unchecked")
  public Map<String, Object> readMap() throws Exception {
    return (Map<String, Object>) getNext(null, 0);
  }
  
  public Object getNext(Object array, int pt) throws Exception {
    int b = doc.readByte() & 0xFF;
    int be0 = b & 0xE0;
    if ((b & POSITIVEFIXINT_x80) == 0) {
      if (array != null) {
        ((int[]) array)[pt] = b;
        return null;
      }
      return Integer.valueOf(b);
    }
    switch (be0) {
    case NEGATIVEFIXINT_xE0:
      b = BC.intToSignedInt(b | 0xFFFFFF00);
      if (array != null) {
        ((int[]) array)[pt] = b;
        return null;
      }
      return Integer.valueOf(b);
    case FIXSTR_xE0: {
      String s = doc.readString(b & 0x1F);
      if (array != null) {
        ((String[]) array)[pt] = s; 
        return null;
      } 
      return s;
    }
    case FIXMAP_xF0:
      return ((b & 0xF0) == FIXMAP_xF0 ? getMap(b & 0x0F) : getArray(b & 0x0F));
    case DEFINITE_xE0:
      switch (b) {
      case NIL:
        return null;
      case FALSE:
        return Boolean.FALSE;
      case TRUE:
        return Boolean.TRUE;
      case EXT8:
        return getObject(doc.readUInt8());
      case EXT16:
        return getObject(doc.readUnsignedShort());
      case EXT32:
        return getObject(doc.readInt()); // should be unsigned int
      case FIXEXT1:
        return getObject(1);
      case FIXEXT2:
        return getObject(2);
      case FIXEXT4:
        return getObject(4);
      case FIXEXT8:
        return getObject(8);
      case FIXEXT16:
        return getObject(16);
      case ARRAY16:
        return getArray(doc.readUnsignedShort());
      case ARRAY32:
        return getArray(doc.readInt());
      case MAP16:
        return getMap(doc.readUnsignedShort());
      case MAP32:
        return getMap(doc.readInt());

        // binary arrays:

      case BIN8:
        return doc.readBytes(doc.readUInt8());
      case BIN16:
        return doc.readBytes(doc.readUnsignedShort());
      case BIN32:
        return doc.readBytes(doc.readInt());
      }
      if (array == null) {
        switch (b) {
        case FLOAT32:
          return Float.valueOf(doc.readFloat());
        case FLOAT64:
          return Float.valueOf((float) doc.readDouble());
        case UINT8:
          return Integer.valueOf(doc.readUInt8());
        case UINT16:
          return Integer.valueOf(doc.readUnsignedShort());
        case UINT32:
          return Integer.valueOf(doc.readInt()); // technically should be UInt32
        case UINT64:
          return Long.valueOf(doc.readLong()); // should be unsigned long; incompatible with JavaScript!
        case INT8:
          return Integer.valueOf(doc.readByte());
        case INT16:
          return Integer.valueOf(doc.readShort());
        case INT32:
          return Integer.valueOf(doc.readInt()); // should be Unsigned Int here
        case INT64:
          return Long.valueOf(doc.readLong());
        case STR8:
          return doc.readString(doc.readUInt8());
        case STR16:
          return doc.readString(doc.readShort());
        case STR32:
          return doc.readString(doc.readInt());
        }
      } else {
        switch (b) {
        case FLOAT32:
          ((float[]) array)[pt] = doc.readFloat();
          break;
        case FLOAT64:
          ((float[]) array)[pt] = (float) doc.readDouble();
          break;
        case UINT8:
          ((int[]) array)[pt] = doc.readUInt8();
          break;
        case UINT16:
          ((int[]) array)[pt] = doc.readUnsignedShort();
          break;
        case UINT32:
          ((int[]) array)[pt] =  doc.readInt(); // should be unsigned int
          break;
        case UINT64:
          ((int[]) array)[pt] =  (int) doc.readLong(); // should be unsigned long; incompatible with JavaScript!
          break;
        case INT8:
          ((int[]) array)[pt] =  doc.readByte();
          break;
        case INT16:
          ((int[]) array)[pt] = doc.readShort();
          break;
        case INT32:
          ((int[]) array)[pt] =  doc.readInt(); // should be Unsigned Int here
          break;
        case INT64:
          ((int[]) array)[pt] =  (int) doc.readLong();
          break;
        case STR8:
          ((String[]) array)[pt] = doc.readString(doc.readUInt8());
          break;
        case STR16:
          ((String[]) array)[pt] = doc.readString(doc.readShort());
          break;
        case STR32:
          ((String[]) array)[pt] = doc.readString(doc.readInt());
          break;
        }
      }
    }
    return null;
  }

  private Object getObject(int n) throws Exception {
    return new Object[] { Integer.valueOf(doc.readUInt8()), doc.readBytes(n) };
  }

  private Object getArray(int n) throws Exception {
    if (isHomo) {
      if (n == 0)
        return null;
      Object v = getNext(null, 0);
      if (v instanceof Integer) {
        int[] a = new int[n];
        a[0] = ((Integer) v).intValue();
        v = a;
      } else if (v instanceof Float) {
        float[] a = new float[n];
        a[0] = ((Float) v).floatValue();
        v = a;
      } else if (v instanceof String) {
        String[] a = new String[n];
        a[0] = (String) v;
        v = a;
      } else {
        Object[] o = new Object[n];
        o[0] = v;
        for (int i = 1; i < n; i++)
          o[i] = getNext(null, 0);
        return o;
      }
      for (int i = 1; i < n; i++)
        getNext(v, i);
      return v;
    }
    Object[] o = new Object[n];
    for (int i = 0; i < n; i++)
      o[i] = getNext(null, 0);
    return o;
  }

  private Object getMap(int n) throws Exception {
    Map<String, Object> map = new Hashtable<String, Object>();
    for (int i = 0; i < n; i++) {
      String key = getNext(null, 0).toString();
      //Logger.info(key);

      Object value = getNext(null, 0);
      if (value == null) {
        //Logger.info("null value for " + key);
      } else {
        map.put(key, value);
      }
    }
    return map;
  }

  /////////////// MMTF MessagePack decoding ///////////////

  /**
   * This single method takes care of all MMTF needs.
   * 
   * See https://github.com/rcsb/mmtf/blob/master/spec.md
   * 
   * @param b
   * 
   * @return array of int, char, or float, depending upon the type
   */
  public static Object decode(byte[] b) {
    int type = BC.bytesToInt(b, 0, true);
    int n = BC.bytesToInt(b, 4, true);
    int param = BC.bytesToInt(b, 8, true);
    switch (type) {
    case 1:
      return getFloats(b, n, 1);
    case 2: // 1-byte
    case 3: // 2-byte
    case 4: // 4-byte
      return getInts(b, n);
    case 5:
      return rldecode32ToStr(b);
    case 6:
      return rldecode32ToChar(b, n);
    case 7:
      return rldecode32(b, n);
    case 8:
      return rldecode32Delta(b, n);
    case 9:
      return rldecodef(b, n, param);
    case 10:
      return unpack16Deltaf(b, n, param);
    case 11:
      return getFloats(b, n, param);
    case 12: // two-byte
    case 13: // one-byte
      return unpackf(b, 14 - type, n, param);
    case 14: // two-byte
    case 15: // one-byte
      return unpack(b, 16 - type, n);
    default:
      System.out.println("MMTF type " + type + " not found!");
      return null;
   }
  }

  /**
   * mmtf type 1 and 11
   * 
   * byte[4] to float32
   * 
   * @param b
   * @param n
   * @param divisor
   * @return array of floats
   */
  public static float[] getFloats(byte[] b, int n, float divisor) {
    if (b == null)
      return null;
    float[] a = new float[n];
    try {
      switch ((b.length - 12) / n) {  
      case 2:
        for (int i = 0, j = 12; i < n; i++, j += 2)
          a[i] = BC.bytesToShort(b, j, false) / divisor;
        break;
      case 4:
        for (int i = 0, j = 12; i < n; i++, j += 4)
          a[i] = BC.bytesToFloat(b, j, false);
        break;
      }
    } catch (Exception e) {
    }
    return a;
  }

  /**
   * mmtf types 2-4
   * 
   * Decode a byte array into a byte, short, or int array.
   * 
   * @param b
   * @param n
   *        
   * @return array of integers
   */
  public static int[] getInts(byte[] b, int n) {
    if (b == null)
      return null;
    int[] a = new int[n];
    switch ((b.length - 12) / n) {
    case 1:
      for (int i = 0, j = 12; i < n; i++, j++)
        a[i] = b[j];
      break;
    case 2:
      for (int i = 0, j = 12; i < n; i++, j += 2)
        a[i] = BC.bytesToShort(b, j, true);
      break;
    case 4:
      for (int i = 0, j = 12; i < n; i++, j += 4)
        a[i] = BC.bytesToInt(b, j, true);
      break;
    }
    return a;
  }

  /**
   * mmtf type 5
   * 
   * Decode each four bytes as a 1- to 4-character string label where a 0 byte
   * indicates end-of-string.
   * 
   * @param b
   *        a byte array
   * @return String[]
   */
  public static String[] rldecode32ToStr(byte[] b) {
    String[] id = new String[(b.length - 12) / 4];
    out: for (int i = 0, len = id.length, pt = 12; i < len; i++) {
      SB sb = new SB();
      for (int j = 0; j < 4; j++) {
        switch (b[pt]) {
        case 0:
          id[i] = sb.toString();
          pt += 4 - j;
          continue out;
        default:
          sb.appendC((char) b[pt++]);
          if (j == 3)
            id[i] = sb.toString();
          continue;
        }
      }
    }
    return id;
  }

  /**
   * mmtf type 6
   * 
   * Decode an array of int32 using run-length decoding to one char per int.
   * 
   * @param b
   * @param n
   * @return array of characters
   */
  public static char[] rldecode32ToChar(byte[] b, int n) {
    if (b == null)
      return null;
    char[] ret = new char[n];
    for (int i = 0, pt = 3; i < n;) {
      char val = (char) b[((pt++) << 2) + 3];
      for (int j = BC.bytesToInt(b, (pt++) << 2, true); --j >= 0;)
        ret[i++] = val;
    }
    return ret;
  }

  /**
   * mmtf type 7
   * 
   * Decode an array of int32 using run-length decoding.
   * 
   * @param b
   * @param n
   * @return array of integers
   */
  public static int[] rldecode32(byte[] b, int n) {
    if (b == null)
      return null;
    int[] ret = new int[n];
    for (int i = 0, pt = 3; i < n;) {
      int val = BC.bytesToInt(b, (pt++) << 2, true);
      for (int j = BC.bytesToInt(b, (pt++) << 2, true); --j >= 0;)
        ret[i++] = val;
    }
    return ret;
  }

  /**
   * mmtf type 8
   * 
   * Decode an array of int32 using run-length decoding of a difference array.
   * 
   * @param b
   * @param n
   * @return array of integers
   */
  public static int[] rldecode32Delta(byte[] b, int n) {
    if (b == null)
      return null;
    int[] ret = new int[n];
    for (int i = 0, pt = 3, val = 0; i < n;) {
      int diff = BC.bytesToInt(b, (pt++) << 2, true);
      for (int j = BC.bytesToInt(b, (pt++) << 2, true); --j >= 0;)
        ret[i++] = (val = val + diff);
    }
    return ret;
  }

  /**
   * mmtf type 9
   * 
   * Decode an array of int32 using run-length decoding and divide by a divisor
   * to give a float32.
   * 
   * @param b
   * @param n
   * @param divisor
   * @return array of floats
   */
  public static float[] rldecodef(byte[] b, int n, float divisor) {
    if (b == null)
      return null;
    float[] ret = new float[n];
    for (int i = 0, pt = 3; i < n;) {
      int val = BC.bytesToInt(b, (pt++) << 2, true);
      for (int j = BC.bytesToInt(b, (pt++) << 2, true); --j >= 0;)
        ret[i++] = val / divisor;
    }
    return ret;
  }

  /**
   * 
   * mmtf type 10
   * 
   * Decode an array of int16 using run-length decoding of a difference array.
   * 
   * @param b
   * @param n
   * @param divisor
   * @return array of floats
   */
  public static float[] unpack16Deltaf(byte[] b, int n, float divisor) {
    if (b == null)
      return null;
    float[] ret = new float[n];
    for (int i = 0, pt = 6, val = 0, buf = 0; i < n;) {
      int diff = BC.bytesToShort(b, (pt++) << 1, true);
      if (diff == Short.MAX_VALUE || diff == Short.MIN_VALUE) {
        buf += diff;
      } else {
        ret[i++] = (val = val + diff + buf) / divisor;
        buf = 0;
      }
    }
    return ret;
  }

  /**
   * 
   * mmtf type 12 and 13
   * 
   * Unpack an array of int8 or int16 to int32 and divide to give a float32.
   * 
   * untested
   * 
   * @param b
   * @param nBytes 
   * @param n
   * @param divisor 
   * @return array of floats
   */
  public static float[] unpackf(byte[] b, int nBytes, int n, float divisor) {
    if (b == null)
      return null;
    float[] ret = new float[n];
    switch (nBytes) {
    case 1:
      for (int i = 0, pt = 12, offset = 0; i < n;) {
        int val = b[pt++];
        if (val == Byte.MAX_VALUE || val == Byte.MIN_VALUE) {
          offset += val;
        } else {
          ret[i++] = (val + offset) / divisor;
          offset = 0;
        }
      }
      break;
    case 2:
      for (int i = 0, pt = 6, offset = 0; i < n;) {
        int val = BC.bytesToShort(b, (pt++) << 1, true);
        if (val == Short.MAX_VALUE || val == Short.MIN_VALUE) {
          offset += val;
        } else {
          ret[i++] = (val + offset) / divisor;
          offset = 0;
        }
      }
      break;
    }
    return ret;
  }

  /**
   * 
   * mmtf type 14 and 15
   * 
   * Unpack an array of int8 or int16 to int32.
   * 
   * untested
   * 
   * @param b
   * @param nBytes 
   * @param n
   * @return array of integers
   */
  public static int[] unpack(byte[] b, int nBytes, int n) {
    if (b == null)
      return null;
    int[] ret = new int[n];
    switch (nBytes) {
    case 1:
      for (int i = 0, pt = 12, offset = 0; i < n;) {
        int val = b[pt++];
        if (val == Byte.MAX_VALUE || val == Byte.MIN_VALUE) {
          offset += val;
        } else {
          ret[i++] = val + offset;
          offset = 0;
        }
      }
      break;
    case 2:
      for (int i = 0, pt = 6, offset = 0; i < n;) {
        int val = BC.bytesToShort(b, (pt++) << 1, true);
        if (val == Short.MAX_VALUE || val == Short.MIN_VALUE) {
          offset += val;
        } else {
          ret[i++] = val + offset;
          offset = 0;
        }
      }
      break;
    }
    return ret;
  }

  ///**
  //* Decode an array of int16 using run-length decoding
  //* of a difference array.
  //* 
  //* @param b
  //* @param n
  //* @param i0 
  //* @return array of integers
  //*/
  //public static int[] rldecode16Delta(byte[] b, int n, int i0) {
  //if (b == null)
  //return null;
  //int[] ret = new int[n];
  //for (int i = 0, pt = i0 / 2, val = 0; i < n;) {
  //int diff = BC.bytesToShort(b, (pt++) << 1, true);
  //for (int j = BC.bytesToShort(b, (pt++) << 1, true); --j >= 0;)
  //ret[i++] = (val = val + diff);
  //}
  //return ret;
  //}

  ///**
  //* Do a split delta to a float[] array
  //* @param xyz label "x", "y", "z", or "bFactor"
  //* @param factor for dividing in the end -- 1000f or 100f 
  //* @return float[]
  //* 
  //*/ 
  //public static float[] getFloatsSplit(String xyz, float factor) {
  //byte[] big = (byte[]) map.get(xyz + "Big");
  //return (big == null ? null : splitDelta(big,
  //(byte[]) map.get(xyz + "Small"), fileAtomCount, factor));
  //}

  ///**
  //* Do a split delta to a float[] array
  //* 
  //* @param big
  //*        [n m n m n m...] where n is a "big delta" and m is a number of
  //*        "small deltas
  //* @param small
  //*        array containing the small deltas
  //* @param n
  //*        the size of the final array
  //* @param factor
  //*        to divide the final result by -- 1000f or 100f here
  //* @return float[]
  //*/
  //public static float[] splitDelta(byte[] big, byte[] small, int n, float factor) {
  //float[] ret = new float[n];
  //for (int i = 0, smallpt = 0, val = 0, datapt = 0, len = big.length >> 2; i < len; i++) {
  //ret[datapt++] = (val = val + BC.bytesToInt(big, i << 2, true)) / factor;
  //if (++i < len)
  //for (int j = BC.bytesToInt(big, i << 2, true); --j >= 0; smallpt++)
  //ret[datapt++] = (val = val + BC.bytesToShort(small, smallpt << 1, true))
  //  / factor;
  //}
  //return ret;
  //}
  //


}
