/**
 * Copyright 2016 LinkedIn Corp. All rights reserved.
 *
 * 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.
 */
package com.github.ambry.utils;

import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * <p>
 * An implementation of <B>bounded</B> {@link ByteBufferPool}. A pool
 * is bounded by non-varying {@code capacity}, and it ensures that the
 * available memory in the pool never goes above this capacity, or goes
 * below zero. It does not actually "pool" deallocated buffers.
 */
public class SimpleByteBufferPool implements ByteBufferPool {
  private static final Logger logger = LoggerFactory.getLogger(SimpleByteBufferPool.class);
  private final long capacity;
  private final Object lock;
  private long availableMemory;

  /**
   * Create a new buffer pool
   * @param capacity the maximum amount of memory that this buffer pool can
   *                 allocate. The caller has the responsibility to assign
   *                 a reasonable value for {@code capacity}.
   */
  public SimpleByteBufferPool(long capacity) {
    this.lock = new Object();
    this.capacity = capacity;
    this.availableMemory = capacity;
  }

  /**
   * Allocate a byte buffer at the requested size
   * @param size the buffer size to allocate in bytes
   * @param timeToBlockInMs a non-negative maximum time in milliseconds to block a request
   *                        until the requested size of memory becomes available. Zero value
   *                        will make the pool to try a single time, either return a {@link
   *                        ByteBuffer} if memory is available, or a {@code TimeoutException}
   * @return A {@link ByteBuffer} at the requested size
   * @throws TimeoutException if request cannot be served within {@code timeToBlockInMs}
   * @throws InterruptedException if the current thread is interrupted while waiting
   * @throws IllegalArgumentException if {@code size} is larger than the pool capacity, or
   *                                  if {@code timeToBlockInMs} is negative.
   */
  @Override
  public ByteBuffer allocate(int size, final long timeToBlockInMs) throws TimeoutException, InterruptedException {
    if (size > capacity) {
      throw new IllegalArgumentException("Requested size cannot exceed pool capacity.");
    } else if (timeToBlockInMs < 0) {
      throw new IllegalArgumentException("timeToBlockInMs cannot be negative.");
    }
    final long startTimeInMs = System.currentTimeMillis();
    synchronized (lock) {
      while (size > availableMemory) {
        long timeout = timeToBlockInMs - (System.currentTimeMillis() - startTimeInMs);
        if (timeout <= 0) {
          throw new TimeoutException("Timed out waiting for allocation.");
        }
        lock.wait(timeout);
      }
      availableMemory -= size;
      if (availableMemory > 0) {
        lock.notify();
      }
    }
    return ByteBuffer.allocate(size);
  }

  /**
   * This method claims back the memory of {@code buffer}. It does not
   * check if the buffer was originally allocated from the pool. If a
   * deallocation will exceed the pool's capacity, the method simply sets
   * the pool's available memory to its {@code capacity}.
   * @param buffer the {@link ByteBuffer} to be deallocated back to the pool
   */
  @Override
  public void deallocate(ByteBuffer buffer) {
    synchronized (lock) {
      availableMemory += buffer.capacity();
      if (availableMemory > capacity) {
        availableMemory = capacity;
        logger.warn("The total deallocated memory is more than that had been allocated from the buffer pool.");
      }
      lock.notify();
    }
  }

  /**
   * @return the amount of memory currently available
   */
  public long availableMemory() {
    return availableMemory;
  }

  /**
   * @return the capacity of the pool
   */
  public long capacity() {
    return capacity;
  }
}

