/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.artemis.core.buffers.impl;

import java.io.IOException;
import java.nio.ByteBuffer;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.utils.ByteUtil;
import org.apache.activemq.artemis.utils.DataConstants;
import org.apache.activemq.artemis.utils.UTF8Util;

public class ChannelBufferWrapper implements ActiveMQBuffer {

   protected ByteBuf buffer; // NO_UCD (use final)
   private final boolean releasable;
   private final boolean isPooled;
   public static ByteBuf unwrap(ByteBuf buffer) {
      ByteBuf parent;
      while ((parent = buffer.unwrap()) != null && parent != buffer) { // this last part is just in case the semantic
         // ever changes where unwrap is returning itself
         buffer = parent;
      }

      return buffer;
   }

   public ChannelBufferWrapper(final ByteBuf buffer) {
      this(buffer, false);
   }
   public ChannelBufferWrapper(final ByteBuf buffer, boolean releasable) {
      this(buffer, releasable, false);
   }
   public ChannelBufferWrapper(final ByteBuf buffer, boolean releasable, boolean pooled) {
      if (!releasable) {
         this.buffer = Unpooled.unreleasableBuffer(buffer);
      } else {
         this.buffer = buffer;
      }
      this.releasable = releasable;
      this.isPooled = pooled;

   }

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

   @Override
   public SimpleString readNullableSimpleString() {
      int b = buffer.readByte();
      if (b == DataConstants.NULL) {
         return null;
      }
      return readSimpleStringInternal();
   }

   @Override
   public String readNullableString() {
      int b = buffer.readByte();
      if (b == DataConstants.NULL) {
         return null;
      }
      return readStringInternal();
   }

   @Override
   public SimpleString readSimpleString() {
      return readSimpleStringInternal();
   }

   private SimpleString readSimpleStringInternal() {
      int len = buffer.readInt();
      byte[] data = new byte[len];
      buffer.readBytes(data);
      return new SimpleString(data);
   }

   @Override
   public String readString() {
      return readStringInternal();
   }

   private String readStringInternal() {
      int len = buffer.readInt();

      if (len < 9) {
         char[] chars = new char[len];
         for (int i = 0; i < len; i++) {
            chars[i] = (char) buffer.readShort();
         }
         return new String(chars);
      } else if (len < 0xfff) {
         return readUTF();
      } else {
         return readSimpleStringInternal().toString();
      }
   }

   @Override
   public String readUTF() {
      return UTF8Util.readUTF(this);
   }

   @Override
   public void writeBoolean(final boolean val) {
      buffer.writeByte((byte) (val ? -1 : 0));
   }

   @Override
   public void writeNullableSimpleString(final SimpleString val) {
      if (val == null) {
         buffer.writeByte(DataConstants.NULL);
      } else {
         buffer.writeByte(DataConstants.NOT_NULL);
         writeSimpleStringInternal(val);
      }
   }

   @Override
   public void writeNullableString(final String val) {
      if (val == null) {
         buffer.writeByte(DataConstants.NULL);
      } else {
         buffer.writeByte(DataConstants.NOT_NULL);
         writeStringInternal(val);
      }
   }

   @Override
   public void writeSimpleString(final SimpleString val) {
      writeSimpleStringInternal(val);
   }

   private void writeSimpleStringInternal(final SimpleString val) {
      byte[] data = val.getData();
      buffer.writeInt(data.length);
      buffer.writeBytes(data);
   }

   @Override
   public void writeString(final String val) {
      writeStringInternal(val);
   }

   private void writeStringInternal(final String val) {
      int length = val.length();

      buffer.writeInt(length);

      if (length < 9) {
         // If very small it's more performant to store char by char
         for (int i = 0; i < val.length(); i++) {
            buffer.writeShort((short) val.charAt(i));
         }
      } else if (length < 0xfff) {
         // Store as UTF - this is quicker than char by char for most strings
         writeUTF(val);
      } else {
         // Store as SimpleString, since can't store utf > 0xffff in length
         writeSimpleStringInternal(new SimpleString(val));
      }
   }

   @Override
   public void writeUTF(final String utf) {
      UTF8Util.saveUTF(this, utf);
   }

   @Override
   public int capacity() {
      return buffer.capacity();
   }

   @Override
   public ByteBuf byteBuf() {
      return buffer;
   }

   @Override
   public void clear() {
      buffer.clear();
   }

   @Override
   public ActiveMQBuffer copy() {
      return new ChannelBufferWrapper(buffer.copy(), releasable);
   }

   @Override
   public ActiveMQBuffer copy(final int index, final int length) {
      return new ChannelBufferWrapper(buffer.copy(index, length), releasable);
   }

   @Override
   public void discardReadBytes() {
      buffer.discardReadBytes();
   }

   @Override
   public ActiveMQBuffer duplicate() {
      return new ChannelBufferWrapper(buffer.duplicate(), releasable);
   }

   @Override
   public byte getByte(final int index) {
      return buffer.getByte(index);
   }

