package org.multiverse.stms.gamma.integration.isolation;

import org.junit.Before;
import org.junit.Test;
import org.multiverse.TestThread;
import org.multiverse.api.Txn;
import org.multiverse.api.TxnExecutor;
import org.multiverse.api.LockMode;
import org.multiverse.api.callables.TxnBooleanCallable;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnLong;

import static java.lang.System.currentTimeMillis;
import static org.junit.Assert.assertEquals;
import static org.multiverse.TestUtils.joinAll;
import static org.multiverse.TestUtils.randomOneOf;
import static org.multiverse.api.GlobalStmInstance.getGlobalStmInstance;
import static org.multiverse.api.TxnThreadLocal.clearThreadLocalTxn;
import static org.multiverse.stms.gamma.BenchmarkUtils.transactionsPerSecondAsString;
import static org.multiverse.stms.gamma.BenchmarkUtils.transactionsPerSecondPerThreadAsString;

/**
 * A Stress test that checks if the system is able to deal with mostly reading transactions and doesn't cause
 * any isolation problems.
 *
 * @author Peter Veentjer.
 */
public class ReadBiasedIsolationStressTest {

    private GammaStm stm = (GammaStm) getGlobalStmInstance();
    private int chanceOfUpdate = new GammaTxnLong(stm).getReadBiasedThreshold() * 5;
    private int threadCount = 4;

    @Before
    public void setUp() {
        clearThreadLocalTxn();
    }

    //todo: testing with and without arriveenabled functionality

    @Test
    public void none_and_dirtyCheckEnabled() {
        test(LockMode.None, true);
    }

    @Test
    public void none_read_and_dirtyCheckDisabled() {
        test(LockMode.None, false);
    }

    @Test
    public void read_and_dirtyCheckEnabled() {
        test(LockMode.Read, true);
    }

    @Test
    public void read_and_dirtyCheckDisabled() {
        test(LockMode.Read, false);
    }

    @Test
    public void write_and_dirtyCheckEnabled() {
        test(LockMode.Write, true);
    }

    @Test
    public void write_and_dirtyCheckDisabled() {
        test(LockMode.Write, false);
    }

    @Test
    public void commit_and_dirtyCheckEnabled() {
        test(LockMode.Exclusive, true);
    }

    @Test
    public void commit_and_dirtyCheckDisabled() {
        test(LockMode.Exclusive, false);
    }


    public void test(LockMode lockMode, boolean dirtyCheckEnabled) {
        StressThread[] threads = new StressThread[threadCount];
        GammaTxnLong ref = new GammaTxnLong(stm);
        long transactionsPerThread = 100 * 1000 * 1000;

        for (int k = 0; k < threads.length; k++) {
            threads[k] = new StressThread(k, ref, transactionsPerThread, lockMode, dirtyCheckEnabled);
        }

        for (StressThread thread : threads) {
            thread.start();
        }

        joinAll(threads);

        long totalDurationMs = 0;
        long sum = 0;
        for (StressThread thread : threads) {
            totalDurationMs += thread.durationMs;
            sum += thread.incrementCount;
        }

        System.out.println("--------------------------------------------------------");
        System.out.printf("Threadcount:       %s\n", threadCount);
        System.out.printf("Performance:       %s transactions/second/thread\n",
                transactionsPerSecondPerThreadAsString(transactionsPerThread, totalDurationMs, threadCount));
        System.out.printf("Performance:       %s transactions/second\n",
                transactionsPerSecondAsString(transactionsPerThread, totalDurationMs, threadCount));

        assertEquals(sum, ref.atomicGet());
        System.out.println("ref.orec: " + ref.___toOrecString());
    }

    class StressThread extends TestThread {
        private final boolean dirtyCheckEnabled;
        private final GammaTxnLong ref;
        private final long count;
        private long durationMs;
        private LockMode lockMode;
        private long incrementCount = 0;

        public StressThread(int id, GammaTxnLong ref, long count, LockMode lockMode, boolean dirtyCheckEnabled) {
            super("StressThread-" + id);
            this.ref = ref;
            this.count = count;
            this.lockMode = lockMode;
            this.dirtyCheckEnabled = dirtyCheckEnabled;
        }

        @Override
        public void doRun() {
            TxnExecutor executor = stm.newTxnFactoryBuilder()
                    .setDirtyCheckEnabled(dirtyCheckEnabled)
                    .newTxnExecutor();

            TxnBooleanCallable callable = new TxnBooleanCallable() {
                @Override
                public boolean call(Txn tx) throws Exception {
                    ref.getLock().acquire(tx, lockMode);

                    if (randomOneOf(chanceOfUpdate)) {
                        ref.incrementAndGet(tx, 1);
                        return true;
                    } else {
                        ref.get(tx);
                        return false;
                    }
                }
            };

            long startMs = currentTimeMillis();

            for (long k = 0; k < count; k++) {
                if (executor.execute(callable)) {
                    incrementCount++;
                }

                if (k % 10000000 == 0) {
                    System.out.printf("%s is at %s\n", getName(), k);
                }
            }

            durationMs = currentTimeMillis() - startMs;

            System.out.printf("finished %s after %s ms\n", getName(), durationMs);
        }
    }
}
