package org.jgroups.util;

import java.lang.reflect.Array;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;


/**
 * Ring buffer of fixed capacity designed for multiple writers but only a single reader. Advancing the read or
 * write index blocks until it is possible to do so.
 * @author Bela Ban
 * @since  4.0
 */
public class RingBuffer<T> {
    protected final T[]          buf;
    protected int                ri, wi;   // read and write indices
    protected int                count;    // number of elements available to be read
    protected final Lock         lock=new ReentrantLock();
    protected final Condition    not_empty=lock.newCondition(); // reader can block on this
    protected final Condition    not_full=lock.newCondition();  // writes can block on this

    public RingBuffer(Class<T> element_type, int capacity) {
        int c=Util.getNextHigherPowerOfTwo(capacity); // power of 2 for faster mod operation
        buf=(T[])Array.newInstance(element_type, c);
    }

    public T[] buf()               {return buf;}
    public int capacity()          {return buf.length;}
    public int readIndexLockless() {return ri;}
    public int countLockLockless() {return count;}

    public int readIndex() {
        lock.lock();
        try {
            return ri;
        }
        finally {
            lock.unlock();
        }
    }

    public int writeIndex() {
        lock.lock();
        try {
            return wi;
        }
        finally {
            lock.unlock();
        }
    }

    public int count() {
        lock.lock();
        try {
            return count;
        }
        finally {
            lock.unlock();
        }
    }



    /**
     * Tries to add a new element at the current write index and advances the write index. If the write index is at the
     * same position as the read index, this will block until the read index is advanced.
     * @param element the element to be added. Must not be null, or else this operation returns immediately without
     *                adding the null element
     */
    public RingBuffer<T> put(T element) throws InterruptedException {
        if(element == null)
            return this;
        lock.lock();
        try {
            while(count == buf.length)
                not_full.await();

            buf[wi]=element;
            if(++wi == buf.length)
                wi=0;
            count++;
            not_empty.signal();
            return this;
        }
        finally {
            lock.unlock();
        }
    }


    public T take() throws InterruptedException {
        lock.lock();
        try {
            while(count == 0)
                not_empty.await();
            T el=buf[ri];
            buf[ri]=null;
            if(++ri == buf.length)
                ri=0;
            count--;
            not_full.signal();
            return el;
        }
        finally {
            lock.unlock();
        }
    }


    public RingBuffer<T> publishReadIndex(int num_elements_read) {
        // this.ri is only read/written by the consumer, and since there's only 1 consumer, there's no need to synchronize on it
        this.ri=realIndex(this.ri + num_elements_read);

        lock.lock();
        try {
            this.count-=num_elements_read;
            not_full.signalAll(); // wake up all writers
            return this;
        }
        finally {
            lock.unlock();
        }
    }

    /** Blocks until messages are available */
    public int waitForMessages() throws InterruptedException {
        return waitForMessages(40, null);
    }

    /**
     *  Blocks until messages are available
     *  @param num_spins the number of times we should spin before acquiring a lock
     *  @param wait_strategy the strategy used to spin. The first parameter is the iteration count and the second
     *                       parameter is the max number of spins
     */

    public int waitForMessages(int num_spins, final BiConsumer<Integer,Integer> wait_strategy) throws InterruptedException {
        // try spinning first (experimental)
        for(int i=0; i < num_spins && count == 0; i++) {
            if(wait_strategy != null)
                wait_strategy.accept(i, num_spins);
            else
                Thread.yield();
        }
        if(count == 0) {
            lock.lock();
            try {
                while(count == 0)
                    not_empty.await();
            }
            finally {
                lock.unlock();
            }
        }
        return count; // whatever is the last count; could have been updated since lock release
    }


    public RingBuffer<T> clear() {
        lock.lock();
        try {
            count=ri=wi=0;
            for(int i=0; i < buf.length; i++)
                buf[i]=null;
            return this;
        }
        finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return count;
        }
        finally {
            lock.unlock();
        }
    }

    public boolean isEmpty() {
        lock.lock();
        try {
            return ri == wi;
        }
        finally {
            lock.unlock();
        }
    }

    public String toString() {
        return String.format("[ri=%d wi=%d size=%d cap=%d]", ri, wi, size(), buf.length);
    }


    /** Apparently much more efficient than mod (%) */
    protected int realIndex(int index) {
        return index & (buf.length -1);
    }


}
