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

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 java.util.concurrent.atomic.AtomicLong;

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;

/**
 * A Stresstest that sees if the stm can be used to create a readwritelock that is not reentrant.
 *
 * @author Peter Veentjer.
 */
public abstract class NonReentrantReadWriteLock_AbstractTest {
    protected GammaStm stm;
    private int threadCount = 10;
    private ReadWriteLock readWriteLock;
    private volatile boolean stop;

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

    protected abstract TxnExecutor newReleaseWriteLockBlock();

    protected abstract TxnExecutor newAcquireWriteLockBlock();

    protected abstract TxnExecutor newReleaseReadLockBlock();

    protected abstract TxnExecutor newAcquireReadLockBlock();

    public void run() {
        readWriteLock = new ReadWriteLock();

        StressThread[] threads = new StressThread[threadCount];
        for (int k = 0; k < threadCount; k++) {
            threads[k] = new StressThread(k);
        }

        startAll(threads);
        sleepMs(30000);
        stop = true;
        joinAll(threads);
    }

    class StressThread extends TestThread {
        public StressThread(int id) {
            super("StressThread-" + id);
        }

        @Override
        public void doRun() throws Exception {
            long count = 0;
            while (!stop) {
                if (randomOneOf(5)) {
                    readWriteLock.acquireWriteLock();
                    sleepMs(100);
                    readWriteLock.releaseWriteLock();
                } else {
                    readWriteLock.acquireReadLock();
                    sleepMs(100);
                    readWriteLock.releaseReadLock();
                }
                count++;
                if (count % 10 == 0) {
                    System.out.printf("%s is at %s\n", getName(), count);
                }
            }
        }
    }

    class ReadWriteLock {
        final GammaTxnRef<Long> lock = new GammaTxnRef<Long>(stm, 0L);
        final AtomicLong readers = new AtomicLong();
        final AtomicLong writers = new AtomicLong();
        final TxnExecutor acquireReadLockBlock = newAcquireReadLockBlock();
        final TxnExecutor releaseReadLockBlock = newReleaseReadLockBlock();
        final TxnExecutor acquireWriteLockBlock = newAcquireWriteLockBlock();
        final TxnExecutor releaseWriteLockBlock = newReleaseWriteLockBlock();


        public void acquireReadLock() {
            acquireReadLockBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    if (lock.get() < 0) {
                        retry();
                    }

                    lock.set(lock.get() + 1);
                }
            });

            readers.incrementAndGet();

            assertEquals(0, writers.get());
        }

        public void releaseReadLock() {
            readers.decrementAndGet();
            assertEquals(0, writers.get());

            releaseReadLockBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    if (lock.get() <= 0) {
                        throw new IllegalMonitorStateException();
                    }

                    lock.set(lock.get() - 1);
                }
            });
        }

        public void acquireWriteLock() {
            acquireWriteLockBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    if (lock.get() != 0) {
                        retry();
                    }

                    lock.set(-1L);
                }
            });

            writers.incrementAndGet();
            assertEquals(0, readers.get());
        }

        public void releaseWriteLock() {
            writers.decrementAndGet();
            assertEquals(0, readers.get());

            releaseWriteLockBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    if (lock.get() != -1) {
                        throw new IllegalMonitorStateException();
                    }

                    lock.set(0L);
                }
            });
        }
    }


}
