/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.
 */

package org.modeshape.common.collection.ring;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.common.statistic.Stopwatch;

/**
 * @author Randall Hauch (rhauch@redhat.com)
 */
public class RingBufferTest {
    protected static final Random RANDOM = new Random();

    protected volatile boolean print = false;
    protected volatile boolean slightPausesInConsumers = false;

    @Before
    public void beforeEach() {
        print = false;
    }

    @Test
    public void shouldBuildWithNoGarbageCollection() {
        Executor executor = Executors.newCachedThreadPool();
        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(8)
                                                                       .garbageCollect(false).build();
        print = false;

        // Add 10 entries with no consumers ...
        long value = 0L;
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
        }

        // Add a single consumer that should start seeing items 10 and up ...
        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
        ringBuffer.addConsumer(consumer1);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        ringBuffer.shutdown();
        print("");
        print("Ring buffer shutdown completed");
        assertTrue(consumer1.isClosed());
    }

    @Test
    public void shouldBuildWithGarbageCollectionAnd8Entries() {
        Executor executor = Executors.newCachedThreadPool();
        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(8)
                                                                       .garbageCollect(true).build();
        print = false;

        // Add 10 entries with no consumers ...
        long value = 0L;
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
        }

        // Add a single consumer that should start seeing items 10 and up ...
        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
        ringBuffer.addConsumer(consumer1);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        ringBuffer.shutdown();
        print("");
        print("Ring buffer shutdown completed");
        // assertTrue(consumer1.isClosed());
    }

    @Test
    public void shouldBuildWithGarbageCollectionAnd1024Entries() {
        Executor executor = Executors.newCachedThreadPool();
        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(1024)
                                                                       .garbageCollect(true).build();
        print = false;

        // Add 10 entries with no consumers ...
        long value = 0L;
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
        }

        // Add a single consumer that should start seeing items 10 and up ...
        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
        ringBuffer.addConsumer(consumer1);

        // Add 10 more entries ...
        for (int i = 0; i != 1000; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        ringBuffer.shutdown();
        print("");
        print("Ring buffer shutdown completed");
        // assertTrue(consumer1.isClosed());
    }

    @Test
    public void shouldBeAbleToAddAndRemoveConsumers() throws Exception {
        Executor executor = Executors.newCachedThreadPool();
        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(8)
                                                                       .build();
        print = false;

        // Add 10 entries with no consumers ...
        long value = 0L;
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
        }

        // Add a single consumer that should start seeing items 10 and up ...
        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
        ringBuffer.addConsumer(consumer1);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        // Add a second consumer that should start seeing items 20 and up ...
        MonotonicallyIncreasingConsumer consumer2 = new MonotonicallyIncreasingConsumer("second", 20L, 20L, 0);
        ringBuffer.addConsumer(consumer2);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        ringBuffer.remove(consumer2);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        assertTrue(consumer2.isClosed());
        ringBuffer.shutdown();
        print("");
        print("Ring buffer shutdown completed");
        assertTrue(consumer1.isClosed());
        assertTrue(consumer2.isClosed());
    }

    @Test
    // @Ignore( "Takes a long time to run" )
    public void consumersShouldSeeEventsInCorrectOrder() throws Exception {
        Executor executor = Executors.newCachedThreadPool();
        RingBuffer<Long, MonotonicallyIncreasingConsumer> ringBuffer = RingBufferBuilder.withSingleProducer(executor,
                                                                                                            LongConsumerAdapter.INSTANCE)
                                                                                        .ofSize(8).garbageCollect(false).build();
        print = false;

        // Add 10 entries with no consumers ...
        long value = 0L;
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
        }

        // Add a single consumer that should start seeing items 10 and up ...
        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
        ringBuffer.addConsumer(consumer1);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        // Add a single consumer that should start seeing items 10 and up ...
        MonotonicallyIncreasingConsumer consumer2 = new MonotonicallyIncreasingConsumer("second", 20L, 20L, 0);
        ringBuffer.addConsumer(consumer2);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        // Add a second consumer that should start seeing items 20 and up ...
        MonotonicallyIncreasingConsumer consumer3 = new MonotonicallyIncreasingConsumer("third", 30L, 30L, 0);
        ringBuffer.addConsumer(consumer3);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        // Add a second consumer that should start seeing items 20 and up ...
        MonotonicallyIncreasingConsumer consumer4 = new MonotonicallyIncreasingConsumer("fourth", 40L, 40L, 0);
        ringBuffer.addConsumer(consumer4);

        // Add 10 more entries ...
        for (int i = 0; i != 10; ++i) {
            print("Adding entry " + value);
            ringBuffer.add(value++);
            // Thread.sleep(100L);
        }

        // print = true;
        slightPausesInConsumers = false;
        boolean slightPauseBetweenEvents = false;

        // Add 400K more entries
        Stopwatch sw = new Stopwatch();
        int count = 2000;
        sw.start();
        for (int i = 0; i != count; ++i) {
            ringBuffer.add(value++);
            if (slightPauseBetweenEvents) {
                Thread.sleep(RANDOM.nextInt(50));
            }
        }
        sw.stop();

        // Do 10 more while printing ...
        for (int i = 0; i != 10; ++i) {
            // print = true;
            print("Adding entry " + value);
            ringBuffer.add(value++);
        }

        ringBuffer.shutdown();
        print("");
        print("Ring buffer shutdown completed");
        assertTrue(consumer1.isClosed());
        assertTrue(consumer2.isClosed());
        assertTrue(consumer3.isClosed());
        assertTrue(consumer4.isClosed());

        --value;
        assertThat(consumer1.getLastValue(), is(value));
        assertThat(consumer2.getLastValue(), is(value));
        assertThat(consumer3.getLastValue(), is(value));
        assertThat(consumer4.getLastValue(), is(value));

        print("");
        print("Time to add " + count + " entries: " + sw.getAverageDuration());
    }

    protected void print( String message ) {
        if (print) System.out.println(message);
    }

    protected class MonotonicallyIncreasingConsumer extends Consumer<Long> {
        private final String id;
        private boolean first = true;
        private long lastValue = -1L;
        private long lastPosition = -1L;
        private boolean closed = false;
        private final int secondsToWork;

        public MonotonicallyIncreasingConsumer( String id,
                                                long firstValue,
                                                long firstPosition,
                                                int secondsToWork ) {
            this.id = id;
            this.lastValue = firstValue;
            this.lastPosition = firstPosition;
            this.secondsToWork = secondsToWork;
        }

        @Override
        public boolean consume( Long entry,
                                long position,
                                long max ) {
            assertTrue(!closed);
            print(id + " consuming " + entry.longValue() + " at position " + position + " with max " + max);
            try {
                Thread.sleep(secondsToWork * 1000);
            } catch (Exception e) {
                e.printStackTrace();
                fail(e.getMessage());
            }
            if (slightPausesInConsumers && position % 1000 == 0) {
                try {
                    Thread.sleep(RANDOM.nextInt(100));
                } catch (Exception e) {
                    e.printStackTrace();
                    fail(e.getMessage());
                }
            }
            if (first) {
                assertTrue(entry.longValue() == lastValue);
                assertTrue(position == lastPosition);
                first = false;
            } else {
                assertTrue(entry.longValue() == (lastValue + 1));
                lastValue = entry.longValue();
                assertTrue(position == (lastPosition + 1));
                lastPosition = position;
            }
            return true;
        }

        @Override
        public void close() {
            super.close();
            print(id + " closing");
            closed = true;
        }

        public long getLastPosition() {
            return lastPosition;
        }

        public long getLastValue() {
            return lastValue;
        }

        public boolean isClosed() {
            return closed;
        }
    }

    private static class LongConsumerAdapter implements RingBuffer.ConsumerAdapter<Long, MonotonicallyIncreasingConsumer> {
        protected static final LongConsumerAdapter INSTANCE = new LongConsumerAdapter();

        private LongConsumerAdapter() {
        }

        @Override
        public boolean consume( MonotonicallyIncreasingConsumer consumer,
                                Long event,
                                long position,
                                long maxPosition ) {
            consumer.consume(event, position, maxPosition);
            return true;
        }

        @Override
        public void close( MonotonicallyIncreasingConsumer consumer ) {
            consumer.close();
        }

        @Override
        public void handleException( MonotonicallyIncreasingConsumer consumer,
                                     Throwable t,
                                     Long entry,
                                     long position,
                                     long maxPosition ) {
            throw new AssertionError("Test failure", t);
        }
    }

}