   @Override
   public void getBytes(final int index, final byte[] dst, final int dstIndex, final int length) {
      buffer.getBytes(index, dst, dstIndex, length);
   }

   @Override
   public void getBytes(final int index, final byte[] dst) {
      buffer.getBytes(index, dst);
   }

   @Override
   public void getBytes(final int index, final ByteBuffer dst) {
      buffer.getBytes(index, dst);
   }

   @Override
   public void getBytes(final int index, final ActiveMQBuffer dst, final int dstIndex, final int length) {
      buffer.getBytes(index, dst.byteBuf(), dstIndex, length);
   }

   @Override
   public void getBytes(final int index, final ActiveMQBuffer dst, final int length) {
      buffer.getBytes(index, dst.byteBuf(), length);
   }

   @Override
   public void getBytes(final int index, final ActiveMQBuffer dst) {
      buffer.getBytes(index, dst.byteBuf());
   }

   @Override
   public char getChar(final int index) {
      return (char) buffer.getShort(index);
   }

   @Override
   public double getDouble(final int index) {
      return Double.longBitsToDouble(buffer.getLong(index));
   }

   @Override
   public float getFloat(final int index) {
      return Float.intBitsToFloat(buffer.getInt(index));
   }

   @Override
   public int getInt(final int index) {
      return buffer.getInt(index);
   }

   @Override
   public long getLong(final int index) {
      return buffer.getLong(index);
   }

   @Override
   public short getShort(final int index) {
      return buffer.getShort(index);
   }

   @Override
   public short getUnsignedByte(final int index) {
      return buffer.getUnsignedByte(index);
   }

   @Override
   public long getUnsignedInt(final int index) {
      return buffer.getUnsignedInt(index);
   }

   @Override
   public int getUnsignedShort(final int index) {
      return buffer.getUnsignedShort(index);
   }

   @Override
   public void markReaderIndex() {
      buffer.markReaderIndex();
   }

   @Override
   public void markWriterIndex() {
      buffer.markWriterIndex();
   }

   @Override
   public boolean readable() {
      return buffer.isReadable();
   }

   @Override
   public int readableBytes() {
      return buffer.readableBytes();
   }

   @Override
   public byte readByte() {
      return buffer.readByte();
   }

   @Override
   public void readBytes(final byte[] dst, final int dstIndex, final int length) {
      buffer.readBytes(dst, dstIndex, length);
   }

   @Override
   public void readBytes(final byte[] dst) {
      buffer.readBytes(dst);
   }

   @Override
   public void readBytes(final ByteBuffer dst) {
      buffer.readBytes(dst);
   }

   @Override
   public void readBytes(final ActiveMQBuffer dst, final int dstIndex, final int length) {
      buffer.readBytes(dst.byteBuf(), dstIndex, length);
   }

   @Override
   public void readBytes(final ActiveMQBuffer dst, final int length) {
      buffer.readBytes(dst.byteBuf(), length);
   }

   @Override
   public void readBytes(final ActiveMQBuffer dst) {
      buffer.readBytes(dst.byteBuf());
   }

   @Override
   public ActiveMQBuffer readBytes(final int length) {
      return new ChannelBufferWrapper(buffer.readBytes(length), releasable);
   }

   @Override
   public char readChar() {
      return (char) buffer.readShort();
   }

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

   @Override
   public int readerIndex() {
      return buffer.readerIndex();
   }

   @Override
   public void readerIndex(final int readerIndex) {
      buffer.readerIndex(readerIndex);
   }

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

   @Override
   public int readInt() {
      return buffer.readInt();
   }

   @Override
   public long readLong() {
      return buffer.readLong();
   }

   @Override
   public short readShort() {
      return buffer.readShort();
   }

   @Override
   public ActiveMQBuffer readSlice(final int length) {
      if ( isPooled ) {
         ByteBuf fromBuffer = buffer.readSlice(length);
         ByteBuf newNettyBuffer = Unpooled.buffer(fromBuffer.capacity());
         int read = fromBuffer.readerIndex();
         int writ = fromBuffer.writerIndex();
         fromBuffer.readerIndex(0);
         fromBuffer.readBytes(newNettyBuffer,0,writ);
         newNettyBuffer.setIndex(read,writ);
         ActiveMQBuffer returnBuffer = new ChannelBufferWrapper(newNettyBuffer,releasable,false);
         returnBuffer.setIndex(read,writ);
         return returnBuffer;
      }
      return new ChannelBufferWrapper(buffer.readSlice(length), releasable, isPooled);
   }

   @Override
   public int readUnsignedByte() {
      return buffer.readUnsignedByte();
   }

   @Override
   public long readUnsignedInt() {
      return buffer.readUnsignedInt();
   }

   @Override
   public int readUnsignedShort() {
      return buffer.readUnsignedShort();
   }

   @Override
   public void resetReaderIndex() {
      buffer.resetReaderIndex();
   }

