/*
 *     Copyright (C) 2015  higherfrequencytrading.com
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.openhft.lang.io;

import net.openhft.lang.model.constraints.NotNull;
import org.junit.Ignore;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 * @author peter.lawrey
 */
public class DirectBytesTest {
    private static void manyToggles(@NotNull DirectStore store1, int lockCount, int from, int to) {
        long id = Thread.currentThread().getId();
        assertEquals(0, id >>> 24);
        System.out.println("Thread " + id);

        DirectBytes slice1 = store1.bytes();

        int records = 64;
        for (int i = 0; i < lockCount; i += records) {
            for (long j = 0; j < records * 64; j += 64) {
                slice1.positionAndSize(j, 64);
                boolean condition = false;
                for (int k = 0; k < 20; k++) {
                    condition = slice1.tryLockInt(0L) || slice1.tryLockNanosInt(0L, 5 * 1000 * 1000);
                    if (condition)
                        break;
                    if (k > 0)
                        System.out.println("k: " + (5 + k * 5));
                }
                if (!condition)
                    assertTrue("i= " + i, condition);
                int toggle1 = slice1.readInt(4);
                if (toggle1 == from) {
                    slice1.writeInt(4L, to);

                } else {
                    // noinspection AssignmentToForLoopParameter,AssignmentToForLoopParameter
                    i--;
                }
                slice1.unlockInt(0L);
                System.currentTimeMillis(); // small delay
            }
        }
    }

    private static void manyLongToggles(@NotNull DirectStore store1, int lockCount, int from, int to) {
        long id = Thread.currentThread().getId();
        assertEquals(0, id >>> 32);
        System.out.println("Thread " + id);

        DirectBytes slice1 = store1.bytes();

        int records = 64;
        for (int i = 0; i < lockCount; i += records) {
            for (long j = 0; j < records * 64; j += 64) {
                slice1.positionAndSize(j, 64);
                boolean condition = false;
                for (int k = 0; k < 20; k++) {
                    condition = slice1.tryLockLong(0L) || slice1.tryLockNanosLong(0L, 5 * 1000 * 1000);
                    if (condition)
                        break;
                    if (k > 0)
                        System.out.println("k: " + (5 + k * 5));
                }
                if (!condition)
                    assertTrue("i= " + i, condition);
                long toggle1 = slice1.readLong(8);
                if (toggle1 == from) {
                    slice1.writeLong(8L, to);

                } else {
                    // noinspection AssignmentToForLoopParameter,AssignmentToForLoopParameter
                    i--;
                }
                slice1.unlockLong(0L);
                System.currentTimeMillis(); // small delay
            }
        }
    }

    @Test
    public void testSimpleLock() {
        DirectBytes bytes = new DirectStore(64).bytes();
        bytes.writeLong(0, -1L);
        assertFalse(bytes.tryLockLong(0));
        bytes.writeLong(0, 0L);
        assertTrue(bytes.tryLockLong(0));
        long val1 = bytes.readLong(0);
        assertTrue(bytes.tryLockLong(0));
        bytes.unlockLong(0);
        assertEquals(val1, bytes.readLong(0));
        bytes.unlockLong(0);
        assertEquals(0L, bytes.readLong(0));
        try {
            bytes.unlockLong(0);
            fail();
        } catch (IllegalMonitorStateException e) {
            // expected.
        }
    }

    @Test
    public void testAllocate()   {
        long size = 1L << 24; // 31; don't overload cloud-bees
        DirectStore store = DirectStore.allocate(size);
        assertEquals(size, store.size());
        DirectBytes slice = store.bytes();
        slice.positionAndSize(0, size);
        slice.writeLong(0, size);
        slice.writeLong(size - 8, size);
        store.free();
    }

    @Test
    public void testLocking() {
        if (Runtime.getRuntime().availableProcessors() < 2) {
            System.err.println("Test requires 2 CPUs, skipping");
            return;
        }
        long start = System.nanoTime();
        // a page
        final DirectStore store1 = DirectStore.allocate(1 << 12);
        final int lockCount = 10 * 1000 * 1000;
        new Thread(new Runnable() {
            @Override
            public void run() {
                manyToggles(store1, lockCount, 1, 0);
            }
        }).start();

        manyToggles(store1, lockCount, 0, 1);

        store1.free();
        long time = System.nanoTime() - start;
        System.out.printf("Contended lock rate was %,d per second%n", (int) (lockCount * 2 * 1e9 / time));
    }

    @Test
    public void testLockingLong() {
        if (Runtime.getRuntime().availableProcessors() < 2) {
            System.err.println("Test requires 2 CPUs, skipping");
            return;
        }
        long start = System.nanoTime();
        // a page
        final DirectStore store1 = DirectStore.allocate(1 << 12);
        final int lockCount = 10 * 1000 * 1000;
        new Thread(new Runnable() {
            @Override
            public void run() {
                manyLongToggles(store1, lockCount, 1, 0);
            }
        }).start();

        manyLongToggles(store1, lockCount, 0, 1);

        store1.free();
        long time = System.nanoTime() - start;
        System.out.printf("Contended lock rate was %,d per second%n", (int) (lockCount * 2 * 1e9 / time));
    }

