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

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.TxnCallable;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.stms.gamma.GammaConstants;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.transactionalobjects.GammaTxnRef;

import java.util.HashSet;
import java.util.LinkedList;

import static org.junit.Assert.assertEquals;
import static org.multiverse.TestUtils.joinAll;
import static org.multiverse.TestUtils.startAll;
import static org.multiverse.api.TxnThreadLocal.clearThreadLocalTxn;

/**
 * The test is not very efficient since a lot of temporary objects like the transaction template are created.
 * But that is alright for this test since it isn't a benchmark.
 *
 * @author Peter Veentjer.
 */
public abstract class StackWithoutCapacity_AbstractTest implements GammaConstants {

    public GammaStm stm;
    private int itemCount = 5 * 1000 * 1000;
    private Stack<Integer> stack;

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

    @After
    public void tearDown() {
        if (stack != null) {
            System.out.println(stack.head.toDebugString());
        }
    }

    public void run() {
        stack = new Stack<Integer>();

        ProduceThread produceThread = new ProduceThread();
        ConsumeThread consumeThread = new ConsumeThread();

        startAll(produceThread, consumeThread);
        joinAll(produceThread, consumeThread);

        System.out.println("finished executing, checking if content is correct (can take some time)");

        assertEquals(itemCount, produceThread.producedItems.size());

        assertEquals(
                new HashSet<Integer>(produceThread.producedItems),
                new HashSet<Integer>(consumeThread.consumedItems));

        System.out.println("Finished comparing content");
    }

    class ConsumeThread extends TestThread {

        private final LinkedList<Integer> consumedItems = new LinkedList<Integer>();

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

        @Override
        public void doRun() throws Exception {
            for (int k = 0; k < itemCount; k++) {
                int item = stack.pop();
                consumedItems.add(item);

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

    class ProduceThread extends TestThread {

        private final LinkedList<Integer> producedItems = new LinkedList<Integer>();

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

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

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

    class Stack<E> {
        private final GammaTxnRef<Node<E>> head = new GammaTxnRef<Node<E>>(stm);
        private final TxnExecutor pushExecutor = newPushTxnExecutor();

        private final TxnExecutor popExecutor = newPopTxnExecutor();

        public void push(final E item) {
            pushExecutor.execute(new TxnVoidCallable() {
                @Override
                public void call(Txn tx) throws Exception {
                    head.set(new Node<E>(item, head.get()));
                }
            });
        }

        public E pop() {
            return popExecutor.execute(new TxnCallable<E>() {
                @Override
                public E call(Txn tx) throws Exception {
                    Node<E> node = head.awaitNotNullAndGet();
                    head.set(node.next);
                    return node.item;
                }
            });
        }
    }

    protected abstract TxnExecutor newPopTxnExecutor();

    protected abstract TxnExecutor newPushTxnExecutor();

    class Node<E> {
        final E item;
        Node<E> next;

        Node(E item, Node<E> next) {
            this.item = item;
            this.next = next;
        }
    }
}
