/*-
 * #%L
 * LmdbJava
 * %%
 * Copyright (C) 2016 - 2019 The LmdbJava Open Source Project
 * %%
 * Licensed 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.
 * #L%
 */

package org.lmdbjava;

import io.netty.buffer.ByteBuf;
import static io.netty.buffer.PooledByteBufAllocator.DEFAULT;
import static java.lang.Class.forName;
import static java.lang.ThreadLocal.withInitial;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import jnr.ffi.Pointer;
import static org.lmdbjava.UnsafeAccess.UNSAFE;

/**
 * A buffer proxy backed by Netty's {@link ByteBuf}.
 *
 * <p>
 * This class requires {@link UnsafeAccess} and netty-buffer must be in the
 * classpath.
 */
public final class ByteBufProxy extends BufferProxy<ByteBuf> {

  private static final long ADDRESS_OFFSET;

  /**
   * A thread-safe pool for a given length. If the buffer found is bigger then
   * the buffer in the pool creates a new buffer. If no buffer is found creates
   * a new buffer.
   */
  private static final ThreadLocal<ArrayDeque<ByteBuf>> BUFFERS = withInitial(()
      -> new ArrayDeque<>(16));

  private static final String FIELD_NAME_ADDRESS = "memoryAddress";
  private static final String FIELD_NAME_LENGTH = "length";
  private static final long LENGTH_OFFSET;
  private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf";

  static {
    try {
      // create buffer (first is SimpleLeakAwareByteBuff but we need PooledUDBB)
      final ByteBuf bb = DEFAULT.directBuffer(0);
      if (!NAME.equals(bb.getClass().getName())) {
        throw new IllegalStateException("Netty buffer must be " + NAME);
      }
      final Field address = findField(NAME, FIELD_NAME_ADDRESS);
      final Field length = findField(NAME, FIELD_NAME_LENGTH);
      ADDRESS_OFFSET = UNSAFE.objectFieldOffset(address);
      LENGTH_OFFSET = UNSAFE.objectFieldOffset(length);
    } catch (final SecurityException e) {
      throw new LmdbException("Field access error", e);
    }
  }

  static Field findField(final String c, final String name) {
    Class<?> clazz;
    try {
      clazz = forName(c);
    } catch (final ClassNotFoundException e) {
      throw new LmdbException(c + " class unavailable", e);
    }
    do {
      try {
        final Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field;
      } catch (final NoSuchFieldException e) {
        clazz = clazz.getSuperclass();
      }
    } while (clazz != null);
    throw new LmdbException(name + " not found");
  }

  @Override
  protected ByteBuf allocate() {
    final ArrayDeque<ByteBuf> queue = BUFFERS.get();
    final ByteBuf buffer = queue.poll();

    if (buffer != null && buffer.capacity() >= 0) {
      return buffer;
    } else {
      return DEFAULT.directBuffer(0);
    }
  }

  @Override
  protected int compare(final ByteBuf o1, final ByteBuf o2) {
    return o1.compareTo(o2);
  }

  @Override
  protected void deallocate(final ByteBuf buff) {
    final ArrayDeque<ByteBuf> queue = BUFFERS.get();
    if (!queue.offer(buff)) {
      buff.release();
    }
  }

  @Override
  protected byte[] getBytes(final ByteBuf buffer) {
    final byte[] dest = new byte[buffer.capacity()];
    buffer.getBytes(0, dest);
    return dest;
  }

  @Override
  protected void in(final ByteBuf buffer, final Pointer ptr, final long ptrAddr) {
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE,
                   buffer.writerIndex() - buffer.readerIndex());
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA,
                   buffer.memoryAddress() + buffer.readerIndex());
  }

  @Override
  protected void in(final ByteBuf buffer, final int size, final Pointer ptr,
                    final long ptrAddr) {
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE,
                   size);
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA,
                   buffer.memoryAddress() + buffer.readerIndex());
  }

  @Override
  protected ByteBuf out(final ByteBuf buffer, final Pointer ptr,
                        final long ptrAddr) {
    final long addr = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA);
    final long size = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE);
    UNSAFE.putLong(buffer, ADDRESS_OFFSET, addr);
    UNSAFE.putInt(buffer, LENGTH_OFFSET, (int) size);
    buffer.writerIndex((int) size).readerIndex(0);
    return buffer;
  }
}

@@ -1573,16 +1573,64 @@
 (16));%0A%0A
+  private static final int BUFFER_RETRIES = 10;%0A
   privat
@@ -1905,266 +1905,25 @@
     
- //
  create
- buffer (first is SimpleLeakAwareByteBuff but we need PooledUDBB)%0A      final ByteBuf bb = DEFAULT.direct
 Buffer(
-0
 );%0A
-      if (!NAME.equals(bb.getClass().getName())) %7B%0A        throw new IllegalStateException(%22Netty buffer must be %22 + NAME);%0A      %7D%0A
     
@@ -2806,32 +2806,328 @@
 t found%22);%0A  %7D%0A%0A
+  private static ByteBuf createBuffer() %7B%0A    for (int i = 0; i %3C BUFFER_RETRIES; i++) %7B%0A      final ByteBuf bb = DEFAULT.directBuffer(0);%0A      if (NAME.equals(bb.getClass().getName())) %7B%0A        return bb;%0A      %7D%0A    %7D%0A    throw new IllegalStateException(%22Netty buffer must be %22 + NAME);%0A  %7D%0A%0A
   @Override%0A  pr
@@ -3114,32 +3114,32 @@
  %7D%0A%0A  @Override%0A
-
   protected Byte
@@ -3349,38 +3349,29 @@
  return 
-DEFAULT.direct
+create
 Buffer(
-0
 );%0A    %7D

Fix IndexOutOfBoundsException due to SimpleLeakAwareByteBuf (fixes #133)