/*
 * Copyright 2014-2017 Real Logic Ltd.
 *
 * 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 io.aeron;

import io.aeron.driver.MediaDriver;
import io.aeron.logbuffer.FragmentHandler;
import io.aeron.logbuffer.Header;
import io.aeron.protocol.DataHeaderFlyweight;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.*;
import org.junit.Test;

import java.nio.ByteBuffer;

import static org.junit.Assert.fail;

public class MemoryOrderingTest
{
    private static final String CHANNEL = "aeron:udp?endpoint=localhost:54325";
    private static final int STREAM_ID = 1;
    private static final int FRAGMENT_COUNT_LIMIT = 10;
    private static final int MESSAGE_LENGTH = 1024 - DataHeaderFlyweight.HEADER_LENGTH;
    private static final int TERM_BUFFER_LENGTH = 1024 * 64;
    private static final int NUM_MESSAGES = 100_000;
    private static final int BURST_LENGTH = 5;
    private static final int INTER_BURST_DURATION_NS = 10_000;

    private static volatile String failedMessage = null;

    @Test(timeout = 10000)
    public void shouldReceiveMessagesInOrderWithFirstLongWordIntact() throws Exception
    {
        final UnsafeBuffer srcBuffer = new UnsafeBuffer(ByteBuffer.allocateDirect(MESSAGE_LENGTH));
        srcBuffer.setMemory(0, MESSAGE_LENGTH, (byte)7);
        final MediaDriver.Context ctx = new MediaDriver.Context()
            .publicationTermBufferLength(TERM_BUFFER_LENGTH);

        try (MediaDriver ignore = MediaDriver.launch(ctx);
             Aeron aeron = Aeron.connect();
             Publication publication = aeron.addPublication(CHANNEL, STREAM_ID);
             Subscription subscription = aeron.addSubscription(CHANNEL, STREAM_ID))
        {
            final BusySpinIdleStrategy idleStrategy = new BusySpinIdleStrategy();

            final Thread subscriberThread = new Thread(new Subscriber(subscription));
            subscriberThread.start();

            for (int i = 0; i < NUM_MESSAGES; i++)
            {
                if (null != failedMessage)
                {
                    fail(failedMessage);
                }

                srcBuffer.putLong(0, i);

                while (publication.offer(srcBuffer) < 0L)
                {
                    if (null != failedMessage)
                    {
                        fail(failedMessage);
                    }

                    idleStrategy.idle();
                }

                if (i % BURST_LENGTH == 0)
                {
                    final long timeout = System.nanoTime() + INTER_BURST_DURATION_NS;
                    long now;
                    do
                    {
                        now = System.nanoTime();
                    }
                    while (now < timeout);
                }
            }

            subscriberThread.join();
        }
        finally
        {
            ctx.deleteAeronDirectory();
        }
    }

    @Test(timeout = 10000)
    public void shouldReceiveMessagesInOrderWithFirstLongWordIntactFromExclusivePublication() throws Exception
    {
        final UnsafeBuffer srcBuffer = new UnsafeBuffer(ByteBuffer.allocateDirect(MESSAGE_LENGTH));
        srcBuffer.setMemory(0, MESSAGE_LENGTH, (byte)7);
        final MediaDriver.Context ctx = new MediaDriver.Context()
            .publicationTermBufferLength(TERM_BUFFER_LENGTH);

        try (MediaDriver ignore = MediaDriver.launch(ctx);
             Aeron aeron = Aeron.connect();
             ExclusivePublication publication = aeron.addExclusivePublication(CHANNEL, STREAM_ID);
             Subscription subscription = aeron.addSubscription(CHANNEL, STREAM_ID))
        {
            final BusySpinIdleStrategy idleStrategy = new BusySpinIdleStrategy();

            final Thread subscriberThread = new Thread(new Subscriber(subscription));
            subscriberThread.start();

            for (int i = 0; i < NUM_MESSAGES; i++)
            {
                if (null != failedMessage)
                {
                    fail(failedMessage);
                }

                srcBuffer.putLong(0, i);

                while (publication.offer(srcBuffer) < 0L)
                {
                    if (null != failedMessage)
                    {
                        fail(failedMessage);
                    }

                    idleStrategy.idle();
                }

                if (i % BURST_LENGTH == 0)
                {
                    final long timeout = System.nanoTime() + INTER_BURST_DURATION_NS;
                    long now;
                    do
                    {
                        now = System.nanoTime();
                    }
                    while (now < timeout);
                }
            }

            subscriberThread.join();
        }
        finally
        {
            ctx.deleteAeronDirectory();
        }
    }

    static class Subscriber implements Runnable, FragmentHandler
    {
        private final Subscription subscription;

        long previousValue = -1;
        int messageNum = 0;

        Subscriber(final Subscription subscription)
        {
            this.subscription = subscription;
        }

        public void run()
        {
            final BusySpinIdleStrategy idleStrategy = new BusySpinIdleStrategy();

            while (messageNum < NUM_MESSAGES && null == failedMessage)
            {
                idleStrategy.idle(subscription.poll(this, FRAGMENT_COUNT_LIMIT));
            }
        }

        public void onFragment(final DirectBuffer buffer, final int offset, final int length, final Header header)
        {
            final long messageValue = buffer.getLong(offset);

            final long expectedValue = previousValue + 1;
            if (messageValue != expectedValue)
            {
                final long messageValueSecondRead = buffer.getLong(offset);

                final String msg = "Issue at message number transition: " + previousValue + " -> " + messageValue;

                System.out.println(msg + "\n" +
                    "offset: "  + offset + "\n" +
                    "length: "  + length + "\n" +
                    "expected bytes: " + byteString(expectedValue) + "\n" +
                    "received bytes: " + byteString(messageValue) + "\n" +
                    "expected bits: " + Long.toBinaryString(expectedValue) + "\n" +
                    "received bits: " + Long.toBinaryString(messageValue) + "\n" +
                    "messageValue on second read: " + messageValueSecondRead + "\n" +
                    "messageValue on third read: " + buffer.getLong(offset));

                failedMessage = msg;
            }

            previousValue = messageValue;
            messageNum++;
        }

        private String byteString(final long value)
        {
            return String.format("%x %x %x %x %x %x %x %x",
                (byte)(value >>> 56),
                (byte)(value >>> 48),
                (byte)(value >>> 40),
                (byte)(value >>> 32),
                (byte)(value >>> 24),
                (byte)(value >>> 18),
                (byte)(value >>> 8),
                (byte)value);
        }
    }
}
@@ -1190,48 +1190,12 @@
 H = 
-1024 - DataHeaderFlyweight.HEADER_LENGTH
+2000
 ;%0A  
@@ -1298,18 +1298,17 @@
 AGES = 1
-00
+5
 _000;%0A  
@@ -1349,17 +1349,17 @@
 ENGTH = 
-5
+7
 ;%0A    pr
@@ -1409,16 +1409,17 @@
 _NS = 10
+0
 _000;%0A%0A 

Increase throughput in memory ordering test.