package org.infinispan.server.hotrod.transport;

import java.util.Optional;

import org.infinispan.commons.io.SignedNumeric;
import org.infinispan.server.core.transport.VInt;
import org.infinispan.server.core.transport.VLong;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;

public class ExtendedByteBuf {
   public static ByteBuf wrappedBuffer(byte[]... arrays) {
      return Unpooled.wrappedBuffer(arrays);
   }

   public static ByteBuf buffer(int capacity) {
      return Unpooled.buffer(capacity);
   }

   public static ByteBuf dynamicBuffer() {
      return Unpooled.buffer();
   }

   public static int readUnsignedShort(ByteBuf bf) {
      return bf.readUnsignedShort();
   }

   public static int readUnsignedInt(ByteBuf bf) {
      return VInt.read(bf);
   }

   public static long readUnsignedLong(ByteBuf bf) {
      return VLong.read(bf);
   }

   public static byte[] readRangedBytes(ByteBuf bf) {
      int length = readUnsignedInt(bf);
      return readRangedBytes(bf, length);
   }

   public static byte[] readRangedBytes(ByteBuf bf, int length) {
      if (length > 0) {
         byte[] array = new byte[length];
         bf.readBytes(array);
         return array;
      } else {
         return new byte[0];
      }
   }

   /**
    * Reads optional range of bytes. Negative lengths are translated to None, 0 length represents empty Array
    */
   public static Optional<byte[]> readOptRangedBytes(ByteBuf bf) {
      int length = SignedNumeric.decode(readUnsignedInt(bf));
      return length < 0 ? Optional.empty() : Optional.of(readRangedBytes(bf, length));
   }

   /**
    * Reads an optional String. 0 length is an empty string, negative length is translated to None.
    */
   public static Optional<String> readOptString(ByteBuf bf) {
      Optional<byte[]> bytes = readOptRangedBytes(bf);
      return bytes.map(b -> new String(b, CharsetUtil.UTF_8));
   }

   /**
    * Reads length of String and then returns an UTF-8 formatted String of such length. If the length is 0, an empty
    * String is returned.
    */
   public static String readString(ByteBuf bf) {
      byte[] bytes = readRangedBytes(bf);
      return bytes.length > 0 ? new String(bytes, CharsetUtil.UTF_8) : "";
   }

   /**
    * Reads a byte if possible.  If not present the reader index is reset to the last mark.
    *
    * @param bf
    * @return
    */
   public static Optional<Byte> readMaybeByte(ByteBuf bf) {
      if (bf.readableBytes() >= 1) {
         return Optional.of(bf.readByte());
      } else {
         bf.resetReaderIndex();
         return Optional.empty();
      }
   }

   public static Optional<Long> readMaybeLong(ByteBuf bf) {
      if (bf.readableBytes() < 8) {
         bf.resetReaderIndex();
         return Optional.empty();
      } else {
         return Optional.of(bf.readLong());
      }
   }

   /**
    * Reads a variable long if possible.  If not present the reader index is reset to the last mark.
    *
    * @param bf
    * @return
    */
   public static Optional<Long> readMaybeVLong(ByteBuf bf) {
      if (bf.readableBytes() >= 1) {
         byte b = bf.readByte();

         return read(bf, b, 7, (long) b & 0x7F, 1);
      } else {
         bf.resetReaderIndex();
         return Optional.empty();
      }
   }

   private static Optional<Long> read(ByteBuf buf, byte b, int shift, long i, int count) {
      if ((b & 0x80) == 0) return Optional.of(i);
      else {
         if (count > 9)
            throw new IllegalStateException(
                  "Stream corrupted.  A variable length long cannot be longer than 9 bytes.");

         if (buf.readableBytes() >= 1) {
            byte bb = buf.readByte();
            return read(buf, bb, shift + 7, i | (bb & 0x7FL) << shift, count + 1);
         } else {
            buf.resetReaderIndex();
            return Optional.empty();
         }
      }
   }

   /**
    * Reads a variable size int if possible.  If not present the reader index is reset to the last mark.
    *
    * @param bf
    * @return
    */
   public static Optional<Integer> readMaybeVInt(ByteBuf bf) {
      if (bf.readableBytes() >= 1) {
         byte b = bf.readByte();
         return read(bf, b, 7, b & 0x7F, 1);
      } else {
         bf.resetReaderIndex();
         return Optional.empty();
      }
   }

