/**
 * 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.hadoop.hbase.nio;

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

import org.apache.hadoop.hbase.util.ByteBufferUtils;
import org.apache.hadoop.hbase.util.ObjectIntPair;
import org.apache.hadoop.hbase.util.UnsafeAccess;
import org.apache.hadoop.hbase.util.UnsafeAvailChecker;
import org.apache.yetus.audience.InterfaceAudience;
import sun.nio.ch.DirectBuffer;

import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;

/**
 * An implementation of ByteBuff where a single BB backs the BBI. This just acts
 * as a wrapper over a normal BB - offheap or onheap
 */
@InterfaceAudience.Private
public class SingleByteBuff extends ByteBuff {

  private static final boolean UNSAFE_AVAIL = UnsafeAvailChecker.isAvailable();
  private static final boolean UNSAFE_UNALIGNED = UnsafeAvailChecker.unaligned();

  // Underlying BB
  private final ByteBuffer buf;

  // To access primitive values from underlying ByteBuffer using Unsafe
  private long unsafeOffset;
  private Object unsafeRef = null;

  public SingleByteBuff(ByteBuffer buf) {
    this.buf = buf;
    if (buf.hasArray()) {
      this.unsafeOffset = UnsafeAccess.BYTE_ARRAY_BASE_OFFSET + buf.arrayOffset();
      this.unsafeRef = buf.array();
    } else {
      this.unsafeOffset = ((DirectBuffer) buf).address();
    }
  }

  @Override
  public int position() {
    return this.buf.position();
  }

  @Override
  public SingleByteBuff position(int position) {
    this.buf.position(position);
    return this;
  }

  @Override
  public SingleByteBuff skip(int len) {
    this.buf.position(this.buf.position() + len);
    return this;
  }

  @Override
  public SingleByteBuff moveBack(int len) {
    this.buf.position(this.buf.position() - len);
    return this;
  }

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

  @Override
  public int limit() {
    return this.buf.limit();
  }

  @Override
  public SingleByteBuff limit(int limit) {
    this.buf.limit(limit);
    return this;
  }

  @Override
  public SingleByteBuff rewind() {
    this.buf.rewind();
    return this;
  }

  @Override
  public SingleByteBuff mark() {
    this.buf.mark();
    return this;
  }

  @Override
  public ByteBuffer asSubByteBuffer(int length) {
    // Just return the single BB that is available
    return this.buf;
  }

  @Override
  public void asSubByteBuffer(int offset, int length, ObjectIntPair<ByteBuffer> pair) {
    // Just return the single BB that is available
    pair.setFirst(this.buf);
    pair.setSecond(offset);
  }

  @Override
  public int remaining() {
    return this.buf.remaining();
  }

  @Override
  public boolean hasRemaining() {
    return buf.hasRemaining();
  }

  @Override
  public SingleByteBuff reset() {
    this.buf.reset();
    return this;
  }

  @Override
  public SingleByteBuff slice() {
    return new SingleByteBuff(this.buf.slice());
  }

  @Override
  public SingleByteBuff duplicate() {
    return new SingleByteBuff(this.buf.duplicate());
  }

  @Override
  public byte get() {
    return buf.get();
  }

  @Override
  public byte get(int index) {
    if (UNSAFE_AVAIL) {
      return UnsafeAccess.toByte(this.unsafeRef, this.unsafeOffset + index);
    }
    return this.buf.get(index);
  }

  @Override
  public byte getByteAfterPosition(int offset) {
    return get(this.buf.position() + offset);
  }

  @Override
  public SingleByteBuff put(byte b) {
    this.buf.put(b);
    return this;
  }

  @Override
  public SingleByteBuff put(int index, byte b) {
    buf.put(index, b);
    return this;
  }

  @Override
  public void get(byte[] dst, int offset, int length) {
    ByteBufferUtils.copyFromBufferToArray(dst, buf, buf.position(), offset, length);
    buf.position(buf.position() + length);
  }

