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

import org.junit.After;
import org.junit.Before;
import org.multiverse.TestThread;
import org.multiverse.api.Txn;
import org.multiverse.api.TxnExecutor;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnRef;
import org.multiverse.stms.gamma.transactions.GammaTxn;

import java.util.concurrent.atomic.AtomicBoolean;

import static org.junit.Assert.assertFalse;
import static org.multiverse.TestUtils.*;
import static org.multiverse.api.GlobalStmInstance.getGlobalStmInstance;
import static org.multiverse.api.TxnThreadLocal.clearThreadLocalTxn;

/**
 * Question: could the problem be in the quick release mechanism?
 * <p/>
 * Problem?
 * if a writing transaction has done n updates (and has released the updates) and has m to go.
 * If a reading transaction reads the n updates, there is no reason for the updating transaction to cause
 * a conflict since they are no conflicting arrives on the part if has already completes. If the reading transactions
 * hits the n+1 update, it is allowed to see a different value than it already has read...
 * problem.. the n updates it has read, already contains the new values, so reading another new value is no problem.
 */
public abstract class RefReadConsistency_AbstractTest {

    private GammaTxnRef<String>[] refs;
    private final AtomicBoolean inconsistencyDetected = new AtomicBoolean();

    private int readerCount = 10;
    private int writerCount = 2;
    private int durationMs = 1 * 60 * 1000;
    private volatile boolean stop;
    protected GammaStm stm;

    @Before
    public void setUp() {
        clearThreadLocalTxn();
        stop = false;
        stm = (GammaStm) getGlobalStmInstance();
        inconsistencyDetected.set(false);
    }

    @After
    public void tearDown() {
        System.out.println("Stm.GlobalConflictCount: " + stm.getGlobalConflictCounter().count());
        for (GammaTxnRef ref : refs) {
            System.out.println(ref.toDebugString());
        }
    }

    protected abstract TxnExecutor createReadBlock();

    protected abstract TxnExecutor createWriteBlock();

    public void run(int refCount) {
        refs = new GammaTxnRef[refCount];
        for (int k = 0; k < refs.length; k++) {
            refs[k] = new GammaTxnRef<String>(stm);
        }

        ReadThread[] readerThreads = new ReadThread[readerCount];
        for (int k = 0; k < readerThreads.length; k++) {
            readerThreads[k] = new ReadThread(k);
        }

        WriterThread[] writerThreads = new WriterThread[writerCount];
        for (int k = 0; k < writerThreads.length; k++) {
            writerThreads[k] = new WriterThread(k);
        }

        startAll(readerThreads);
        startAll(writerThreads);
        System.out.printf("Running for %s milliseconds\n", durationMs);
        sleepMs(getStressTestDurationMs(durationMs));
        stop = true;
        joinAll(readerThreads);
        joinAll(writerThreads);
        assertFalse("Inconsistency detected", inconsistencyDetected.get());
    }

    public class WriterThread extends TestThread {

        public WriterThread(int id) {
            super("WriterThread-" + id);
        }

        @Override
        public void doRun() throws Exception {
            final String value = getName();

            TxnExecutor executor = createWriteBlock();
            TxnVoidCallable callable = new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;
                    //String initial = refs[0].get(btx);

                    for (int k = 0; k < refs.length; k++) {
                        refs[k].openForWrite(btx, LOCKMODE_NONE).ref_value = value;
                        //String s = refs[k].getAndSet(tx, value);
                        //assertSame("failed at " + k, initial, s);
                    }
                }
            };

            int mod = 1;
            int k = 0;
            while (!stop) {
                executor.execute(callable);
                sleepRandomUs(100);

                k++;

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

    public class ReadThread extends TestThread {

        public ReadThread(int id) {
            super("ReadThread-" + id);
        }

        @Override
        public void doRun() throws Exception {
            TxnExecutor executor = createReadBlock();

            TxnVoidCallable callable = new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    GammaTxn btx = (GammaTxn) tx;

                    String initial = (String) refs[0].openForRead(btx, LOCKMODE_NONE).ref_value;

                    for (int k = 1; k < refs.length; k++) {
                        String s = (String) refs[k].openForRead(btx, LOCKMODE_NONE).ref_value;
                        if (s != initial) {
                            System.out.printf("Inconsistency detected at index %s!!!\n",k);
                            inconsistencyDetected.set(true);
                            stop = true;
                        }
                    }
                }
            };

            int mod = 1;
            int k = 0;
            while (!stop) {
                executor.execute(callable);
                k++;

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

