package org.multiverse.stms.gamma.benchmarks;

import org.benchy.BenchmarkDriver;
import org.benchy.TestCaseResult;
import org.multiverse.TestThread;
import org.multiverse.api.Txn;
import org.multiverse.api.TxnExecutor;
import org.multiverse.api.LockMode;
import org.multiverse.api.callables.TxnBooleanCallable;
import org.multiverse.api.callables.TxnVoidCallable;
import org.multiverse.api.references.TxnInteger;
import org.multiverse.api.references.TxnRef;
import org.multiverse.stms.gamma.GammaStm;

import static org.benchy.BenchyUtils.format;
import static org.multiverse.TestUtils.joinAll;
import static org.multiverse.TestUtils.startAll;

public class SimpleStackDriver extends BenchmarkDriver {

    private int pushThreadCount = 1;
    private int popThreadCount = 1;
    private int capacity = Integer.MAX_VALUE;
    private boolean poolCallables = false;
    private LockMode readLockMode = LockMode.None;
    private LockMode writeLockMode = LockMode.None;
    private boolean dirtyCheck = false;

    private GammaStm stm;
    private PopThread[] popThreads;
    private PushThread[] pushThreads;
    private Stack<String> stack;

    @Override
    public void setUp() {
        System.out.printf("Multiverse > Pop threadcount %s\n", pushThreadCount);
        System.out.printf("Multiverse > Push threadcount %s\n", popThreadCount);
        if (capacity == Integer.MAX_VALUE) {
            System.out.printf("Multiverse > Capacity unbound\n");
        } else {
            System.out.printf("Multiverse > Capacity %s\n", capacity);
        }
        System.out.printf("Multiverse > Pool Callables %s\n", poolCallables);
        System.out.printf("Multiverse > LockLevel %s\n", readLockMode);
        System.out.printf("Multiverse > DirtyCheck %s\n", dirtyCheck);

        stm = new GammaStm();
        stack = new Stack<String>();

        pushThreads = new PushThread[pushThreadCount];
        for (int k = 0; k < pushThreadCount; k++) {
            pushThreads[k] = new PushThread(k, stack);
        }

        popThreads = new PopThread[popThreadCount];
        for (int k = 0; k < popThreadCount; k++) {
            popThreads[k] = new PopThread(k, stack);
        }
    }

    @Override
    public void run(TestCaseResult testCaseResult) {
        startAll(pushThreads);
        startAll(popThreads);

        joinAll(pushThreads);
        joinAll(popThreads);
    }

    @Override
    public void processResults(TestCaseResult testCaseResult) {
        long pushCount = 0;
        long totalDurationMs = 0;
        for (PushThread t : pushThreads) {
            pushCount += t.count;
            totalDurationMs += t.getDurationMs();
        }

        long popCount = 0;
        for (PopThread t : popThreads) {
            popCount += t.count;
            totalDurationMs += t.getDurationMs();
        }

        int threadCount = pushThreadCount + popThreadCount;
        long count = pushCount + popCount;
        System.out.printf("Multiverse > Total number of transactions %s\n", count);
        double transactionsPerSecond = (count * 1000.0d) / totalDurationMs;
        System.out.printf("Multiverse > Performance %s transactions/second with %s threads\n",
                format(transactionsPerSecond), threadCount);

        testCaseResult.put("transactionsPerSecond", transactionsPerSecond);
    }

    class PushThread extends TestThread {
        private final Stack<String> stack;
        private long count;
        private final TxnExecutor pushBlock = stm.newTxnFactoryBuilder()
                .setDirtyCheckEnabled(dirtyCheck)
                .setReadLockMode(readLockMode)
                .setWriteLockMode(writeLockMode)
                .newTxnExecutor();

        public PushThread(int id, Stack<String> stack) {
            super("PushThread-" + id);
            this.stack = stack;
        }

        @Override
        public void doRun() throws Exception {
            if (poolCallables) {
                runWithPooledCallables();
            } else {
                runWithoutPooledCallables();
            }
        }

        private void runWithoutPooledCallables() {
            while (!shutdown) {
                pushBlock.execute(new TxnVoidCallable() {
                    @Override
                    public void call(Txn tx) throws Exception {
                        stack.push(tx, "item");
                    }
                });
                count++;
            }


            for (int k = 0; k < popThreadCount; k++) {
                pushBlock.execute(new TxnVoidCallable() {
                    @Override
                    public void call(Txn tx) throws Exception {
                        stack.push(tx, "end");

                    }
                });
            }
        }

        private void runWithPooledCallables() {
            final PushCallable pushCallable = new PushCallable();

            while (!shutdown) {
                pushCallable.item = "item";
                pushBlock.execute(pushCallable);
                count++;
            }

            for (int k = 0; k < popThreadCount; k++) {
                pushCallable.item = "end";
                pushBlock.execute(pushCallable);
            }
        }

        class PushCallable implements TxnVoidCallable {
            String item;

            @Override
            public void call(Txn tx) throws Exception {
                stack.push(tx, item);
            }
        }
    }

    class PopThread extends TestThread {

        private final Stack<String> stack;
        private long count;
        private final TxnExecutor popBlock = stm.newTxnFactoryBuilder()
                .setDirtyCheckEnabled(dirtyCheck)
                .setReadLockMode(readLockMode)
                .setWriteLockMode(writeLockMode)
                .newTxnExecutor();

        public PopThread(int id, Stack<String> stack) {
            super("PopThread-" + id);
            this.stack = stack;
        }

        @Override
        public void doRun() throws Exception {
            if (poolCallables) {
                runWithoutPooledCallable();
            } else {
                runWithPooledCallable();
            }
        }

        private void runWithPooledCallable() {
            boolean end = false;
            while (!end) {
                end = popBlock.execute(new TxnBooleanCallable() {
                    @Override
                    public boolean call(Txn tx) throws Exception {
                        return !stack.pop(tx).equals("end");
                    }
                });

                count++;
            }
        }

        private void runWithoutPooledCallable() {
            PopCallable popCallable = new PopCallable();
            boolean end = false;
            while (!end) {
                end = popBlock.execute(popCallable);
                count++;
            }
        }

        class PopCallable implements TxnBooleanCallable {
            @Override
            public boolean call(Txn tx) throws Exception {
                return !stack.pop(tx).endsWith("end");
            }
        }
    }

    class Stack<E> {
        private final TxnRef<StackNode<E>> head = stm.getDefaultRefFactory().newTxnRef(null);
        private final TxnInteger size = stm.getTxRefFactoryBuilder().build().newTxnInteger(0);

        public void push(Txn tx, final E item) {
            if (capacity != Integer.MAX_VALUE) {
                if (size.get(tx) == capacity) {
                    tx.retry();
                }
                size.increment(tx);
            }
            head.set(tx, new StackNode<E>(item, head.get(tx)));
        }

        public E pop(Txn tx) {
            E value = head.awaitNotNullAndGet(tx).value;

            if (capacity != Integer.MAX_VALUE) {
                size.decrement(tx);
            }
            return value;
        }
    }

    static class StackNode<E> {
        final E value;
        final StackNode<E> next;

        StackNode(E value, StackNode<E> next) {
            this.value = value;
            this.next = next;
        }
    }
}