  @Override
  public void get(int sourceOffset, byte[] dst, int offset, int length) {
    ByteBufferUtils.copyFromBufferToArray(dst, buf, sourceOffset, offset, length);
  }

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

  @Override
  public SingleByteBuff put(int offset, ByteBuff src, int srcOffset, int length) {
    if (src instanceof SingleByteBuff) {
      ByteBufferUtils.copyFromBufferToBuffer(((SingleByteBuff) src).buf, this.buf, srcOffset,
          offset, length);
    } else {
      // TODO we can do some optimization here? Call to asSubByteBuffer might
      // create a copy.
      ObjectIntPair<ByteBuffer> pair = new ObjectIntPair<>();
      src.asSubByteBuffer(srcOffset, length, pair);
      if (pair.getFirst() != null) {
        ByteBufferUtils.copyFromBufferToBuffer(pair.getFirst(), this.buf, pair.getSecond(), offset,
            length);
      }
    }
    return this;
  }

  @Override
  public SingleByteBuff put(byte[] src, int offset, int length) {
    ByteBufferUtils.copyFromArrayToBuffer(this.buf, src, offset, length);
    return this;
  }

  @Override
  public SingleByteBuff put(byte[] src) {
    return put(src, 0, src.length);
  }

  @Override
  public boolean hasArray() {
    return this.buf.hasArray();
  }

  @Override
  public byte[] array() {
    return this.buf.array();
  }

  @Override
  public int arrayOffset() {
    return this.buf.arrayOffset();
  }

  @Override
  public short getShort() {
    return this.buf.getShort();
  }

  @Override
  public short getShort(int index) {
    if (UNSAFE_UNALIGNED) {
      return UnsafeAccess.toShort(unsafeRef, unsafeOffset + index);
    }
    return this.buf.getShort(index);
  }

  @Override
  public short getShortAfterPosition(int offset) {
    return getShort(this.buf.position() + offset);
  }

  @Override
  public int getInt() {
    return this.buf.getInt();
  }

  @Override
  public SingleByteBuff putInt(int value) {
    ByteBufferUtils.putInt(this.buf, value);
    return this;
  }

  @Override
  public int getInt(int index) {
    if (UNSAFE_UNALIGNED) {
      return UnsafeAccess.toInt(unsafeRef, unsafeOffset + index);
    }
    return this.buf.getInt(index);
  }

  @Override
  public int getIntAfterPosition(int offset) {
    return getInt(this.buf.position() + offset);
  }

  @Override
  public long getLong() {
    return this.buf.getLong();
  }

  @Override
  public SingleByteBuff putLong(long value) {
    ByteBufferUtils.putLong(this.buf, value);
    return this;
  }

  @Override
  public long getLong(int index) {
    if (UNSAFE_UNALIGNED) {
      return UnsafeAccess.toLong(unsafeRef, unsafeOffset + index);
    }
    return this.buf.getLong(index);
  }

  @Override
  public long getLongAfterPosition(int offset) {
    return getLong(this.buf.position() + offset);
  }

  @Override
  public byte[] toBytes(int offset, int length) {
    byte[] output = new byte[length];
    ByteBufferUtils.copyFromBufferToArray(output, buf, offset, 0, length);
    return output;
  }

  @Override
  public void get(ByteBuffer out, int sourceOffset, int length) {
    ByteBufferUtils.copyFromBufferToBuffer(buf, out, sourceOffset, length);
  }

  @Override
  public int read(ReadableByteChannel channel) throws IOException {
    return channelRead(channel, buf);
  }

  @Override
  public boolean equals(Object obj) {
    if(!(obj instanceof SingleByteBuff)) return false;
    return this.buf.equals(((SingleByteBuff)obj).buf);
  }

  @Override
  public int hashCode() {
    return this.buf.hashCode();
  }

  /**
   * @return the ByteBuffer which this wraps.
   */
  @VisibleForTesting
  public ByteBuffer getEnclosingByteBuffer() {
    return this.buf;
  }
}
