//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.io;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jetty.util.BufferUtil;

/**
 * <p>A {@link ByteBuffer} pool.</p>
 * <p>Acquired buffers may be {@link #release(ByteBuffer) released} but they do not need to;
 * if they are released, they may be recycled and reused, otherwise they will be garbage
 * collected as usual.</p>
 */
public interface ByteBufferPool
{
    /**
     * <p>Requests a {@link ByteBuffer} of the given size.</p>
     * <p>The returned buffer may have a bigger capacity than the size being
     * requested but it will have the limit set to the given size.</p>
     *
     * @param size   the size of the buffer
     * @param direct whether the buffer must be direct or not
     * @return the requested buffer
     * @see #release(ByteBuffer)
     */
    public ByteBuffer acquire(int size, boolean direct);

    /**
     * <p>Returns a {@link ByteBuffer}, usually obtained with {@link #acquire(int, boolean)}
     * (but not necessarily), making it available for recycling and reuse.</p>
     *
     * @param buffer the buffer to return
     * @see #acquire(int, boolean)
     */
    public void release(ByteBuffer buffer);

    default ByteBuffer newByteBuffer(int capacity, boolean direct)
    {
        return direct ? BufferUtil.allocateDirect(capacity) : BufferUtil.allocate(capacity);
    }

    public static class Lease
    {
        private final ByteBufferPool byteBufferPool;
        private final List<ByteBuffer> buffers;
        private final List<Boolean> recycles;

        public Lease(ByteBufferPool byteBufferPool)
        {
            this.byteBufferPool = byteBufferPool;
            this.buffers = new ArrayList<>();
            this.recycles = new ArrayList<>();
        }

        public ByteBuffer acquire(int capacity, boolean direct)
        {
            ByteBuffer buffer = byteBufferPool.acquire(capacity, direct);
            BufferUtil.clearToFill(buffer);
            return buffer;
        }

        public void append(ByteBuffer buffer, boolean recycle)
        {
            buffers.add(buffer);
            recycles.add(recycle);
        }

        public void insert(int index, ByteBuffer buffer, boolean recycle)
        {
            buffers.add(index, buffer);
            recycles.add(index, recycle);
        }

        public List<ByteBuffer> getByteBuffers()
        {
            return buffers;
        }

        public long getTotalLength()
        {
            long length = 0;
            for (ByteBuffer buffer : buffers)
                length += buffer.remaining();
            return length;
        }

        public int getSize()
        {
            return buffers.size();
        }

        public void recycle()
        {
            for (int i = 0; i < buffers.size(); ++i)
            {
                ByteBuffer buffer = buffers.get(i);
                if (recycles.get(i))
                    byteBufferPool.release(buffer);
            }
            buffers.clear();
            recycles.clear();
        }
    }

    class Bucket
    {
        private final Deque<ByteBuffer> _queue = new ConcurrentLinkedDeque<>();
        private final ByteBufferPool _pool;
        private final int _capacity;
        private final AtomicInteger _space;

        public Bucket(ByteBufferPool pool, int bufferSize, int maxSize)
        {
            _pool = pool;
            _capacity = bufferSize;
            _space = maxSize > 0 ? new AtomicInteger(maxSize) : null;
        }

        public ByteBuffer acquire(boolean direct)
        {
            ByteBuffer buffer = queuePoll();
            if (buffer == null)
                return _pool.newByteBuffer(_capacity, direct);
            if (_space != null)
                _space.incrementAndGet();
            return buffer;
        }

        public void release(ByteBuffer buffer)
        {
            BufferUtil.clear(buffer);
            if (_space == null)
                queueOffer(buffer);
            else if (_space.decrementAndGet() >= 0)
                queueOffer(buffer);
            else
                _space.incrementAndGet();
        }

        public void clear()
        {
            if (_space == null)
            {
                queueClear();
            }
            else
            {
                int s = _space.getAndSet(0);
                while (s-- > 0)
                {
                    if (queuePoll() == null)
                        _space.incrementAndGet();
                }
            }
        }

        private void queueOffer(ByteBuffer buffer)
        {
            _queue.offerFirst(buffer);
        }

        private ByteBuffer queuePoll()
        {
            return _queue.poll();
        }

        private void queueClear()
        {
            _queue.clear();
        }

        boolean isEmpty()
        {
            return _queue.isEmpty();
        }

        int size()
        {
            return _queue.size();
        }

        @Override
        public String toString()
        {
            return String.format("Bucket@%x{%d/%d}", hashCode(), size(), _capacity);
        }
    }
}
