package org.infinispan.marshall.core;

import java.io.IOException;
import java.io.ObjectInput;

/**
 * Array backed, expandable {@link ObjectInput} implementation.
 *
 * {@link #skip(long)} and {@link #skipBytes(int)} have been enhanced so that
 * if a negative number is passed in, they skip backwards effectively
 * providing rewind capabilities.
 */
final class BytesObjectInput implements ObjectInput {

   final byte bytes[];

   final GlobalMarshaller marshaller;

   int pos;
   int offset; // needed for external JBoss Marshalling, to be able to rewind correctly when the bytes are prepended :(

   private BytesObjectInput(byte[] bytes, int offset, GlobalMarshaller marshaller) {
      this.bytes = bytes;
      this.pos = offset;
      this.offset = offset;
      this.marshaller = marshaller;
   }

   static BytesObjectInput from(byte[] bytes, GlobalMarshaller marshaller) {
      return from(bytes, 0, marshaller);
   }

   static BytesObjectInput from(byte[] bytes, int offset, GlobalMarshaller marshaller) {
      return new BytesObjectInput(bytes, offset, marshaller);
   }

   @Override
   public Object readObject() throws ClassNotFoundException, IOException {
      return marshaller.readNullableObject(this);
   }

   @Override
   public int read() {
      return readUnsignedByte();
   }

   @Override
   public int read(byte[] b) {
      readFully(b);
      return b.length;
   }

   @Override
   public int read(byte[] b, int off, int len) {
      readFully(b, off, len);
      return len;
   }

   @Override
   public long skip(long n) {
      if (n > 0) {
         long skip = bytes.length - pos;
         if (skip > n)
            skip = n;

         pos += skip;
         return skip;
      } else {
         int idx = Math.min(bytes.length, pos);
         long skip = idx + n;
         // Calculate max to avoid skipping before offset
         pos = (int) Math.max(skip, offset);
         return skip;
      }
   }

   @Override
   public int available() {
      return bytes.length - pos;
   }

   @Override
   public void close() {
      // No-op
   }

   @Override
   public void readFully(byte[] b) {
      readFully(b, 0, b.length);
   }

   @Override
   public void readFully(byte[] b, int off, int len) {
      System.arraycopy(bytes, pos, b, off, len);
      pos += len;
   }

   @Override
   public int skipBytes(int n) {
      return (int) skip(n);
   }

   @Override
   public boolean readBoolean() {
      return readByte() != 0;
   }

   @Override
   public byte readByte() {
      return bytes[pos++];
   }

   @Override
   public int readUnsignedByte() {
      return readByte() & 0xff;
   }

   @Override
   public short readShort() {
      short v = (short) (bytes[pos] << 8 | (bytes[pos + 1] & 0xff));
      pos += 2;
      return v;
   }

   @Override
   public int readUnsignedShort() {
      int v = (bytes[pos] & 0xff) << 8 | (bytes[pos + 1] & 0xff);
      pos += 2;
      return v;
   }

   @Override
   public char readChar() {
      char v = (char) (bytes[pos] << 8 | (bytes[pos + 1] & 0xff));
      pos += 2;
      return v;
   }

   @Override
   public int readInt() {
      int v = bytes[pos] << 24
            | (bytes[pos + 1] & 0xff) << 16
            | (bytes[pos + 2] & 0xff) << 8
            | (bytes[pos + 3] & 0xff);
      pos = pos + 4;
      return v;
   }

   @Override
   public long readLong() {
      return (long) readInt() << 32L | (long) readInt() & 0xffffffffL;
   }

   @Override
   public float readFloat() {
      return Float.intBitsToFloat(readInt());
   }

   @Override
   public double readDouble() {
      return Double.longBitsToDouble(readLong());
   }

   @Override
   public String readLine() {
      return null;
   }

   @Override
   public String readUTF() {
      int utflen = readInt();

      byte[] bytearr = bytes;
      char[] chararr = new char[utflen];

      utflen = pos + utflen;
      int c, char2, char3;
      int count = pos;
      int chararr_count=0;

      while (count < utflen) {
         c = (int) bytearr[count] & 0xff;
         if (c > 127) break;
         count++;
         chararr[chararr_count++]=(char)c;
      }

      while (count < utflen) {
         c = (int) bytearr[count] & 0xff;
         switch (c >> 4) {
            case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    /* 0xxxxxxx*/
               count++;
               chararr[chararr_count++]=(char)c;
               break;
            case 12: case 13:
                    /* 110x xxxx   10xx xxxx*/
               count += 2;
               if (count > utflen)
                  throw new RuntimeException(
                        "malformed input: partial character at end");
               char2 = (int) bytearr[count-1];
               if ((char2 & 0xC0) != 0x80)
                  throw new RuntimeException(
                        "malformed input around byte " + count);
               chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
                     (char2 & 0x3F));
               break;
            case 14:
                    /* 1110 xxxx  10xx xxxx  10xx xxxx */
               count += 3;
               if (count > utflen)
                  throw new RuntimeException(
                        "malformed input: partial character at end");
               char2 = (int) bytearr[count-2];
               char3 = (int) bytearr[count-1];
               if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
                  throw new RuntimeException(
                        "malformed input around byte " + (count-1));
               chararr[chararr_count++]=(char)(((c     & 0x0F) << 12) |
                     ((char2 & 0x3F) << 6)  |
                     (char3 & 0x3F));
               break;
            default:
                    /* 10xx xxxx,  1111 xxxx */
               throw new RuntimeException(
                     "malformed input around byte " + count);
         }
      }

      pos = count;

      return new String(chararr, 0, chararr_count);
   }

   String readString() {
      byte mark = readByte();

      switch(mark) {
         case 0:
            return ""; // empty string
         case 1:
            // small ascii
            int size = readByte();
            String str = new String(bytes, 0, pos, size);
            pos += size;
            return str;
         case 2:
            // large string
            return readUTF();
         default:
            throw new RuntimeException("Unknown marker(String). mark=" + mark);
      }
   }

}