   @Override
   public void resetWriterIndex() {
      buffer.resetWriterIndex();
   }

   @Override
   public void setByte(final int index, final byte value) {
      buffer.setByte(index, value);
   }

   @Override
   public void setBytes(final int index, final byte[] src, final int srcIndex, final int length) {
      buffer.setBytes(index, src, srcIndex, length);
   }

   @Override
   public void setBytes(final int index, final byte[] src) {
      buffer.setBytes(index, src);
   }

   @Override
   public void setBytes(final int index, final ByteBuffer src) {
      buffer.setBytes(index, src);
   }

   @Override
   public void setBytes(final int index, final ActiveMQBuffer src, final int srcIndex, final int length) {
      buffer.setBytes(index, src.byteBuf(), srcIndex, length);
   }

   @Override
   public void setBytes(final int index, final ActiveMQBuffer src, final int length) {
      buffer.setBytes(index, src.byteBuf(), length);
   }

   @Override
   public void setBytes(final int index, final ActiveMQBuffer src) {
      buffer.setBytes(index, src.byteBuf());
   }

   @Override
   public void setChar(final int index, final char value) {
      buffer.setShort(index, (short) value);
   }

   @Override
   public void setDouble(final int index, final double value) {
      buffer.setLong(index, Double.doubleToLongBits(value));
   }

   @Override
   public void setFloat(final int index, final float value) {
      buffer.setInt(index, Float.floatToIntBits(value));
   }

   @Override
   public void setIndex(final int readerIndex, final int writerIndex) {
      buffer.setIndex(readerIndex, writerIndex);
   }

   @Override
   public void setInt(final int index, final int value) {
      buffer.setInt(index, value);
   }

   @Override
   public void setLong(final int index, final long value) {
      buffer.setLong(index, value);
   }

   @Override
   public void setShort(final int index, final short value) {
      buffer.setShort(index, value);
   }

   @Override
   public int skipBytes(final int length) {
      buffer.skipBytes(length);
      return length;
   }

   @Override
   public ActiveMQBuffer slice() {
      return new ChannelBufferWrapper(buffer.slice(), releasable);
   }

   @Override
   public ActiveMQBuffer slice(final int index, final int length) {
      return new ChannelBufferWrapper(buffer.slice(index, length), releasable);
   }

   @Override
   public ByteBuffer toByteBuffer() {
      return buffer.nioBuffer();
   }

   @Override
   public ByteBuffer toByteBuffer(final int index, final int length) {
      return buffer.nioBuffer(index, length);
   }

   @Override
   public void release() {
      if ( this.isPooled ) {
         buffer.release();
      }
   }

   @Override
   public boolean writable() {
      return buffer.isWritable();
   }

   @Override
   public int writableBytes() {
      return buffer.writableBytes();
   }

   @Override
   public void writeByte(final byte value) {
      buffer.writeByte(value);
   }

   @Override
   public void writeBytes(final byte[] src, final int srcIndex, final int length) {
      buffer.writeBytes(src, srcIndex, length);
   }

   @Override
   public void writeBytes(final byte[] src) {
      buffer.writeBytes(src);
   }

   @Override
   public void writeBytes(final ByteBuffer src) {
      buffer.writeBytes(src);
   }

   @Override
   public void writeBytes(ByteBuf src, int srcIndex, int length) {
      buffer.writeBytes(src, srcIndex, length);
   }

   @Override
   public void writeBytes(final ActiveMQBuffer src, final int srcIndex, final int length) {
      buffer.writeBytes(src.byteBuf(), srcIndex, length);
   }

   @Override
   public void writeBytes(final ActiveMQBuffer src, final int length) {
      buffer.writeBytes(src.byteBuf(), length);
   }

   @Override
   public void writeChar(final char chr) {
      buffer.writeShort((short) chr);
   }

   @Override
   public void writeDouble(final double value) {
      buffer.writeLong(Double.doubleToLongBits(value));
   }

   @Override
   public void writeFloat(final float value) {
      buffer.writeInt(Float.floatToIntBits(value));
   }

   @Override
   public void writeInt(final int value) {
      buffer.writeInt(value);
   }

   @Override
   public void writeLong(final long value) {
      buffer.writeLong(value);
   }

   @Override
   public int writerIndex() {
      return buffer.writerIndex();
   }

   @Override
   public void writerIndex(final int writerIndex) {
      buffer.writerIndex(writerIndex);
   }

   @Override
   public void writeShort(final short value) {
      buffer.writeShort(value);
   }

   /**
    * from {@link java.io.DataInput} interface
    */
   @Override
   public void readFully(byte[] b) throws IOException {
      readBytes(b);
   }

   /**
    * from {@link java.io.DataInput} interface
    */
   @Override
   public void readFully(byte[] b, int off, int len) throws IOException {
      readBytes(b, off, len);
   }

   /**
    * from {@link java.io.DataInput} interface
    */
   @Override
   public String readLine() throws IOException {
      return ByteUtil.readLine(this);
   }

}
