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.TxnCallable;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.api.references.TxnBoolean;
import org.multiverse.api.references.TxnInteger;
import org.multiverse.api.references.TxnRef;
import org.multiverse.stms.gamma.GammaStm;

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

public abstract class ConditionVariable_AbstractTest {
    protected GammaStm stm;
    private Stack stack;
    private int itemCount = 10000000;

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

    protected abstract TxnExecutor newPopBlock();

    protected abstract TxnExecutor newPushBlock();


    public void run() {
        stack = new Stack(100);

        PushThread pushThread = new PushThread();
        PopThread popThread = new PopThread();

        startAll(pushThread, popThread);
        joinAll(pushThread, popThread);
    }

    public class PushThread extends TestThread {

        public PushThread() {
            super("PushThread");
        }

        @Override
        public void doRun() throws Exception {
            for (int k = 0; k < itemCount; k++) {
                stack.push("foo");

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

    public class PopThread extends TestThread {
        @Override
        public void doRun() throws Exception {
            for (int k = 0; k < itemCount; k++) {
                stack.pop();

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

    class Stack {
        ConditionVariable isNotFull = new ConditionVariable(true);
        ConditionVariable isNotEmpty = new ConditionVariable(false);
        TxnRef<Node> head = newTxnRef();
        TxnInteger size = newTxnInteger();
        final int capacity;
        final TxnExecutor pushBlock = newPushBlock();
        final TxnExecutor popBlock = newPopBlock();

        Stack(int capacity) {
            this.capacity = capacity;
        }

        void push(final String item) {
            pushBlock.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    isNotFull.awaitTrue();

                    head.set(new Node(item, head.get()));
                    size.increment();
                    if (size.get() == capacity) {
                        isNotFull.set(false);
                    }

                    isNotEmpty.set(true);
                }
            });
        }

        String pop() {
            return popBlock.execute(new TxnCallable<String>() {
                @Override
                public String call(Txn tx) throws Exception {
                    isNotEmpty.awaitTrue();

                    Node node = head.get();
                    head.set(node.next);
                    size.decrement();
                    if (size.get() == 0) {
                        isNotEmpty.set(false);
                    }
                    isNotFull.set(true);
                    return node.value;
                }
            });
        }
    }

    class Node {
        final String value;
        final Node next;

        Node(String value, Node next) {
            this.value = value;
            this.next = next;
        }
    }

    class ConditionVariable {
        final TxnBoolean ref;

        ConditionVariable(boolean value) {
            this.ref = newTxnBoolean(value);
        }

        void awaitTrue() {
            ref.await(true);
        }

        void awaitFalse() {
            ref.await(false);
        }

        void set(boolean value) {
            ref.set(value);
        }
    }
}
