/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.nifi.util;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Thread-safe implementation of a RingBuffer
 *
 */
public class RingBuffer<T> {

    private final Object[] buffer;
    private int insertionPointer = 0;
    private boolean filled = false;

    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    public RingBuffer(final int size) {
        buffer = new Object[size];
    }

    /**
     * Adds the given value to the RingBuffer and returns the value that was removed in order to make room.
     *
     * @param value the new value to add
     * @return value previously in the buffer
     */
    @SuppressWarnings("unchecked")
    public T add(final T value) {
        Objects.requireNonNull(value);

        writeLock.lock();
        try {
            final Object removed = buffer[insertionPointer];

            buffer[insertionPointer] = value;

            if (insertionPointer == buffer.length - 1) {
                filled = true;
            }

            insertionPointer = (insertionPointer + 1) % buffer.length;
            return (T) removed;
        } finally {
            writeLock.unlock();
        }
    }

    public int getSize() {
        readLock.lock();
        try {
            return filled ? buffer.length : insertionPointer;
        } finally {
            readLock.unlock();
        }
    }

    public List<T> getSelectedElements(final Filter<T> filter) {
        return getSelectedElements(filter, Integer.MAX_VALUE);
    }

    public List<T> getSelectedElements(final Filter<T> filter, final int maxElements) {
        final List<T> selected = new ArrayList<>(1000);
        int numSelected = 0;
        readLock.lock();
        try {
            for (int i = 0; i < buffer.length && numSelected < maxElements; i++) {
                final int idx = (insertionPointer + i) % buffer.length;
                final Object val = buffer[idx];
                if (val == null) {
                    continue;
                }

                @SuppressWarnings("unchecked")
                final T element = (T) val;
                if (filter.select(element)) {
                    selected.add(element);
                    numSelected++;
                }
            }
        } finally {
            readLock.unlock();
        }
        return selected;
    }

    public int countSelectedElements(final Filter<T> filter) {
        int numSelected = 0;
        readLock.lock();
        try {
            for (int i = 0; i < buffer.length; i++) {
                final int idx = (insertionPointer + i) % buffer.length;
                final Object val = buffer[idx];
                if (val == null) {
                    continue;
                }

                @SuppressWarnings("unchecked")
                final T element = (T) val;
                if (filter.select(element)) {
                    numSelected++;
                }
            }
        } finally {
            readLock.unlock();
        }

        return numSelected;
    }

    /**
     * Removes all elements from the RingBuffer that match the given filter
     *
     * @param filter to use for deciding what is removed
     * @return always zero
     */
    public int removeSelectedElements(final Filter<T> filter) {
        int count = 0;

        writeLock.lock();
        try {
            for (int i = 0; i < buffer.length; i++) {
                final int idx = (insertionPointer + i + 1) % buffer.length;
                final Object val = buffer[idx];
                if (val == null) {
                    continue;
                }

                @SuppressWarnings("unchecked")
                final T element = (T) val;

                if (filter.select(element)) {
                    buffer[idx] = null;
                }
            }
        } finally {
            writeLock.unlock();
        }

        return count;
    }

    public List<T> asList() {
        return getSelectedElements(new Filter<T>() {
            @Override
            public boolean select(final T value) {
                return true;
            }
        });
    }

    public T getOldestElement() {
        readLock.lock();
        try {
            return getElementData(insertionPointer);
        } finally {
            readLock.unlock();
        }
    }

    public T getNewestElement() {
        readLock.lock();
        try {
            int index = (insertionPointer == 0) ? buffer.length - 1 : insertionPointer - 1;
            return getElementData(index);
        } finally {
            readLock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    private T getElementData(final int index) {
        readLock.lock();
        try {
            return (T) buffer[index];
        } finally {
            readLock.unlock();
        }
    }

    /**
     * Iterates over each element in the RingBuffer, calling the {@link ForEachEvaluator#evaluate(Object) evaluate} method on each element in the RingBuffer. If the Evaluator returns {@code false},
     * the method will skip all remaining elements in the RingBuffer; otherwise, the next element will be evaluated until all elements have been evaluated.
     *
     * @param evaluator used to evaluate each item in the ring buffer
     */
    public void forEach(final ForEachEvaluator<T> evaluator) {
        forEach(evaluator, IterationDirection.FORWARD);
    }

    /**
     * Iterates over each element in the RingBuffer, calling the {@link ForEachEvaluator#evaluate(Object) evaluate} method on each element in the RingBuffer. If the Evaluator returns {@code false},
     * the method will skip all remaining elements in the RingBuffer; otherwise, the next element will be evaluated until all elements have been evaluated.
     *
     * @param evaluator the evaluator
     * @param iterationDirection the order in which to iterate over the elements in the RingBuffer
     */
    public void forEach(final ForEachEvaluator<T> evaluator, final IterationDirection iterationDirection) {
        readLock.lock();
        try {
            final int startIndex;
            final int endIndex;
            final int increment;

            if (iterationDirection == IterationDirection.FORWARD) {
                startIndex = 0;
                endIndex = buffer.length - 1;
                increment = 1;
            } else {
                startIndex = buffer.length - 1;
                endIndex = 0;
                increment = -1;
            }

            for (int i = startIndex; (iterationDirection == IterationDirection.FORWARD ? i <= endIndex : i >= endIndex); i += increment) {
                final int idx = (insertionPointer + i) % buffer.length;
                final Object val = buffer[idx];
                if (val == null) {
                    continue;
                }

                @SuppressWarnings("unchecked")
                final T element = (T) val;
                if (!evaluator.evaluate(element)) {
                    return;
                }
            }
        } finally {
            readLock.unlock();
        }
    }

    public static interface Filter<S> {

        boolean select(S value);
    }

    /**
     * Defines an interface that can be used to iterate over all of the elements in the RingBuffer via the {@link #forEach} method
     *
     * @param <S> the type to evaluate
     */
    public static interface ForEachEvaluator<S> {

        /**
         * Evaluates the given element and returns {@code true} if the next element should be evaluated, {@code false} otherwise
         *
         * @param value the value to evaluate
         * @return true if should continue evaluating; false otherwise
         */
        boolean evaluate(S value);
    }

    public static enum IterationDirection {

        FORWARD,
        BACKWARD;
    }
}
