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

import org.junit.Before;
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.GammaStm;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnRef;

import java.util.concurrent.atomic.AtomicLong;

import static org.junit.Assert.fail;
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 checks if a the Semaphore; a traditional synchronization structure can be build
 * using an STM.
 */
public abstract class Semaphore_AbstractTest {
    protected GammaStm stm;
    private volatile boolean stop;
    private int threadCount = 10;
    private int resourceCount = 5;
    private Semaphore semaphore;

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

    protected abstract TxnExecutor newDownBlock();

    protected abstract TxnExecutor newUpBlock();

    public void run() {
        semaphore = new Semaphore(resourceCount);

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

        startAll(workers);
        sleepMs(TestUtils.getStressTestDurationMs(30 * 1000));
        System.out.println("Terminating");
        stop = true;
        System.out.println(semaphore.ref.toDebugString());
        joinAll(workers);
    }

    class WorkerThread extends TestThread {
        long count;

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

        @Override
        public void doRun() throws Exception {
            while (!stop) {
                semaphore.down();
                semaphore.up();
                count++;

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

    class Semaphore {

        private GammaTxnRef<Long> ref;
        private AtomicLong users = new AtomicLong();
        private TxnExecutor upBlock = newUpBlock();
        private TxnExecutor downBlock = newDownBlock();

        public Semaphore(int initial) {
            ref = new GammaTxnRef<Long>(stm, new Long(initial));
        }

        public void up() {
            upBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    ref.set(ref.get() + 1);
                }
            });

            users.incrementAndGet();
            if (users.get() > resourceCount) {
                fail();
            }
        }

        public void down() {
            users.decrementAndGet();

            downBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    if (ref.get() == 0) {
                        retry();
                    }

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

}