   private static Optional<Integer> read(ByteBuf buf, byte b, int shift, int i, int count) {
      if ((b & 0x80) == 0) return Optional.of(i);
      else {
         if (count > 5)
            throw new IllegalStateException(
                  "Stream corrupted.  A variable length integer cannot be longer than 5 bytes.");

         if (buf.readableBytes() >= 1) {
            byte bb = buf.readByte();
            return read(buf, bb, shift + 7, i | (int) ((bb & 0x7FL) << shift), count + 1);
         } else {
            buf.resetReaderIndex();
            return Optional.empty();
         }
      }
   }

   /**
    * Reads a range of bytes if possible.  If not present the reader index is reset to the last mark.
    *
    * @param bf
    * @return
    */
   public static Optional<byte[]> readMaybeRangedBytes(ByteBuf bf) {
      Optional<Integer> length = readMaybeVInt(bf);
      if (length.isPresent()) {
         int l = length.get();
         if (bf.readableBytes() >= l) {
            if (l > 0) {
               byte[] array = new byte[l];
               bf.readBytes(array);
               return Optional.of(array);
            } else {
               return Optional.of(new byte[0]);
            }
         } else {
            bf.resetReaderIndex();
            return Optional.empty();
         }
      } else return Optional.empty();
   }

   public static Optional<byte[]> readMaybeRangedBytes(ByteBuf bf, int length) {
      if (bf.readableBytes() < length) {
         bf.resetReaderIndex();
         return Optional.empty();
      } else {
         byte[] bytes = new byte[length];
         bf.readBytes(bytes);
         return Optional.of(bytes);
      }
   }

   public static Optional<Integer> readMaybeSignedInt(ByteBuf bf) {
      return readMaybeVInt(bf).map(SignedNumeric::decode);
   }

   public static Optional<Optional<byte[]>> readMaybeOptRangedBytes(ByteBuf bf) {
      Optional<Integer> l = readMaybeSignedInt(bf);
      if (l.isPresent()) {
         int length = l.get();
         if (length < 0) {
            return Optional.of(Optional.empty());
         } else {
            Optional<byte[]> rb = readMaybeRangedBytes(bf, length);
            if (rb.isPresent()) {
               return Optional.of(rb);
            } else {
               return Optional.of(Optional.empty());
            }
         }
      } else {
         return Optional.empty();
      }
   }

   /**
    * Reads a string if possible.  If not present the reader index is reset to the last mark.
    *
    * @param bf
    * @return
    */
   public static Optional<String> readMaybeString(ByteBuf bf) {
      Optional<byte[]> bytes = readMaybeRangedBytes(bf);
      return bytes.map(b -> {
         if (b.length == 0) return "";
         else return new String(b, CharsetUtil.UTF_8);
      });
   }

   public static Optional<Optional<String>> readMaybeOptString(ByteBuf bf) {
      return readMaybeOptRangedBytes(bf).map(optionalBytes -> optionalBytes.map(
            bytes -> {
               if (bytes.length == 0) return "";
               else return new String(bytes, CharsetUtil.UTF_8);
            }));
   }

   public static void writeUnsignedShort(int i, ByteBuf bf) {
      bf.writeShort(i);
   }

   public static void writeUnsignedInt(int i, ByteBuf bf) {
      VInt.write(bf, i);
   }

   public static void writeUnsignedLong(long l, ByteBuf bf) {
      VLong.write(bf, l);
   }

   public static void writeRangedBytes(byte[] src, ByteBuf bf) {
      writeUnsignedInt(src.length, bf);
      if (src.length > 0)
         bf.writeBytes(src);
   }

   public static void writeRangedBytes(byte[] src, int offset, ByteBuf bf) {
      int l = src.length - offset;
      writeUnsignedInt(l, bf);
      if (l > 0)
         bf.writeBytes(src);
   }

   public static void writeString(String msg, ByteBuf bf) {
      writeRangedBytes(msg.getBytes(CharsetUtil.UTF_8), bf);
   }

   public static void writeString(Optional<String> msg, ByteBuf bf) {
      writeRangedBytes(msg.map(m -> m.getBytes(CharsetUtil.UTF_8)).orElse(new byte[0]), bf);
   }
}
