package org.infinispan.marshall.core;

import java.io.IOException;
import java.io.ObjectOutput;

import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.commons.io.ByteBufferImpl;

/**
 * Array backed, expandable {@link ObjectOutput} implementation.
 */
final class BytesObjectOutput implements ObjectOutput {

   final GlobalMarshaller marshaller;

   byte bytes[];
   int pos;

   BytesObjectOutput(int size, GlobalMarshaller marshaller) {
      this.bytes = new byte[size];
      this.marshaller = marshaller;
   }

   @Override
   public void writeObject(Object obj) throws IOException {
      marshaller.writeNullableObject(obj, this);
   }

   @Override
   public void write(int b) {
      writeByte(b);
   }

   @Override
   public void write(byte[] b) {
      final int len = b.length;
      final int newcount = ensureCapacity(len);
      System.arraycopy(b, 0, bytes, pos, len);
      pos = newcount;
   }

   @Override
   public void write(byte[] b, int off, int len) {
      final int newcount = ensureCapacity(len);
      System.arraycopy(b, off, bytes, pos, len);
      pos = newcount;
   }

   @Override
   public void writeBoolean(boolean v) {
      writeByte((byte) (v ? 1 : 0));
   }

   @Override
   public void writeByte(int v) {
      final int newcount = ensureCapacity(1);
      bytes[pos] = (byte) v;
      pos = newcount;
   }

   @Override
   public void writeShort(int v) {
      int newcount = ensureCapacity(2);
      final int s = pos;
      bytes[s] = (byte) (v >> 8);
      bytes[s+1] = (byte) v;
      pos = newcount;
   }

   @Override
   public void writeChar(int v) {
      int newcount = ensureCapacity(2);
      final int s = pos;
      bytes[s] = (byte) (v >> 8);
      bytes[s+1] = (byte) v;
      pos = newcount;
   }

   @Override
   public void writeInt(int v) {
      int newcount = ensureCapacity(4);
      final int s = pos;
      bytes[s] = (byte) (v >> 24);
      bytes[s+1] = (byte) (v >> 16);
      bytes[s+2] = (byte) (v >> 8);
      bytes[s+3] = (byte) v;
      pos = newcount;
   }

   @Override
   public void writeLong(long v) {
      int newcount = ensureCapacity(8);
      final int s = pos;
      bytes[s] = (byte) (v >> 56L);
      bytes[s+1] = (byte) (v >> 48L);
      bytes[s+2] = (byte) (v >> 40L);
      bytes[s+3] = (byte) (v >> 32L);
      bytes[s+4] = (byte) (v >> 24L);
      bytes[s+5] = (byte) (v >> 16L);
      bytes[s+6] = (byte) (v >> 8L);
      bytes[s+7] = (byte) v;
      pos = newcount;
   }

   @Override
   public void writeFloat(float v) {
      writeInt(Float.floatToIntBits(v));
   }

   @Override
   public void writeDouble(double v) {
      writeLong(Double.doubleToLongBits(v));
   }

   @Override
   public void writeBytes(String s) {
      writeString(s);
   }

   @Override
   public void writeChars(String s) {
      writeString(s);
   }

   void writeString(String s) {
      int len;
      if ((len = s.length()) == 0){
         writeByte(0); // empty string
      } else if (isAscii(s, len)) {
         writeByte(1); // small ascii
         writeByte(len);
         int newcount = ensureCapacity(len);
         s.getBytes(0, len, bytes, pos);
         pos = newcount;
      } else {
         writeByte(2);  // large string
         writeUTF(s);
      }
   }

   private boolean isAscii(String s, int len) {
      boolean ascii = false;
      if(len < 64) {
         ascii = true;
         for (int i = 0; i < len; i++) {
            if (s.charAt(i) > 127) {
               ascii = false;
               break;
            }
         }
      }
      return ascii;
   }

   @Override
   public void writeUTF(String s) {
      int startPos = skipIntSize();
      int localPos = pos; /* avoid getfield opcode */
      byte[] localBuf = bytes; /* avoid getfield opcode */

      int strlen = s.length();
      int c = 0;

      int i=0;
      for (i=0; i<strlen; i++) {
         c = s.charAt(i);
         if (!((c >= 0x0001) && (c <= 0x007F))) break;

         if(localPos == bytes.length) {
            pos = localPos;
            ensureCapacity(1);
            localBuf = bytes;
         }
         localBuf[localPos++] = (byte) c;
      }

      for (;i < strlen; i++){
         c = s.charAt(i);
         if ((c >= 0x0001) && (c <= 0x007F)) {
            if(localPos == bytes.length) {
               pos = localPos;
               ensureCapacity(1);
               localBuf = bytes;
            }
            localBuf[localPos++] = (byte) c;

         } else if (c > 0x07FF) {
            if(localPos+3 >= bytes.length) {
               pos = localPos;
               ensureCapacity(3);
               localBuf = bytes;
            }

            localBuf[localPos++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
            localBuf[localPos++] = (byte) (0x80 | ((c >>  6) & 0x3F));
            localBuf[localPos++] = (byte) (0x80 | (c & 0x3F));
         } else {
            if(localPos + 2 >= bytes.length) {
               pos = localPos;
               ensureCapacity(2);
               localBuf = bytes;
            }

            localBuf[localPos++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
            localBuf[localPos++] = (byte) (0x80 | (c & 0x3F));
         }
      }
      pos = localPos;
      writeIntDirect(localPos - 4 - startPos, startPos);
   }

   private int skipIntSize() {
      ensureCapacity(4);
      int count = pos;
      pos +=4;
      return count;
   }

   private void writeIntDirect(int intValue, int index) {
      byte[] buf = bytes; /* avoid getfield opcode */
      buf[index] =   (byte) ((intValue >>> 24) & 0xFF);
      buf[index+1] = (byte) ((intValue >>> 16) & 0xFF);
      buf[index+2] = (byte) ((intValue >>>  8) & 0xFF);
      buf[index+3] = (byte) (intValue & 0xFF);
   }

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

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

   private int ensureCapacity(int len) {
      int newcount = pos + len;
      if (newcount > bytes.length) {
         byte newbuf[] = new byte[getNewBufferSize(bytes.length, newcount)];
         System.arraycopy(bytes, 0, newbuf, 0, pos);
         bytes = newbuf;
      }
      return newcount;
   }

   private static final int DEFAULT_DOUBLING_SIZE = 4 * 1024 * 1024; // 4MB

   /**
    * Gets the number of bytes to which the internal buffer should be resized.
    * If not enough space, it doubles the size until the internal buffer
    * reaches a configurable max size (default is 4MB), after which it begins
    * growing the buffer in 25% increments.  This is intended to help prevent
    * an OutOfMemoryError during a resize of a large buffer.
    *
    * @param curSize    the current number of bytes
    * @param minNewSize the minimum number of bytes required
    * @return the size to which the internal buffer should be resized
    */
   private int getNewBufferSize(int curSize, int minNewSize) {
      if (curSize <= DEFAULT_DOUBLING_SIZE)
         return Math.max(curSize << 1, minNewSize);
      else
         return Math.max(curSize + (curSize >> 2), minNewSize);
   }

   byte[] toBytes() {
      // Trim out unused bytes
      byte[] b = new byte[pos];
      System.arraycopy(bytes, 0, b, 0, pos);
      pos = 0;
      return b;
   }

   ByteBuffer toByteBuffer() {
      // No triming, just take position as length
      return new ByteBufferImpl(bytes, 0, pos);
   }

}
