/*-
 * #%L
 * LmdbJava
 * %%
 * Copyright (C) 2016 - 2020 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 static java.lang.ThreadLocal.withInitial;
import static java.nio.ByteBuffer.allocateDirect;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.util.Objects.requireNonNull;
import static org.lmdbjava.UnsafeAccess.UNSAFE;

import java.nio.ByteBuffer;

import jnr.ffi.Pointer;
import org.agrona.DirectBuffer;
import org.agrona.MutableDirectBuffer;
import org.agrona.concurrent.OneToOneConcurrentArrayQueue;
import org.agrona.concurrent.UnsafeBuffer;

/**
 * A buffer proxy backed by Agrona's {@link DirectBuffer}.
 *
 * <p>
 * This class requires {@link UnsafeAccess} and Agrona must be in the classpath.
 */
public final class DirectBufferProxy extends BufferProxy<DirectBuffer> {

  /**
   * The {@link MutableDirectBuffer} proxy. Guaranteed to never be null,
   * although a class initialization exception will occur if an attempt is made
   * to access this field when unsafe or Agrona is unavailable.
   */
  public static final BufferProxy<DirectBuffer> PROXY_DB
      = new DirectBufferProxy();

  /**
   * A thread-safe pool for a given length. If the buffer found is valid (ie not
   * of a negative length) then that buffer is used. If no valid buffer is
   * found, a new buffer is created.
   */
  private static final ThreadLocal<OneToOneConcurrentArrayQueue<DirectBuffer>> BUFFERS
      = withInitial(() -> new OneToOneConcurrentArrayQueue<>(16));

  private DirectBufferProxy() {
  }

  /**
   * Lexicographically compare two buffers.
   *
   * @param o1 left operand (required)
   * @param o2 right operand (required)
   * @return as specified by {@link Comparable} interface
   */
  public static int compareBuff(final DirectBuffer o1, final DirectBuffer o2) {
    requireNonNull(o1);
    requireNonNull(o2);
    if (o1.equals(o2)) {
      return 0;
    }
    final int minLength = Math.min(o1.capacity(), o2.capacity());
    final int minWords = minLength / Long.BYTES;

    for (int i = 0; i < minWords * Long.BYTES; i += Long.BYTES) {
      final long lw = o1.getLong(i, BIG_ENDIAN);
      final long rw = o2.getLong(i, BIG_ENDIAN);
      final int diff = Long.compareUnsigned(lw, rw);
      if (diff != 0) {
        return diff;
      }
    }

    for (int i = minWords * Long.BYTES; i < minLength; i++) {
      final int lw = Byte.toUnsignedInt(o1.getByte(i));
      final int rw = Byte.toUnsignedInt(o2.getByte(i));
      final int result = Integer.compareUnsigned(lw, rw);
      if (result != 0) {
        return result;
      }
    }

    return o1.capacity() - o2.capacity();
  }

  @Override
  protected DirectBuffer allocate() {
    final OneToOneConcurrentArrayQueue<DirectBuffer> q = BUFFERS.get();
    final DirectBuffer buffer = q.poll();

    if (buffer != null && buffer.capacity() >= 0) {
      return buffer;
    } else {
      final ByteBuffer bb = allocateDirect(0);
      return new UnsafeBuffer(bb);
    }
  }

  @Override
  protected int compare(final DirectBuffer o1, final DirectBuffer o2) {
    return compareBuff(o1, o2);
  }

  @Override
  protected void deallocate(final DirectBuffer buff) {
    final OneToOneConcurrentArrayQueue<DirectBuffer> q = BUFFERS.get();
    q.offer(buff);
  }

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

  @Override
  protected void in(final DirectBuffer buffer, final Pointer ptr,
                    final long ptrAddr) {
    final long addr = buffer.addressOffset();
    final long size = buffer.capacity();
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, addr);
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size);
  }

  @Override
  protected void in(final DirectBuffer buffer, final int size, final Pointer ptr,
                    final long ptrAddr) {
    final long addr = buffer.addressOffset();
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, addr);
    UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size);
  }

  @Override
  protected DirectBuffer out(final DirectBuffer 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);
    buffer.wrap(addr, (int) size);
    return buffer;
  }

}