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

import org.junit.Before;
import org.junit.Test;
import org.multiverse.TestThread;
import org.multiverse.TestUtils;
import org.multiverse.api.Txn;
import org.multiverse.api.TxnExecutor;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.stms.gamma.GammaConstants;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.LeanGammaTxnExecutor;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnLong;
import org.multiverse.stms.gamma.transactions.GammaTxnConfig;
import org.multiverse.stms.gamma.transactions.fat.FatFixedLengthGammaTxnFactory;
import org.multiverse.stms.gamma.transactions.fat.FatVariableLengthGammaTxnFactory;

import static org.junit.Assert.assertEquals;
import static org.multiverse.TestUtils.*;
import static org.multiverse.api.GlobalStmInstance.getGlobalStmInstance;
import static org.multiverse.api.StmUtils.retry;
import static org.multiverse.api.TxnThreadLocal.clearThreadLocalTxn;

public class MultipleReadsRetryStressTest implements GammaConstants {
    private GammaStm stm;
    private GammaTxnLong[] refs;
    private GammaTxnLong stopRef;
    private volatile boolean stop;

    @Before
    public void setUp() {
        clearThreadLocalTxn();
        stm = (GammaStm) getGlobalStmInstance();
        stop = false;
        stopRef = new GammaTxnLong(stm, 0);
    }

    @Test
    public void withMapTransactionAnd2Threads() throws InterruptedException {
        FatVariableLengthGammaTxnFactory txFactory = new FatVariableLengthGammaTxnFactory(stm);
        test(new LeanGammaTxnExecutor(txFactory), 10, 2);
    }

    @Test
    public void withArrayTransactionAnd2Threads() throws InterruptedException {
        int refCount = 10;
        GammaTxnConfig config = new GammaTxnConfig(stm, refCount + 1);
        FatFixedLengthGammaTxnFactory txFactory = new FatFixedLengthGammaTxnFactory(config);
        test(new LeanGammaTxnExecutor(txFactory), refCount, 2);
    }

    @Test
    public void withMapTransactionAnd5Threads() throws InterruptedException {
        FatVariableLengthGammaTxnFactory txFactory = new FatVariableLengthGammaTxnFactory(stm);
        test(new LeanGammaTxnExecutor(txFactory), 10, 5);
    }

    @Test
    public void withArrayTransactionAnd5Threads() throws InterruptedException {
        int refCount = 10;
        GammaTxnConfig config = new GammaTxnConfig(stm, refCount + 1);
        FatFixedLengthGammaTxnFactory txFactory = new FatFixedLengthGammaTxnFactory(config);
        test(new LeanGammaTxnExecutor(txFactory), refCount, 5);
    }

    public void test(TxnExecutor txnExecutor, int refCount, int threadCount) throws InterruptedException {
        refs = new GammaTxnLong[refCount];
        for (int k = 0; k < refs.length; k++) {
            refs[k] = new GammaTxnLong(stm);
        }

        UpdateThread[] threads = new UpdateThread[threadCount];
        for (int k = 0; k < threads.length; k++) {
            threads[k] = new UpdateThread(k, txnExecutor, threadCount);
        }

        startAll(threads);

        sleepMs(30 * 1000);
        stop = true;

        stopRef.atomicSet(-1);

        System.out.println("Waiting for joining threads");

        joinAll(threads);

        assertEquals(sumCount(threads), sumRefs());
    }

    private long sumRefs() {
        long result = 0;
        for (GammaTxnLong ref : refs) {
            result += ref.atomicGet();
        }
        return result;
    }

    private long sumCount(UpdateThread[] threads) {
        long result = 0;
        for (UpdateThread thread : threads) {
            result += thread.count;
        }
        return result;
    }

    private class UpdateThread extends TestThread {

        private final TxnExecutor txnExecutor;
        private final int id;
        private final int threadCount;
        private long count;

        public UpdateThread(int id, TxnExecutor txnExecutor, int threadCount) {
            super("UpdateThread-" + id);
            this.txnExecutor = txnExecutor;
            this.id = id;
            this.threadCount = threadCount;
        }

        @Override
        public void doRun() {
            TxnVoidCallable callable = new TxnVoidCallable() {
                @Override
                public void call(Txn tx) {
                    if (stopRef.get() < 0) {
                        throw new StopException();
                    }

                    long sum = 0;
                    for (GammaTxnLong ref : refs) {
                        sum += ref.get();
                    }

                    if (sum % threadCount != id) {
                        retry();
                    }

                    GammaTxnLong ref = refs[TestUtils.randomInt(refs.length)];
                    ref.incrementAndGet(1);
                }

            };

            while (!stop) {
                if (count % (10000) == 0) {
                    System.out.println(getName() + " " + count);
                }

                try {
                    txnExecutor.execute(callable);
                } catch (StopException e) {
                    break;
                }

                count++;
            }
        }
    }

    class StopException extends RuntimeException {
    }
}
