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

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.TxnVoidCallable;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnLong;
import org.multiverse.stms.gamma.transactions.GammaTxn;

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

public class DeadLockStressTest {

    enum Mode {
        Normal, Mix, PrivatizeReadLevelMode, PrivatizeWriteLevelMode
    }

    private volatile boolean stop;
    private int refCount = 100;
    private int threadCount = 10;
    private GammaTxnLong[] refs;
    private ChangeThread[] threads;
    private GammaStm stm;
    private Mode mode;

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

    @Test
    public void whenNormal() {
        test(Mode.Normal);
    }

    @Test
    public void whenMix() {
        test(Mode.Mix);
    }

    @Test
    public void whenPessimisticReadLevel() {
        test(Mode.PrivatizeReadLevelMode);
    }

    @Test
    public void whenPessimisticWriteLevel() {
        test(Mode.PrivatizeWriteLevelMode);
    }

    public void test(Mode mode) {
        this.mode = mode;

        refs = new GammaTxnLong[refCount];
        for (int k = 0; k < refCount; k++) {
            refs[k] = new GammaTxnLong(stm);
        }

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

        startAll(threads);
        sleepMs(getStressTestDurationMs(60 * 1000));
        stop = true;
        joinAll(threads);
    }

    public class ChangeThread extends TestThread {

        private final TxnExecutor normalBlock = stm.newTxnFactoryBuilder()
                .newTxnExecutor();

        private final TxnExecutor pessimisticReadLevelBlock = stm.newTxnFactoryBuilder()
                .setReadLockMode(LockMode.Exclusive)
                .newTxnExecutor();

        private final TxnExecutor pessimisticWriteLevelBlock = stm.newTxnFactoryBuilder()
                .setWriteLockMode(LockMode.Exclusive)
                .newTxnExecutor();

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

        @Override
        public void doRun() throws Exception {
            int k = 0;
            while (!stop) {
                if (k % 100000 == 0) {
                    System.out.printf("%s is at %s\n", getName(), k);
                }
                switch (mode) {
                    case PrivatizeReadLevelMode:
                        privatizeReadLevel();
                        break;
                    case PrivatizeWriteLevelMode:
                        privatizeWriteLevel();
                        break;
                    case Normal:
                        normal();
                        break;
                    case Mix:
                        switch (randomInt(3)) {
                            case 0:
                                privatizeReadLevel();
                                break;
                            case 1:
                                privatizeWriteLevel();
                                break;
                            case 2:
                                normal();
                                break;
                            default:
                                throw new IllegalStateException();
                        }
                        break;
                    default:
                        throw new IllegalStateException();

                }
                k++;
            }
        }

        public void normal() {
            normalBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    doIt((GammaTxn) tx);
                }
            });
        }

        public void privatizeReadLevel() {
            pessimisticReadLevelBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    doIt((GammaTxn) tx);
                }
            });
        }

        public void privatizeWriteLevel() {
            pessimisticWriteLevelBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    doIt((GammaTxn) tx);
                }
            });
        }

        public void doIt(GammaTxn tx) {
            for (int k = 0; k < refs.length; k++) {
                if (!randomOneOf(10)) {
                    continue;
                }

                int index = randomInt(refs.length);
                GammaTxnLong ref = refs[index];
                ref.getAndSet(tx, ref.get(tx) + 1);
            }
        }
    }
}