    @Test
    public void testLocking2() throws InterruptedException {
        if (Runtime.getRuntime().availableProcessors() < 2) {
            System.err.println("Test requires 2 CPUs, skipping");
            return;
        }
        // a page
        final DirectStore store1 = DirectStore.allocate(1 << 12);
        final DirectStore store2 = DirectStore.allocate(1 << 12);
        final int lockCount = 1000000;

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                long id = Thread.currentThread().getId();
                System.out.println("Thread " + id);
                assertEquals(0, id >>> 24);
                int expected = (1 << 24) | (int) id;
                try {
                    DirectBytes slice1 = store1.bytes();
                    DirectBytes slice2 = store2.bytes();

                    for (int i = 0; i < lockCount; i += 2) {
                        slice1.busyLockInt(0);
                        slice2.busyLockInt(0);
                        int lockValue1 = slice1.readInt(0);
                        if (lockValue1 != expected)
                            assertEquals(expected, lockValue1);
                        int lockValue2 = slice2.readInt(0);
                        if (lockValue2 != expected)
                            assertEquals(expected, lockValue2);
                        int toggle1 = slice1.readInt(4);
                        if (toggle1 == 1) {
                            slice1.writeInt(4, 0);
                            // if (i % 10000== 0)
                            // System.out.println("t: " + i);

                        } else {
                            // noinspection AssignmentToForLoopParameter
                            i--;
                        }
                        int toggle2 = slice2.readInt(4);
                        if (toggle2 == 1) {
                            slice2.writeInt(4, 0);
                            // if (i % 10000== 0)
                            // System.out.println("t: " + i);

                        } else {
                            // noinspection AssignmentToForLoopParameter
                            i--;
                        }
                        int lockValue1A = slice1.readInt(0);
                        int lockValue2A = slice1.readInt(0);
                        try {
                            slice2.unlockInt(0);
                            slice1.unlockInt(0);
                        } catch (IllegalStateException e) {
                            int lockValue1B = slice1.readInt(0);
                            int lockValue2B = slice2.readInt(0);
                            System.err.println("i= " + i +
                                    " lock: " + Integer.toHexString(lockValue1A) + " / " + Integer.toHexString(lockValue2A) +
                                    " lock: " + Integer.toHexString(lockValue1B) + " / " + Integer.toHexString(lockValue2B));
                            throw e;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        long id = Thread.currentThread().getId();
        assertEquals(0, id >>> 24);
        int expected = (1 << 24) | (int) id;
        System.out.println("Thread " + id);

        DirectBytes slice1 = store1.bytes();
        DirectBytes slice2 = store2.bytes();
        for (int i = 0; i < lockCount; i += 2) {
            slice1.busyLockInt(0);
            slice2.busyLockInt(0);
            int lockValue1 = slice1.readInt(0);
            if (lockValue1 != expected)
                assertEquals(expected, lockValue1);
            int lockValue2 = slice2.readInt(0);
            if (lockValue2 != expected)
                assertEquals(expected, lockValue2);
            int toggle1 = slice1.readInt(4);
            if (toggle1 == 0) {
                slice1.writeInt(4, 1);
                // if (i % 10000== 0)
                // System.out.println("t: " + i);

            } else {
                // noinspection AssignmentToForLoopParameter
                i--;
            }
            int toggle2 = slice2.readInt(4);
            if (toggle2 == 0) {
                slice2.writeInt(4, 1);
                // if (i % 10000== 0)
                // System.out.println("t: " + i);

            } else {
                // noinspection AssignmentToForLoopParameter
                i--;
            }
            int lockValue1A = slice1.readInt(0);
            int lockValue2A = slice1.readInt(0);
            try {
                slice2.unlockInt(0);
                slice1.unlockInt(0);
            } catch (IllegalStateException e) {
                int lockValue1B = slice1.readInt(0);
                int lockValue2B = slice2.readInt(0);
                System.err.println("i= " + i +
                        " lock: " + Integer.toHexString(lockValue1A) + " / " + Integer.toHexString(lockValue2A) +
                        " lock: " + Integer.toHexString(lockValue1B) + " / " + Integer.toHexString(lockValue2B));
                throw e;
            }
        }

        store1.free();
        store2.free();
    }

    @Test
    @Ignore
    public void benchmarkByteLargeArray() {
        long length = 1L << 32;
        long start = System.nanoTime();
        DirectBytes array = DirectStore.allocateLazy(length).bytes();
        System.out.println("Constructor time: " + (System.nanoTime() - start) / 1e9 + " sec");
        int iters = 2;
        byte one = 1;
        start = System.nanoTime();
        for (int it = 0; it < iters; it++) {
            for (long i = 0; i < length; i++) {
                array.writeByte(i, one);
                array.writeByte(i, (byte) (array.readByte(i) + one));
            }
        }
        System.out.println("Computation time: " + (System.nanoTime() - start) / 1e9 / iters + " sec");
    }
}
